Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ee835ea295 | |||
| 61d92052ac | |||
| 0ebd0a4a85 | |||
| 59c62b73e6 | |||
| a2bad29f2a | |||
| 0ddada4568 | |||
| 96ea2dd1ea | |||
| 8b6f444d55 | |||
| 29326f4b78 | |||
| e5aa13cef7 | |||
| 7479cbd656 | |||
| f760c8c9f7 | |||
| 8e14fb0f29 | |||
| 6346d563e0 | |||
| fd7f9f3a22 | |||
| e002c2fb4a | |||
| 0872271c8c | |||
| b9c9a3b73a | |||
| 7d694de307 | |||
| 3f951fef64 | |||
| 11b4d3c138 | |||
| bbf982c2f6 |
+23
-14
@@ -36,10 +36,15 @@ func _on_body_entered(body: Node3D) -> void:
|
|||||||
if not body.is_in_group(target_group):
|
if not body.is_in_group(target_group):
|
||||||
return
|
return
|
||||||
print("Container _on_body_entered body: ", body)
|
print("Container _on_body_entered body: ", body)
|
||||||
var picked_by = xr_pickable.get_picked_up_by()
|
|
||||||
if picked_by and picked_by.is_in_group("station"):
|
|
||||||
|
|
||||||
# If compatible and enough space, add item
|
|
||||||
|
# If one of us is in a station
|
||||||
|
var picked_by = xr_pickable.get_picked_up_by()
|
||||||
|
var body_pickable = body as XRToolsPickable
|
||||||
|
var body_picked_by = body_pickable.get_picked_up_by() if body_pickable else null
|
||||||
|
if (picked_by and picked_by.is_in_group("station")) or (body_picked_by and body_picked_by.is_in_group("station")):
|
||||||
|
|
||||||
|
# If enough space, add item
|
||||||
var food_item = body.get_node("FoodItem")
|
var food_item = body.get_node("FoodItem")
|
||||||
print("Container in station found %s of type %s: %s" % [target_group, FoodItem.Type.keys()[food_item.type], body.name])
|
print("Container in station found %s of type %s: %s" % [target_group, FoodItem.Type.keys()[food_item.type], body.name])
|
||||||
var meal_count := contained_items.filter(func(f): return f.type == FoodItem.Type.MEAL).size()
|
var meal_count := contained_items.filter(func(f): return f.type == FoodItem.Type.MEAL).size()
|
||||||
@@ -52,6 +57,7 @@ func _on_body_entered(body: Node3D) -> void:
|
|||||||
_add_item(body)
|
_add_item(body)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Contents are synced as data (ids on the plate's PlateController), not
|
# Contents are synced as data (ids on the plate's PlateController), not
|
||||||
# reparented nodes: reparenting a MultiplayerSpawner-tracked item out of
|
# reparented nodes: reparenting a MultiplayerSpawner-tracked item out of
|
||||||
# WorldContent would despawn it on every client the instant it happened.
|
# WorldContent would despawn it on every client the instant it happened.
|
||||||
@@ -148,19 +154,22 @@ func refresh_visuals(ids: Array[String]) -> void:
|
|||||||
visual.rotation = positions[idx].rotation
|
visual.rotation = positions[idx].rotation
|
||||||
|
|
||||||
|
|
||||||
# Strip interactivity/networking from a display-only copy: it's not spawned
|
# Strip interactivity from a display-only copy. It is instantiated straight from
|
||||||
# through NetworkManager, so it must never try to sync (its NetPickable child,
|
# its scene rather than spawned through NetWorld, so it is not part of the
|
||||||
# if any, would have no corresponding replicated identity on other peers) or
|
# replicated world at all — it exists only to be looked at, and must not be
|
||||||
# be grabbable/collidable.
|
# grabbable or collidable.
|
||||||
|
#
|
||||||
|
# It gets no synchronizers either: NetReplication.attach only ever runs for
|
||||||
|
# direct children of the content root, and this is parented under a plate.
|
||||||
func _make_cosmetic(visual: Node3D) -> void:
|
func _make_cosmetic(visual: Node3D) -> void:
|
||||||
var net_pickable := visual.get_node_or_null("NetPickable")
|
var despawn_timer := visual.get_node_or_null("DespawningItem")
|
||||||
if net_pickable:
|
if despawn_timer:
|
||||||
# Detach and free it outright rather than queue_free(): this runs before
|
# Detached and freed outright rather than queue_free()d: this runs before
|
||||||
# `visual` is added to the tree, and a merely-queued node still enters the
|
# `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)
|
# tree with its parent and runs a frame of _process before the queued
|
||||||
# before the queued deletion lands at the end of the frame.
|
# deletion lands at the end of the frame.
|
||||||
visual.remove_child(net_pickable)
|
visual.remove_child(despawn_timer)
|
||||||
net_pickable.free()
|
despawn_timer.free()
|
||||||
if visual is RigidBody3D:
|
if visual is RigidBody3D:
|
||||||
visual.freeze = true
|
visual.freeze = true
|
||||||
# STATIC, not KINEMATIC: a kinematic body is still driven by the physics
|
# STATIC, not KINEMATIC: a kinematic body is still driven by the physics
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
[ext_resource type="Script" uid="uid://6lyhyial1fh5" path="res://Containers/container.gd" id="1_r4cle"]
|
[ext_resource type="Script" uid="uid://6lyhyial1fh5" path="res://Containers/container.gd" id="1_r4cle"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
|
||||||
radius = 0.2
|
radius = 0.3
|
||||||
|
|
||||||
[node name="Container" type="Node3D" unique_id=377793422 node_paths=PackedStringArray("meal_positions")]
|
[node name="Container" type="Node3D" unique_id=377793422 node_paths=PackedStringArray("meal_positions")]
|
||||||
script = ExtResource("1_r4cle")
|
script = ExtResource("1_r4cle")
|
||||||
|
|||||||
@@ -8,7 +8,6 @@
|
|||||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_dhtvl"]
|
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="4_dhtvl"]
|
||||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vlcpl"]
|
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_vlcpl"]
|
||||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_el51w"]
|
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_el51w"]
|
||||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_kek77"]
|
||||||
height = 0.0635376
|
height = 0.0635376
|
||||||
@@ -27,23 +26,6 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
|||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
|
||||||
albedo_color = Color(0.36656043, 0.16690676, 0.12531222, 1)
|
albedo_color = Color(0.36656043, 0.16690676, 0.12531222, 1)
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_plate"]
|
|
||||||
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
|
|
||||||
properties/3/path = NodePath("PlateController:contained_ids")
|
|
||||||
properties/3/spawn = true
|
|
||||||
properties/3/replication_mode = 1
|
|
||||||
properties/4/path = NodePath("PlateController:is_dirty")
|
|
||||||
properties/4/spawn = true
|
|
||||||
properties/4/replication_mode = 1
|
|
||||||
|
|
||||||
[node name="Plate" type="RigidBody3D" unique_id=190487773]
|
[node name="Plate" type="RigidBody3D" unique_id=190487773]
|
||||||
collision_layer = 4
|
collision_layer = 4
|
||||||
collision_mask = 196615
|
collision_mask = 196615
|
||||||
@@ -153,7 +135,3 @@ polygon = PackedVector2Array(-0.053057775, 0.2586278, 0.09469998, 0.40791017, 0.
|
|||||||
depth = 0.02
|
depth = 0.02
|
||||||
material = SubResource("StandardMaterial3D_vlqg6")
|
material = SubResource("StandardMaterial3D_vlqg6")
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_plate")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
@@ -30,11 +30,11 @@ func _process(_delta: float) -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _set_contained_ids(value: Array[String]) -> void:
|
func _set_contained_ids(value: Array[String]) -> void:
|
||||||
# Only rebuild when the contents actually changed. This property is
|
# Only rebuild when the contents actually changed. refresh_visuals() frees and
|
||||||
# replicated in ALWAYS mode, so the synchronizer assigns it every network
|
# re-instantiates a scene per item, so a redundant call is thousands of
|
||||||
# tick on every peer that doesn't own the plate — and refresh_visuals()
|
# throwaway nodes over a session. NetReplication uses ON_CHANGE for state, so
|
||||||
# frees and re-instantiates a scene per item each time. That was thousands
|
# the synchronizer should not be assigning this unless it really changed — but
|
||||||
# of throwaway nodes per run (and a log line from each one's NetPickable).
|
# a local write can still repeat a value, and this stays cheap either way.
|
||||||
if contained_ids == value:
|
if contained_ids == value:
|
||||||
return
|
return
|
||||||
contained_ids = value
|
contained_ids = value
|
||||||
|
|||||||
+2
-15
@@ -8,7 +8,7 @@
|
|||||||
[ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="6_ftab3"]
|
[ext_resource type="Animation" uid="uid://bediglpx0rj7i" path="res://addons/godot-xr-tools/hands/animations/left/Grip 5.res" id="6_ftab3"]
|
||||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_wb51u"]
|
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_wb51u"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="8_t9y3x"]
|
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="8_t9y3x"]
|
||||||
[ext_resource type="Script" uid="uid://bwd0pe2udb5xo" path="res://Net/net_pickable.gd" id="20_netpk"]
|
[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://Prefabs/despawning_item.tscn" id="10_46e7r"]
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"]
|
||||||
height = 0.10708985
|
height = 0.10708985
|
||||||
@@ -27,17 +27,6 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
|||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"]
|
||||||
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_buns"]
|
|
||||||
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="BurgerBuns" unique_id=1088240294 instance=ExtResource("1_g1t48")]
|
[node name="BurgerBuns" unique_id=1088240294 instance=ExtResource("1_g1t48")]
|
||||||
|
|
||||||
[node name="CollisionShape3D" parent="." index="0"]
|
[node name="CollisionShape3D" parent="." index="0"]
|
||||||
@@ -85,6 +74,4 @@ type = 2
|
|||||||
|
|
||||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_wb51u")]
|
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_wb51u")]
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="." index="6" unique_id=2086885420]
|
[node name="DespawningItem" parent="." index="7" unique_id=303090111 instance=ExtResource("10_46e7r")]
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_buns")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
+2
-16
@@ -8,7 +8,7 @@
|
|||||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"]
|
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_cp3eg"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_mde49"]
|
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_mde49"]
|
||||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="8_wmrff"]
|
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="8_wmrff"]
|
||||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://Prefabs/despawning_item.tscn" id="10_6ypk4"]
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
|
||||||
height = 0.1
|
height = 0.1
|
||||||
@@ -33,17 +33,6 @@ script = ExtResource("4_mgacb")
|
|||||||
closed_pose = ExtResource("6_cp3eg")
|
closed_pose = ExtResource("6_cp3eg")
|
||||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_charcoal"]
|
|
||||||
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="PickableObject" unique_id=1675596942 instance=ExtResource("1_r73y2")]
|
[node name="PickableObject" unique_id=1675596942 instance=ExtResource("1_r73y2")]
|
||||||
|
|
||||||
[node name="CollisionShape3D" parent="." index="0"]
|
[node name="CollisionShape3D" parent="." index="0"]
|
||||||
@@ -66,7 +55,4 @@ type = 2
|
|||||||
|
|
||||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("8_wmrff")]
|
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("8_wmrff")]
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
[node name="DespawningItem" parent="." index="7" unique_id=303090111 instance=ExtResource("10_6ypk4")]
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_charcoal")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
+2
-16
@@ -7,8 +7,8 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_a0a5b"]
|
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="5_a0a5b"]
|
||||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"]
|
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="6_bdp75"]
|
||||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_bdp75"]
|
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="7_bdp75"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://Prefabs/despawning_item.tscn" id="10_uygc5"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"]
|
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="11_vjw41"]
|
||||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_m1xbq"]
|
||||||
size = Vector3(0.1, 0.1, 0.1)
|
size = Vector3(0.1, 0.1, 0.1)
|
||||||
@@ -30,17 +30,6 @@ script = ExtResource("4_hc3f7")
|
|||||||
closed_pose = ExtResource("6_bdp75")
|
closed_pose = ExtResource("6_bdp75")
|
||||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_cube"]
|
|
||||||
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="PickableObject" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_dbtw8")]
|
[node name="PickableObject" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_dbtw8")]
|
||||||
|
|
||||||
[node name="CollisionShape3D" parent="." index="0"]
|
[node name="CollisionShape3D" parent="." index="0"]
|
||||||
@@ -63,7 +52,4 @@ type = 1
|
|||||||
|
|
||||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_bdp75")]
|
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_bdp75")]
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
[node name="DespawningItem" parent="." index="7" unique_id=303090111 instance=ExtResource("10_uygc5")]
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_cube")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
+2
-16
@@ -7,8 +7,8 @@
|
|||||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_w8sii"]
|
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_w8sii"]
|
||||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"]
|
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_mvnl5"]
|
||||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"]
|
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_wqxjj"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://Prefabs/despawning_item.tscn" id="10_l4hsd"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_yxtxs"]
|
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_yxtxs"]
|
||||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||||
height = 0.0338974
|
height = 0.0338974
|
||||||
@@ -27,17 +27,6 @@ script = ExtResource("5_w8sii")
|
|||||||
closed_pose = ExtResource("7_wqxjj")
|
closed_pose = ExtResource("7_wqxjj")
|
||||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_burger"]
|
|
||||||
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="raw_burger" unique_id=1675596942 instance=ExtResource("1_fco8w")]
|
[node name="raw_burger" unique_id=1675596942 instance=ExtResource("1_fco8w")]
|
||||||
|
|
||||||
[node name="CollisionShape3D" parent="." index="0"]
|
[node name="CollisionShape3D" parent="." index="0"]
|
||||||
@@ -62,7 +51,4 @@ hand_pose = SubResource("Resource_qyiot")
|
|||||||
id = "raw_burger"
|
id = "raw_burger"
|
||||||
type = 2
|
type = 2
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
[node name="DespawningItem" parent="." index="6" unique_id=303090111 instance=ExtResource("10_l4hsd")]
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_burger")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@
|
|||||||
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_ychvb"]
|
[ext_resource type="Script" uid="uid://dvobm6vcfnqe8" path="res://addons/godot-xr-tools/hands/poses/hand_pose_settings.gd" id="5_ychvb"]
|
||||||
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_etdv2"]
|
[ext_resource type="PackedScene" uid="uid://ctw7nbntd5pcj" path="res://addons/godot-xr-tools/objects/grab_points/grab_point_hand_right.tscn" id="6_etdv2"]
|
||||||
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_rl64h"]
|
[ext_resource type="Animation" uid="uid://d1xnpyc08njjx" path="res://addons/godot-xr-tools/hands/animations/right/Grip 4.res" id="7_rl64h"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://Prefabs/despawning_item.tscn" id="10_6p8pj"]
|
||||||
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="10_ut7mg"]
|
[ext_resource type="PackedScene" uid="uid://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="10_ut7mg"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_zz42p"]
|
[ext_resource type="PackedScene" uid="uid://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="10_zz42p"]
|
||||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||||
height = 0.04777527
|
height = 0.04777527
|
||||||
@@ -27,17 +27,6 @@ script = ExtResource("5_ychvb")
|
|||||||
closed_pose = ExtResource("7_rl64h")
|
closed_pose = ExtResource("7_rl64h")
|
||||||
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_cookedburger"]
|
|
||||||
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="CookedBurger" unique_id=1675596942 instance=ExtResource("1_ut7mg")]
|
[node name="CookedBurger" unique_id=1675596942 instance=ExtResource("1_ut7mg")]
|
||||||
|
|
||||||
[node name="CollisionShape3D" parent="." index="0"]
|
[node name="CollisionShape3D" parent="." index="0"]
|
||||||
@@ -64,7 +53,4 @@ type = 2
|
|||||||
|
|
||||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("10_ut7mg")]
|
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("10_ut7mg")]
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
[node name="DespawningItem" parent="." index="7" unique_id=303090111 instance=ExtResource("10_6p8pj")]
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_cookedburger")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
+2
-16
@@ -7,7 +7,7 @@
|
|||||||
[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="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="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://bfj80jh13t6e5" path="res://Prefabs/food_item.tscn" id="7_yy3y8"]
|
||||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
[ext_resource type="PackedScene" uid="uid://b5ukku8i0hilb" path="res://Prefabs/despawning_item.tscn" id="9_ej8jm"]
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||||
height = 0.10392761
|
height = 0.10392761
|
||||||
@@ -29,17 +29,6 @@ albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
|||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"]
|
||||||
albedo_color = Color(0.29, 0.101500005, 0, 1)
|
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=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")]
|
||||||
gravity_scale = 0.04
|
gravity_scale = 0.04
|
||||||
|
|
||||||
@@ -94,7 +83,4 @@ id = "hamburger"
|
|||||||
type = 0
|
type = 0
|
||||||
sell_value = 4
|
sell_value = 4
|
||||||
|
|
||||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
[node name="DespawningItem" parent="." index="7" unique_id=303090111 instance=ExtResource("9_ej8jm")]
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_hamburger")
|
|
||||||
script = ExtResource("20_netpk")
|
|
||||||
|
|||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bv82nioivbmnx
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
# 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
|
|
||||||
|
|
||||||
|
|
||||||
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
|
|
||||||
_original_enabled = _pickable.enabled
|
|
||||||
_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:
|
|
||||||
var old := net_held_by
|
|
||||||
net_held_by = value
|
|
||||||
if old != value and NetworkManager.is_online():
|
|
||||||
print("%s net_held_by: %d -> %d (local state: %s, authority=%d)" % [
|
|
||||||
_pickable.name, old, value, _holder_desc(), get_multiplayer_authority()
|
|
||||||
])
|
|
||||||
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.
|
|
||||||
##
|
|
||||||
## 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 NetworkManager.is_online() or 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
|
|
||||||
# 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():
|
|
||||||
var changed := _pickable.freeze_mode != _original_freeze_mode \
|
|
||||||
or _pickable.collision_mask != _pickable.original_collision_mask
|
|
||||||
if changed:
|
|
||||||
if NetworkManager.is_online():
|
|
||||||
print(
|
|
||||||
"%s: reclaiming ownership, restoring freeze_mode %d->%d collision_mask %d->%d" % [
|
|
||||||
_pickable.name, _pickable.freeze_mode, _original_freeze_mode,
|
|
||||||
_pickable.collision_mask, _pickable.original_collision_mask
|
|
||||||
]
|
|
||||||
)
|
|
||||||
_pickable.freeze_mode = _original_freeze_mode
|
|
||||||
_pickable.collision_mask = _pickable.original_collision_mask
|
|
||||||
# Unlike freeze/collision (which XRToolsPickable manages itself while
|
|
||||||
# held), `enabled` is only ever written by the non-authority branch
|
|
||||||
# below, so it must be restored here or it stays false forever: once a
|
|
||||||
# client grabbed this item, every other peer set enabled=false, and
|
|
||||||
# regaining authority left it that way. On the server that silently
|
|
||||||
# broke everything downstream — hands couldn't pick the item up again,
|
|
||||||
# and a station snap zone would "snap" it (emitting has_picked_up, so
|
|
||||||
# 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.
|
|
||||||
if _pickable.enabled != _original_enabled:
|
|
||||||
if NetworkManager.is_online():
|
|
||||||
print("%s: reclaiming ownership, restoring enabled %s->%s" % [
|
|
||||||
_pickable.name, _pickable.enabled, _original_enabled
|
|
||||||
])
|
|
||||||
_pickable.enabled = _original_enabled
|
|
||||||
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:
|
|
||||||
# Log once per grab, not once per tick.
|
|
||||||
if NetworkManager.is_online() and not _grab_race_logged:
|
|
||||||
_grab_race_logged = true
|
|
||||||
print(
|
|
||||||
"%s: ignoring non-authority sync (net_held_by=%d) — 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():
|
|
||||||
print(
|
|
||||||
"%s: was held by %s on this peer, but authority now says peer %d owns it — force-dropping" % [
|
|
||||||
_pickable.name, _holder_desc(), net_held_by
|
|
||||||
]
|
|
||||||
)
|
|
||||||
_pickable.drop()
|
|
||||||
# Bail out when we're already in the follow-the-sync state. Without this the
|
|
||||||
# writes below (and the line logged with them) repeated every tick for every
|
|
||||||
# item on every non-authority peer — 90% of the log, plus four redundant
|
|
||||||
# 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)
|
|
||||||
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():
|
|
||||||
print("%s: freezing (non-authority, owner=peer %d)" % [_pickable.name, net_held_by])
|
|
||||||
_pickable.freeze = true
|
|
||||||
_pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
|
||||||
_pickable.collision_mask = 0
|
|
||||||
_pickable.enabled = want_enabled
|
|
||||||
|
|
||||||
|
|
||||||
## 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):
|
|
||||||
# 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.
|
|
||||||
if NetworkManager.is_online():
|
|
||||||
print("%s picked up by %s (not a hand) — no authority request" % [_pickable.name, _holder_desc()])
|
|
||||||
return
|
|
||||||
if NetworkManager.is_online():
|
|
||||||
print("%s grabbed by hand (authority was peer %d), requesting authority" % [_pickable.name, net_held_by])
|
|
||||||
NetworkManager.request_item_authority_from(_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():
|
|
||||||
if NetworkManager.is_online():
|
|
||||||
print("%s dropped locally, but we aren't its authority (peer %d is) — not reporting" % [_pickable.name, net_held_by])
|
|
||||||
return
|
|
||||||
if NetworkManager.is_online():
|
|
||||||
print("%s dropped, reporting release to server (lin=%s ang=%s)" % [
|
|
||||||
_pickable.name, _pickable.linear_velocity, _pickable.angular_velocity
|
|
||||||
])
|
|
||||||
# Send our own final transform too: we were the authority until now, and the
|
|
||||||
# server's copy may not have received our last position sync yet.
|
|
||||||
NetworkManager.release_item_authority_from(
|
|
||||||
_pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity,
|
|
||||||
_pickable.global_transform
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
## Human-readable description of what's currently holding this item on THIS
|
|
||||||
## peer, for diagnosing desyncs between a hand's own "what am I holding"
|
|
||||||
## bookkeeping and the item's actual grab state (see the snap-zone-grab race
|
|
||||||
## in NetworkManager._try_snap_into_station for a real example).
|
|
||||||
func _holder_desc() -> String:
|
|
||||||
if not _pickable or not _pickable.is_picked_up():
|
|
||||||
return "loose"
|
|
||||||
var by := _pickable.get_picked_up_by()
|
|
||||||
if not by:
|
|
||||||
return "held(no grabber?)"
|
|
||||||
if by is XRToolsFunctionPickup:
|
|
||||||
return "hand(%s)" % by.get_path()
|
|
||||||
if by is XRToolsSnapZone:
|
|
||||||
var station := by.get_parent()
|
|
||||||
return "zone(%s)" % (station.name if station else str(by.get_path()))
|
|
||||||
return "other(%s: %s)" % [by.get_class(), by.get_path()]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
uid://bwd0pe2udb5xo
|
|
||||||
@@ -0,0 +1,188 @@
|
|||||||
|
extends Node
|
||||||
|
class_name NetReplication
|
||||||
|
|
||||||
|
## Builds MultiplayerSynchronizers for any node by convention, so no scene needs
|
||||||
|
## a hand-authored replication config.
|
||||||
|
##
|
||||||
|
## Every replicated node gets TWO synchronizers, both generated here:
|
||||||
|
##
|
||||||
|
## NetSync script state (is_dirty, contained_ids, time_cooked, ...).
|
||||||
|
## Always owned by the server. Gameplay outcomes are the server's to
|
||||||
|
## decide, so this never changes hands.
|
||||||
|
## NetXform position + quaternion. Owned by the server while the object is
|
||||||
|
## loose, and handed to a peer for as long as that peer holds it.
|
||||||
|
##
|
||||||
|
## Splitting them is what makes client-side grab prediction work. A
|
||||||
|
## MultiplayerSynchronizer never applies inbound state on the peer that owns it,
|
||||||
|
## so giving the holder NetXform means their own hand drives the object with no
|
||||||
|
## round trip and no fight with the server's copy — while is_dirty and friends
|
||||||
|
## keep flowing one way, server to client, exactly as before.
|
||||||
|
##
|
||||||
|
## The obvious alternative, per-peer visibility (set_visibility_for), does NOT
|
||||||
|
## work: MultiplayerSpawner also uses synchronizer visibility to decide whether
|
||||||
|
## a node should exist on a peer, so hiding an object from its holder despawns
|
||||||
|
## it in their hand. Verified in test/spike/.
|
||||||
|
##
|
||||||
|
## The convention for what gets replicated:
|
||||||
|
## * a Node3D replicates `position` and `quaternion` (never `scale` — nothing
|
||||||
|
## in this game animates scale as gameplay state)
|
||||||
|
## * every script variable, on the node and on any scripted descendant, whose
|
||||||
|
## name does not start with "_" and whose declared type is serialisable
|
||||||
|
##
|
||||||
|
## Underscore-prefixed vars are the opt-out, and they already mark exactly the
|
||||||
|
## state that must not be replicated: @onready node references, cached lookups,
|
||||||
|
## per-frame bookkeeping.
|
||||||
|
|
||||||
|
## SceneReplicationConfig's replication modes. The enum is not exposed under a
|
||||||
|
## friendly name in GDScript, so they are spelled out here.
|
||||||
|
const MODE_ALWAYS := 1
|
||||||
|
const MODE_ON_CHANGE := 2
|
||||||
|
|
||||||
|
const SYNC_NAME := "NetSync"
|
||||||
|
const XFORM_NAME := "NetXform"
|
||||||
|
|
||||||
|
## Types that survive a network round trip. Object/Callable/Signal/RID cannot be
|
||||||
|
## serialised, and are exactly what @onready and cached references hold.
|
||||||
|
const SYNCABLE_TYPES := [
|
||||||
|
TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING, TYPE_STRING_NAME,
|
||||||
|
TYPE_VECTOR2, TYPE_VECTOR3, TYPE_QUATERNION, TYPE_TRANSFORM3D,
|
||||||
|
TYPE_COLOR, TYPE_PACKED_STRING_ARRAY, TYPE_ARRAY,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
## Adds both synchronizers to `node`, unless they are already there. Idempotent.
|
||||||
|
##
|
||||||
|
## Must run on EVERY peer, not just the authority: the client's copy is built by
|
||||||
|
## MultiplayerSpawner straight from the .tscn, so it only has synchronizers if
|
||||||
|
## something puts them there. NetWorld calls this from the content root's
|
||||||
|
## child_entered_tree on both sides — early enough that they enter the tree
|
||||||
|
## inside the spawner's own add_child(), which is what lets them pick up the
|
||||||
|
## spawn payload (verified in test/spike/: a late joiner receives position and
|
||||||
|
## script state from a synchronizer that exists only at runtime).
|
||||||
|
static func attach(node: Node) -> void:
|
||||||
|
if node is Node3D and not node.has_node(XFORM_NAME):
|
||||||
|
_add_sync(node, XFORM_NAME, _transform_config())
|
||||||
|
if not node.has_node(SYNC_NAME):
|
||||||
|
_add_sync(node, SYNC_NAME, state_config(node))
|
||||||
|
|
||||||
|
|
||||||
|
static func _add_sync(node: Node, sync_name: String, config: SceneReplicationConfig) -> void:
|
||||||
|
var sync := MultiplayerSynchronizer.new()
|
||||||
|
sync.name = sync_name
|
||||||
|
sync.replication_config = config
|
||||||
|
# Sync every network tick. ON_CHANGE properties are only sent when they
|
||||||
|
# actually change regardless of this interval.
|
||||||
|
sync.replication_interval = 0.0
|
||||||
|
# Inherit the node's authority rather than defaulting to the server. Items are
|
||||||
|
# server-owned and this changes nothing for them, but a player avatar sets its
|
||||||
|
# authority to the peer it belongs to in _enter_tree — which has already run by
|
||||||
|
# the time we get here — and its synchronizer has to agree, or the owning peer
|
||||||
|
# would be receiving its own head and hands back from the server.
|
||||||
|
sync.set_multiplayer_authority(node.get_multiplayer_authority())
|
||||||
|
node.add_child(sync)
|
||||||
|
|
||||||
|
|
||||||
|
## Transform-only config. ALWAYS rather than ON_CHANGE: a carried or simulated
|
||||||
|
## object changes every tick anyway, so ON_CHANGE would only add a comparison
|
||||||
|
## per property per tick. spawn = true so a replicated object arrives already in
|
||||||
|
## the right place instead of sitting at the origin for a frame.
|
||||||
|
static func _transform_config() -> SceneReplicationConfig:
|
||||||
|
var config := SceneReplicationConfig.new()
|
||||||
|
_add(config, ".:position", true, MODE_ALWAYS)
|
||||||
|
_add(config, ".:quaternion", true, MODE_ALWAYS)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
## Script-state config for `node` and its scripted descendants. Public so the
|
||||||
|
## tests can inspect what the convention picked up without spinning up a session.
|
||||||
|
##
|
||||||
|
## ON_CHANGE, not ALWAYS: the value is then only sent — and, crucially, only
|
||||||
|
## ASSIGNED on the receiving peer — when it actually changes. Under ALWAYS every
|
||||||
|
## replicated setter becomes a per-tick hot path, which is how this project
|
||||||
|
## previously ended up rebuilding every plate's visuals 60 times a second.
|
||||||
|
static func state_config(node: Node) -> SceneReplicationConfig:
|
||||||
|
var config := SceneReplicationConfig.new()
|
||||||
|
for path in _script_var_paths(node, node):
|
||||||
|
_add(config, path, true, MODE_ON_CHANGE)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
static func _add(config: SceneReplicationConfig, path: String, spawn: bool, mode: int) -> void:
|
||||||
|
var np := NodePath(path)
|
||||||
|
config.add_property(np)
|
||||||
|
config.property_set_spawn(np, spawn)
|
||||||
|
config.property_set_replication_mode(np, mode)
|
||||||
|
|
||||||
|
|
||||||
|
## Whether a script comes from a third-party addon rather than this game.
|
||||||
|
##
|
||||||
|
## Addon components are local plumbing, and replicating their configuration is
|
||||||
|
## not merely wasteful — it is actively wrong. godot-xr-tools' snap zones and
|
||||||
|
## pickables both expose a public `enabled`, which is precisely the flag each
|
||||||
|
## peer has to set for ITSELF: a client disables its stations' snap zones because
|
||||||
|
## placement is the server's decision, and disables a pickable another player is
|
||||||
|
## holding. Replicating those meant the server helpfully sent `enabled = true`
|
||||||
|
## straight back over every client's gate, so stations on clients went on
|
||||||
|
## grabbing objects out of the local player's hands.
|
||||||
|
##
|
||||||
|
## Their values are authored in the scene file and therefore already identical on
|
||||||
|
## every peer, so nothing is lost by leaving them alone.
|
||||||
|
static func _is_addon(script: Script) -> bool:
|
||||||
|
return script.resource_path.begins_with("res://addons/")
|
||||||
|
|
||||||
|
|
||||||
|
## Whether a typed array holds something that can cross the wire.
|
||||||
|
##
|
||||||
|
## An array of nodes cannot, and this game has several: ItemContainer's
|
||||||
|
## meal_positions/side_positions are Array[Node3D], and contained_items is
|
||||||
|
## Array[FoodItem]. Replicating one would try to serialise live node references.
|
||||||
|
##
|
||||||
|
## GDScript reports the element type three different ways, so all three are
|
||||||
|
## handled here (verified against the real scripts):
|
||||||
|
## Array[String] hint 23, hint_string "4:" -> element type 4
|
||||||
|
## Array[Node3D] hint 23, hint_string "24/34:Node3D" -> element type 24
|
||||||
|
## Array[FoodItem] hint 31, hint_string "FoodItem" -> a class name
|
||||||
|
## An untyped Array reports an empty hint_string and is excluded too: it can hold
|
||||||
|
## anything, including nodes, so there is no safe answer.
|
||||||
|
static func _is_syncable_array(prop: Dictionary) -> bool:
|
||||||
|
var hint_string := str(prop["hint_string"])
|
||||||
|
if hint_string.is_empty():
|
||||||
|
return false
|
||||||
|
var head := hint_string.split(":")[0].split("/")[0]
|
||||||
|
if not head.is_valid_int():
|
||||||
|
# A bare class name, e.g. "FoodItem".
|
||||||
|
return false
|
||||||
|
return SYNCABLE_TYPES.has(head.to_int()) and head.to_int() != TYPE_ARRAY
|
||||||
|
|
||||||
|
|
||||||
|
## Every "<relative path>:<var>" on `node` and its scripted descendants.
|
||||||
|
## Descends through children but stops at anything carrying its own synchronizer
|
||||||
|
## — that subtree replicates itself and must not be replicated twice.
|
||||||
|
static func _script_var_paths(root: Node, node: Node) -> Array[String]:
|
||||||
|
var paths: Array[String] = []
|
||||||
|
var prefix: String = "." if node == root else str(root.get_path_to(node))
|
||||||
|
var script: Script = node.get_script() as Script
|
||||||
|
# Note the descent below still happens for an addon-scripted node — only its
|
||||||
|
# own properties are skipped. plate.tscn's root is godot-xr-tools' pickable.gd
|
||||||
|
# and its PlateController child is where the game state actually lives.
|
||||||
|
if script and _is_addon(script):
|
||||||
|
script = null
|
||||||
|
if script:
|
||||||
|
for prop in script.get_script_property_list():
|
||||||
|
var prop_name := str(prop["name"])
|
||||||
|
if prop_name.begins_with("_"):
|
||||||
|
continue
|
||||||
|
if not (prop["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE):
|
||||||
|
continue
|
||||||
|
if not SYNCABLE_TYPES.has(prop["type"]):
|
||||||
|
continue
|
||||||
|
if prop["type"] == TYPE_ARRAY and not _is_syncable_array(prop):
|
||||||
|
continue
|
||||||
|
paths.append("%s:%s" % [prefix, prop_name])
|
||||||
|
for child in node.get_children():
|
||||||
|
if child is MultiplayerSynchronizer or child is MultiplayerSpawner:
|
||||||
|
continue
|
||||||
|
if child.has_node(SYNC_NAME) or child.has_node(XFORM_NAME):
|
||||||
|
continue
|
||||||
|
paths.append_array(_script_var_paths(root, child))
|
||||||
|
return paths
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bmuqb0yywjfyw
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cgp15pxa7ui1r
|
||||||
@@ -0,0 +1,251 @@
|
|||||||
|
extends Node
|
||||||
|
class_name NetWorld
|
||||||
|
|
||||||
|
## Owns the replicated contents of the world: what exists, where it lives, and
|
||||||
|
## which peer is allowed to simulate it.
|
||||||
|
##
|
||||||
|
## Everything here runs on Godot's stock MultiplayerSpawner in its DEFAULT mode.
|
||||||
|
## There is no spawn_function and no payload dictionary: the server instantiates
|
||||||
|
## a scene, sets it up, and adds it under the spawn path — the spawner replicates
|
||||||
|
## the creation (and later the deletion) to every peer, including late joiners,
|
||||||
|
## and the synchronizers NetReplication attaches carry the state.
|
||||||
|
##
|
||||||
|
## The two hooks that make this generic, rather than something each object opts
|
||||||
|
## into:
|
||||||
|
##
|
||||||
|
## * content_root.child_entered_tree -> NetReplication.attach, on EVERY peer.
|
||||||
|
## The client's copy is built by the spawner straight from the .tscn, so this
|
||||||
|
## is what gives it synchronizers at all. It also runs early enough for them
|
||||||
|
## to pick up the spawn payload.
|
||||||
|
## * the same hook, on clients only -> _gate. One rule decides what a client is
|
||||||
|
## not allowed to simulate, for any object, whatever it happens to be.
|
||||||
|
|
||||||
|
## Directories scanned for spawnable scenes. Anything in them can be spawned
|
||||||
|
## over the network without being registered by hand anywhere.
|
||||||
|
const SPAWNABLE_DIRS := [
|
||||||
|
"res://Items/",
|
||||||
|
"res://Containers/",
|
||||||
|
"res://Stations/",
|
||||||
|
"res://Prefabs/",
|
||||||
|
]
|
||||||
|
|
||||||
|
var _spawner: MultiplayerSpawner
|
||||||
|
var _content_root: Node
|
||||||
|
|
||||||
|
|
||||||
|
## Wires this up to the world scene's spawner and content root. Must run on every
|
||||||
|
## peer BEFORE it connects, so the spawnable list is identical by the time any
|
||||||
|
## spawn packet can arrive.
|
||||||
|
func setup(spawner: MultiplayerSpawner, content_root: Node) -> void:
|
||||||
|
_spawner = spawner
|
||||||
|
_content_root = content_root
|
||||||
|
_register_spawnables()
|
||||||
|
_content_root.child_entered_tree.connect(_on_content_child_entered)
|
||||||
|
|
||||||
|
|
||||||
|
## Registers every scene under SPAWNABLE_DIRS, sorted.
|
||||||
|
##
|
||||||
|
## The sort is not cosmetic. Auto-spawn puts an INDEX into this list on the wire,
|
||||||
|
## not a path, so a peer whose list is ordered differently instantiates the wrong
|
||||||
|
## scene entirely. Directory listing order is not guaranteed to match across
|
||||||
|
## machines, so it is pinned here.
|
||||||
|
func _register_spawnables() -> void:
|
||||||
|
var scenes: Array[String] = []
|
||||||
|
for dir in SPAWNABLE_DIRS:
|
||||||
|
for file in ResourceLoader.list_directory(dir):
|
||||||
|
if file.ends_with(".tscn"):
|
||||||
|
scenes.append(dir + file)
|
||||||
|
scenes.sort()
|
||||||
|
for scene in scenes:
|
||||||
|
_spawner.add_spawnable_scene(scene)
|
||||||
|
NetworkManager.log_line("Registered %d spawnable scenes" % scenes.size())
|
||||||
|
|
||||||
|
|
||||||
|
func _on_content_child_entered(node: Node) -> void:
|
||||||
|
# Our own synchronizers re-enter here as children of the node, not of the
|
||||||
|
# content root, so this only ever sees spawned roots — but the guard is cheap
|
||||||
|
# and makes the intent explicit.
|
||||||
|
if node is MultiplayerSynchronizer:
|
||||||
|
return
|
||||||
|
NetReplication.attach(node)
|
||||||
|
# Grab handling is wired up centrally, so no scene has to carry a networking
|
||||||
|
# component to be pickable over the network.
|
||||||
|
if node is XRToolsPickable:
|
||||||
|
NetGrab.watch(node)
|
||||||
|
if not NetworkManager.owns_world():
|
||||||
|
_gate(node)
|
||||||
|
# Deferred, in this order, because both depend on the node being fully
|
||||||
|
# constructed: XRToolsPickable captures original_collision_mask in an @onready,
|
||||||
|
# which has not run yet at child_entered_tree time. Reading it now would
|
||||||
|
# record 0 as the object's authored collision mask and it would never collide
|
||||||
|
# with anything again.
|
||||||
|
_remember_authored.call_deferred(node)
|
||||||
|
apply_physics_role.call_deferred(node)
|
||||||
|
|
||||||
|
|
||||||
|
# --- spawning --------------------------------------------------------------
|
||||||
|
|
||||||
|
## Creates a networked object. Server-only when online (the spawner replicates
|
||||||
|
## it from there); works directly when offline.
|
||||||
|
##
|
||||||
|
## `props` are applied BEFORE the node enters the tree, so they ride the spawn
|
||||||
|
## packet as the synchronizer's spawn properties and every peer builds the object
|
||||||
|
## already configured. Note that this means their setters run before the node is
|
||||||
|
## in the tree, where @onready references are still null — any setter reachable
|
||||||
|
## this way has to be null-guarded.
|
||||||
|
func spawn(scene_path: String, xform: Transform3D, node_name: String = "", props: Dictionary = {}) -> Node:
|
||||||
|
if not NetworkManager.owns_world():
|
||||||
|
return null
|
||||||
|
var scene: PackedScene = load(scene_path)
|
||||||
|
if not scene:
|
||||||
|
push_error("NetWorld.spawn: could not load scene %s" % scene_path)
|
||||||
|
return null
|
||||||
|
var inst := scene.instantiate()
|
||||||
|
if node_name != "":
|
||||||
|
inst.name = node_name
|
||||||
|
for key in props:
|
||||||
|
inst.set(key, props[key])
|
||||||
|
if inst is Node3D:
|
||||||
|
inst.transform = xform
|
||||||
|
_content_root.add_child(inst)
|
||||||
|
# global_transform can only be honoured once the node has a parent to be
|
||||||
|
# global relative to. Content roots are normally at the origin, but a debug
|
||||||
|
# scene is free to move one.
|
||||||
|
if inst is Node3D:
|
||||||
|
inst.global_transform = xform
|
||||||
|
NetworkManager.log_line("spawn: %s as %s" % [scene_path.get_file(), inst.name])
|
||||||
|
return inst
|
||||||
|
|
||||||
|
|
||||||
|
## Destroys a networked object everywhere. The spawner broadcasts the despawn
|
||||||
|
## when a tracked node leaves the tree on the authority, so freeing it here is
|
||||||
|
## the whole implementation — there is no despawn RPC any more.
|
||||||
|
##
|
||||||
|
## Callers reach this holding all sorts of nodes (a component such as
|
||||||
|
## DespawningItem, or a visual deep inside a plate), so it walks up to the object
|
||||||
|
## the spawner actually knows about. Getting that wrong used to leave a ghost
|
||||||
|
## copy on every client, which then blocked the station it was sitting in.
|
||||||
|
func despawn(node: Node) -> void:
|
||||||
|
if not NetworkManager.owns_world() or not is_instance_valid(node):
|
||||||
|
return
|
||||||
|
var root := _spawned_root(node)
|
||||||
|
if not root:
|
||||||
|
# Not part of the replicated world at all (a cosmetic copy parented under
|
||||||
|
# a plate, say). Freeing it locally is all that was ever meant.
|
||||||
|
node.queue_free()
|
||||||
|
return
|
||||||
|
NetworkManager.log_line("despawn: %s" % root.name)
|
||||||
|
root.queue_free()
|
||||||
|
|
||||||
|
|
||||||
|
# The ancestor that is a direct child of the content root — i.e. the node the
|
||||||
|
# spawner tracks — or null if this node is not part of the replicated world.
|
||||||
|
func _spawned_root(node: Node) -> Node:
|
||||||
|
var current := node
|
||||||
|
while current:
|
||||||
|
if current.get_parent() == _content_root:
|
||||||
|
return current
|
||||||
|
current = current.get_parent()
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
## Every replicated object currently in the world.
|
||||||
|
func objects() -> Array[Node]:
|
||||||
|
var found: Array[Node] = []
|
||||||
|
if _content_root:
|
||||||
|
for child in _content_root.get_children():
|
||||||
|
found.append(child)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
# --- what a client is not allowed to simulate ------------------------------
|
||||||
|
|
||||||
|
## The one gating rule, applied to every replicated object on peers that do not
|
||||||
|
## own world logic.
|
||||||
|
##
|
||||||
|
## Stations decide things (what has cooked, what is clean, what snaps where) and
|
||||||
|
## those decisions are the server's, so a client neither runs their logic nor
|
||||||
|
## lets their snap zones grab anything. Without this each peer independently
|
||||||
|
## grabs and simulates the same shared object, and the copies drift apart.
|
||||||
|
##
|
||||||
|
## Note what is NOT here: display code. A client still has to show a lit hob and
|
||||||
|
## a dirty plate, so anything visual must be driven from a replicated value's
|
||||||
|
## setter rather than from _process — which is where it belongs anyway, since
|
||||||
|
## that is the only version that also works for a late joiner.
|
||||||
|
func _gate(node: Node) -> void:
|
||||||
|
if node.is_in_group("station"):
|
||||||
|
node.set_process(false)
|
||||||
|
var zones := _snap_zones_of(node)
|
||||||
|
for zone in zones:
|
||||||
|
zone.enabled = false
|
||||||
|
zone.set_process(false)
|
||||||
|
NetworkManager.log_line("gated %s (%d snap zones)" % [node.name, zones.size()])
|
||||||
|
|
||||||
|
|
||||||
|
func _snap_zones_of(node: Node) -> Array:
|
||||||
|
var zones := []
|
||||||
|
for child in node.get_children():
|
||||||
|
if child is XRToolsSnapZone:
|
||||||
|
zones.append(child)
|
||||||
|
return zones
|
||||||
|
|
||||||
|
|
||||||
|
## Puts a body into the right physics state for whether this peer is currently
|
||||||
|
## driving it, which is exactly "do we own its NetXform".
|
||||||
|
##
|
||||||
|
## The authority IS the state here. The old implementation carried a replicated
|
||||||
|
## `net_held_by` peer id alongside the authority and reconciled the two by hand,
|
||||||
|
## which is what made held items get stuck frozen, or disabled forever, when the
|
||||||
|
## two disagreed. There is only one source of truth now.
|
||||||
|
##
|
||||||
|
## Called when an object is created and again whenever its NetXform changes
|
||||||
|
## hands, which is the complete set of moments the answer can change.
|
||||||
|
func apply_physics_role(node: Node) -> void:
|
||||||
|
if not (node is RigidBody3D):
|
||||||
|
return
|
||||||
|
var xform := node.get_node_or_null(NetReplication.XFORM_NAME)
|
||||||
|
if not xform:
|
||||||
|
return
|
||||||
|
var body: RigidBody3D = node
|
||||||
|
if xform.is_multiplayer_authority():
|
||||||
|
# We simulate it: restore whatever the scene authored. XRToolsPickable
|
||||||
|
# manages freeze and collision itself while an object is actually in a
|
||||||
|
# hand, so a held object is left alone.
|
||||||
|
if body is XRToolsPickable and body.is_picked_up():
|
||||||
|
return
|
||||||
|
body.freeze = false
|
||||||
|
body.freeze_mode = body.get_meta("net_freeze_mode", body.freeze_mode)
|
||||||
|
if body is XRToolsPickable:
|
||||||
|
body.collision_mask = body.original_collision_mask
|
||||||
|
# `enabled` has to be restored explicitly. Nothing else ever writes it
|
||||||
|
# back: the non-authority branch below clears it, so once any peer had
|
||||||
|
# held this object every other peer left it disabled forever. On the
|
||||||
|
# server that quietly broke everything downstream — hands could no
|
||||||
|
# longer pick the object up, while a snap zone still reported having
|
||||||
|
# grabbed it.
|
||||||
|
body.enabled = body.get_meta("net_enabled", body.enabled)
|
||||||
|
return
|
||||||
|
# Someone else drives it: stop simulating and just follow the sync. Kinematic
|
||||||
|
# rather than static so the incoming transform can still move it.
|
||||||
|
if body is XRToolsPickable and body.is_picked_up():
|
||||||
|
body.drop()
|
||||||
|
body.freeze = true
|
||||||
|
body.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||||
|
body.collision_mask = 0
|
||||||
|
if body is XRToolsPickable:
|
||||||
|
# Still grabbable if the SERVER owns it, because that just means the object
|
||||||
|
# is lying around loose — being able to pick those up is the entire point.
|
||||||
|
# Only an object held by another player is off limits, and that is exactly
|
||||||
|
# when NetXform belongs to a peer other than 1.
|
||||||
|
body.enabled = xform.get_multiplayer_authority() == 1
|
||||||
|
|
||||||
|
|
||||||
|
# Records the state the scene authored, before gating has a chance to overwrite
|
||||||
|
# it, so reclaiming an object restores what it was built with rather than
|
||||||
|
# whatever the frozen-follower state last forced on it.
|
||||||
|
func _remember_authored(node: Node) -> void:
|
||||||
|
if not (node is RigidBody3D) or node.has_meta("net_freeze_mode"):
|
||||||
|
return
|
||||||
|
node.set_meta("net_freeze_mode", node.freeze_mode)
|
||||||
|
if node is XRToolsPickable:
|
||||||
|
node.set_meta("net_enabled", node.enabled)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bgo0xq4uupuhi
|
||||||
+78
-362
@@ -1,36 +1,34 @@
|
|||||||
extends Node
|
extends Node
|
||||||
|
|
||||||
## Client-server session manager for VRyHungry (listen-server model).
|
## Session manager for VRyHungry (listen-server model: the host is peer 1 and
|
||||||
|
## also plays).
|
||||||
##
|
##
|
||||||
## Registered as the "NetworkManager" autoload. Owns transport (ENet), tracks
|
## Registered as the "NetworkManager" autoload. This owns the transport and the
|
||||||
## the session, and is the single place that reassigns multiplayer authority
|
## session lifecycle, and nothing else — what exists in the world and who is
|
||||||
## (only the server does so). The world scene (main.gd) registers its spawners
|
## allowed to simulate it belongs to NetWorld, and interaction belongs to
|
||||||
## here via [method register_world]; higher layers (players, items, stations)
|
## NetGrab. The spawn_item/despawn_item pair below are deliberately thin
|
||||||
## build on top of this in later phases.
|
## forwards, so game code keeps one obvious place to call.
|
||||||
|
|
||||||
const DEFAULT_PORT := 24565
|
const DEFAULT_PORT := 24565
|
||||||
const MAX_CLIENTS := 7
|
const MAX_CLIENTS := 7
|
||||||
|
|
||||||
## Emitted on every peer (including the server for its own local player) when a
|
## 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
|
## player peer joins. On the server this fires for each remote peer; the server
|
||||||
## uses it to spawn that peer's player.
|
## uses it to spawn that peer's avatar.
|
||||||
signal player_joined(peer_id: int)
|
signal player_joined(peer_id: int)
|
||||||
signal player_left(peer_id: int)
|
signal player_left(peer_id: int)
|
||||||
signal session_started(is_server: bool)
|
signal session_started(is_server: bool)
|
||||||
signal session_ended()
|
signal session_ended()
|
||||||
signal connection_failed()
|
signal connection_failed()
|
||||||
|
|
||||||
# World hooks, registered by main.gd once the scene tree exists.
|
|
||||||
var _world: Node = null
|
var _world: Node = null
|
||||||
var _players_spawner: MultiplayerSpawner = null
|
var _net_world: NetWorld = null
|
||||||
var _items_spawner: MultiplayerSpawner = null
|
|
||||||
var _content_root: Node = null
|
|
||||||
|
|
||||||
var _log_file: FileAccess
|
var _log_file: FileAccess
|
||||||
|
|
||||||
## Reason the session ended, shown by the menu on the next _ready() (see
|
## Reason the session ended, shown by the menu on its next _ready() (see
|
||||||
## take_status). Avoids depending on a live signal connection to a panel that
|
## take_status). Avoids depending on a live signal connection to a panel that
|
||||||
## doesn't exist yet at the moment the session actually ends.
|
## does not exist yet at the moment the session actually ends.
|
||||||
var last_status := ""
|
var last_status := ""
|
||||||
|
|
||||||
|
|
||||||
@@ -43,9 +41,9 @@ func _ready() -> void:
|
|||||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||||
|
|
||||||
|
|
||||||
# --- Public API ------------------------------------------------------------
|
# --- session ---------------------------------------------------------------
|
||||||
|
|
||||||
## Start hosting. The host is peer 1 and also plays (listen server).
|
## Start hosting. The host is peer 1 and also plays.
|
||||||
func host(port: int = DEFAULT_PORT) -> Error:
|
func host(port: int = DEFAULT_PORT) -> Error:
|
||||||
var peer := ENetMultiplayerPeer.new()
|
var peer := ENetMultiplayerPeer.new()
|
||||||
var err := peer.create_server(port, MAX_CLIENTS)
|
var err := peer.create_server(port, MAX_CLIENTS)
|
||||||
@@ -55,8 +53,7 @@ func host(port: int = DEFAULT_PORT) -> Error:
|
|||||||
multiplayer.multiplayer_peer = peer
|
multiplayer.multiplayer_peer = peer
|
||||||
log_line("HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()])
|
log_line("HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()])
|
||||||
session_started.emit(true)
|
session_started.emit(true)
|
||||||
# The host's own local player joins immediately.
|
player_joined.emit(multiplayer.get_unique_id())
|
||||||
_on_player_present(multiplayer.get_unique_id())
|
|
||||||
return OK
|
return OK
|
||||||
|
|
||||||
|
|
||||||
@@ -79,9 +76,9 @@ func leave() -> void:
|
|||||||
session_ended.emit()
|
session_ended.emit()
|
||||||
|
|
||||||
|
|
||||||
# Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer
|
# Restore Godot's default OfflineMultiplayerPeer rather than leaving the peer
|
||||||
# null), so is_multiplayer_authority()/get_unique_id() keep working while we are
|
# null: is_multiplayer_authority() and get_unique_id() both throw on a null peer,
|
||||||
# back in single-player / menu state.
|
# and plenty of code keeps calling them while we are back in menu state.
|
||||||
func _go_offline() -> void:
|
func _go_offline() -> void:
|
||||||
if multiplayer.multiplayer_peer:
|
if multiplayer.multiplayer_peer:
|
||||||
multiplayer.multiplayer_peer.close()
|
multiplayer.multiplayer_peer.close()
|
||||||
@@ -89,286 +86,6 @@ func _go_offline() -> void:
|
|||||||
unregister_world()
|
unregister_world()
|
||||||
|
|
||||||
|
|
||||||
# --- Item spawning ---------------------------------------------------------
|
|
||||||
|
|
||||||
## Spawn a networked item. Server-only when online (replicates to all peers via
|
|
||||||
## the ItemsSpawner, including late joiners); works directly when offline.
|
|
||||||
## node_name gives the spawned node a deterministic, identical name on every
|
|
||||||
## peer (needed for NodePath-based RPCs to resolve it); props are applied to
|
|
||||||
## the instance before it enters the tree, so exported vars land correctly.
|
|
||||||
## Returns the new node on the machine that owns spawning, else null.
|
|
||||||
func spawn_item(scene_path: String, xform: Transform3D, node_name: String = "", props: Dictionary = {}) -> Node:
|
|
||||||
if is_online() and not is_server():
|
|
||||||
return null
|
|
||||||
var data := {"scene": scene_path, "xform": xform, "name": node_name, "props": props}
|
|
||||||
var via := "spawner" if (is_online() and _items_spawner) else "offline"
|
|
||||||
log_line("spawn_item: %s (name=%s, via=%s)" % [scene_path.get_file(), node_name, via])
|
|
||||||
if is_online() and _items_spawner:
|
|
||||||
return _items_spawner.spawn(data)
|
|
||||||
# Offline: instantiate directly under the registered content root, or (for
|
|
||||||
# scenes that never call register_world, e.g. the offline menu/dev scenes)
|
|
||||||
# the current scene, so this keeps working without every offline scene
|
|
||||||
# needing to opt in.
|
|
||||||
var inst := _spawn_item_from_data(data)
|
|
||||||
if inst:
|
|
||||||
var parent: Node = _content_root if _content_root else get_tree().current_scene
|
|
||||||
if parent:
|
|
||||||
parent.add_child(inst)
|
|
||||||
return inst
|
|
||||||
|
|
||||||
|
|
||||||
## Despawn a server-spawned item. MultiplayerSpawner broadcasts a despawn to
|
|
||||||
## every peer when a tracked node exits the tree on the authority, so this is
|
|
||||||
## the single seam for destroying spawned items (works offline too).
|
|
||||||
func despawn_item(node: Node) -> void:
|
|
||||||
if not owns_world() or not is_instance_valid(node):
|
|
||||||
return
|
|
||||||
log_line("despawn_item: %s" % node.name)
|
|
||||||
# Items that came from the ItemsSpawner are despawned on every peer
|
|
||||||
# automatically when they leave the tree here. Items baked into a scene file
|
|
||||||
# are unknown to the spawner, so their removal has to be broadcast
|
|
||||||
# explicitly — otherwise every client keeps a ghost copy of an item the
|
|
||||||
# server has consumed, which then blocks the station it was sitting in and
|
|
||||||
# gets grabbed instead of the real item that replaced it.
|
|
||||||
if is_online() and not _is_spawner_tracked(node):
|
|
||||||
_despawn_static_item.rpc(node.get_path())
|
|
||||||
node.queue_free()
|
|
||||||
|
|
||||||
|
|
||||||
# Items the ItemsSpawner replicates live under its spawn path; anything else was
|
|
||||||
# baked into the scene file and the spawner knows nothing about it.
|
|
||||||
func _is_spawner_tracked(node: Node) -> bool:
|
|
||||||
return _content_root != null and _content_root.is_ancestor_of(node)
|
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable")
|
|
||||||
func _despawn_static_item(path: NodePath) -> void:
|
|
||||||
var node := get_node_or_null(path)
|
|
||||||
if node:
|
|
||||||
log_line("despawn_static_item: freeing %s (the server consumed it)" % node.name)
|
|
||||||
node.queue_free()
|
|
||||||
|
|
||||||
|
|
||||||
# MultiplayerSpawner custom spawn function: runs on every peer to build the node
|
|
||||||
# from the replicated payload.
|
|
||||||
func _spawn_item_from_data(data: Variant) -> Node:
|
|
||||||
var scene: PackedScene = load(data["scene"])
|
|
||||||
if not scene:
|
|
||||||
push_error("spawn_item: could not load scene %s" % str(data.get("scene")))
|
|
||||||
return null
|
|
||||||
var inst := scene.instantiate()
|
|
||||||
if inst is Node3D:
|
|
||||||
inst.transform = data["xform"]
|
|
||||||
if data.get("name", "") != "":
|
|
||||||
inst.name = data["name"]
|
|
||||||
for key in data.get("props", {}):
|
|
||||||
inst.set(key, data["props"][key])
|
|
||||||
if not owns_world():
|
|
||||||
_gate_station(inst)
|
|
||||||
return inst
|
|
||||||
|
|
||||||
|
|
||||||
# Stations run their own logic and auto-grab (XRToolsSnapZone with
|
|
||||||
# snap_mode=RANGE) identically on every peer by default, which would let each
|
|
||||||
# peer independently grab/simulate the same shared object. Disable both on
|
|
||||||
# every peer except the one that owns world logic; the server-authoritative
|
|
||||||
# item-authority RPCs are what let clients still grab a server-held item by
|
|
||||||
# hand. Runs before the node enters the tree, so its own _ready() sees the
|
|
||||||
# final (disabled) state.
|
|
||||||
func _gate_station(node: Node) -> void:
|
|
||||||
if not (node is StaticBody3D):
|
|
||||||
return
|
|
||||||
for child in node.get_children():
|
|
||||||
if child is XRToolsSnapZone:
|
|
||||||
child.enabled = false
|
|
||||||
child.set_process(false)
|
|
||||||
node.set_process(false)
|
|
||||||
log_line("gated station (non-owner peer): %s" % node.name)
|
|
||||||
|
|
||||||
|
|
||||||
## Gate every station already sitting in the scene tree, for peers that don't
|
|
||||||
## own world logic. Stations that arrive through spawn_item() are gated as they
|
|
||||||
## are built (see _spawn_item_from_data), but ones baked into a scene file never
|
|
||||||
## pass through there — leaving a client running its own snap zones, which then
|
|
||||||
## grab items straight out of the local hand and fight the server's
|
|
||||||
## authoritative placement. Idempotent, so it's safe on every session start.
|
|
||||||
func gate_existing_stations() -> void:
|
|
||||||
if owns_world():
|
|
||||||
return
|
|
||||||
for station in get_tree().get_nodes_in_group("station"):
|
|
||||||
_gate_station(station)
|
|
||||||
|
|
||||||
|
|
||||||
# --- 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.
|
|
||||||
log_line("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
|
|
||||||
log_line("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:
|
|
||||||
log_line("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:
|
|
||||||
log_line("releasing %s from %s's snap zone (authority just granted elsewhere)" % [item.name, zone.get_parent().name])
|
|
||||||
zone.drop_object()
|
|
||||||
|
|
||||||
|
|
||||||
# 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()
|
|
||||||
log_line("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:
|
|
||||||
log_line("snapped %s into %s" % [item.name, zone.get_parent().name])
|
|
||||||
zone.pick_up_object(item)
|
|
||||||
return
|
|
||||||
log_line("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
|
|
||||||
log_line("_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:
|
|
||||||
log_line("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:
|
func is_server() -> bool:
|
||||||
return is_online() and multiplayer.is_server()
|
return is_online() and multiplayer.is_server()
|
||||||
|
|
||||||
@@ -381,71 +98,69 @@ func is_online() -> bool:
|
|||||||
|
|
||||||
|
|
||||||
## True on the machine that owns authoritative world logic: the server when
|
## True on the machine that owns authoritative world logic: the server when
|
||||||
## online, or the single player when offline. Station logic and spawning should
|
## online, or the single player when offline. Station logic and spawning only run
|
||||||
## only run where this is true, so state has one source of truth.
|
## where this is true, so state has one source of truth.
|
||||||
func owns_world() -> bool:
|
func owns_world() -> bool:
|
||||||
return not is_online() or is_server()
|
return not is_online() or is_server()
|
||||||
|
|
||||||
|
|
||||||
# --- Station work-progress seam -------------------------------------------
|
# --- world -----------------------------------------------------------------
|
||||||
|
|
||||||
## Reusable entry point for a client to contribute work to a station (e.g. a
|
## Called by the world scene once its tree exists. Must run before
|
||||||
## future chopping/gesture station). The client detects the gesture locally and
|
## world_ready()/host()/join() on every peer, so the spawnable scene list is
|
||||||
## calls this; the server validates and accumulates. Timer-driven stations like
|
## registered before any spawn packet can arrive.
|
||||||
## the Hob don't need it, but it is the drop-in seam for input-driven ones.
|
func register_world(world: Node, net_world: NetWorld) -> void:
|
||||||
@rpc("any_peer", "reliable")
|
|
||||||
func submit_work(station_path: NodePath, amount: float) -> void:
|
|
||||||
if not is_server():
|
|
||||||
return
|
|
||||||
var station := get_node_or_null(station_path)
|
|
||||||
if station and station.has_method("add_work"):
|
|
||||||
log_line("submit_work: peer %d contributed %.2f to %s" % [multiplayer.get_remote_sender_id(), amount, station.name])
|
|
||||||
station.add_work(multiplayer.get_remote_sender_id(), amount)
|
|
||||||
|
|
||||||
|
|
||||||
## Called by the world scene once it's ready, passing its spawners. Must run
|
|
||||||
## before world_ready()/host()/join() on every peer so the custom spawn
|
|
||||||
## function is installed before any spawn packet can arrive.
|
|
||||||
func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void:
|
|
||||||
_world = world
|
_world = world
|
||||||
_players_spawner = players_spawner
|
_net_world = net_world
|
||||||
_items_spawner = items_spawner
|
log_line("World registered")
|
||||||
_content_root = items_spawner.get_node(items_spawner.spawn_path) if items_spawner else world
|
|
||||||
if _items_spawner:
|
|
||||||
_items_spawner.spawn_function = _spawn_item_from_data
|
|
||||||
log_line("World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)])
|
|
||||||
|
|
||||||
|
|
||||||
## Called when the world scene goes away (disconnect, leaving the session) so
|
## Called when the world scene goes away (disconnect, leaving the session) so the
|
||||||
## the autoload doesn't hold stale/freed references across a scene reload.
|
## autoload does not hold freed references across a scene reload.
|
||||||
func unregister_world() -> void:
|
func unregister_world() -> void:
|
||||||
_world = null
|
_world = null
|
||||||
_content_root = null
|
_net_world = null
|
||||||
_players_spawner = null
|
|
||||||
_items_spawner = null
|
|
||||||
|
|
||||||
|
|
||||||
## Returns the reason the last session ended (if any) and clears it. The menu
|
## Creates a networked object. See NetWorld.spawn.
|
||||||
## pulls this on its own _ready() rather than depending on a live signal
|
func spawn_item(scene_path: String, xform: Transform3D, node_name: String = "", props: Dictionary = {}) -> Node:
|
||||||
## connection to a panel that doesn't exist yet when the session ends.
|
if not _net_world:
|
||||||
func take_status() -> String:
|
push_error("spawn_item called with no world registered: %s" % scene_path)
|
||||||
var s := last_status
|
return null
|
||||||
last_status = ""
|
return _net_world.spawn(scene_path, xform, node_name, props)
|
||||||
return s
|
|
||||||
|
|
||||||
|
|
||||||
## Called by main.gd after it has registered the world and connected its
|
## Destroys a networked object everywhere. See NetWorld.despawn.
|
||||||
## player_joined/left listeners. Kicks off any menu- or command-line-driven
|
func despawn_item(node: Node) -> void:
|
||||||
## session so that session signals never fire before the world is listening.
|
if _net_world:
|
||||||
|
_net_world.despawn(node)
|
||||||
|
|
||||||
|
|
||||||
|
## Puts an object into the right physics state for whether this peer drives it.
|
||||||
|
func apply_physics_role(node: Node) -> void:
|
||||||
|
if _net_world:
|
||||||
|
_net_world.apply_physics_role(node)
|
||||||
|
|
||||||
|
|
||||||
|
## Every replicated object currently in the world.
|
||||||
|
func replicated_objects() -> Array[Node]:
|
||||||
|
if not _net_world:
|
||||||
|
return []
|
||||||
|
return _net_world.objects()
|
||||||
|
|
||||||
|
|
||||||
|
## Called by the world scene after it has registered itself and connected its
|
||||||
|
## listeners. Kicks off any menu- or command-line-driven session, so session
|
||||||
|
## signals never fire before the world is listening for them.
|
||||||
func world_ready() -> void:
|
func world_ready() -> void:
|
||||||
consume_pending_session()
|
consume_pending_session()
|
||||||
|
|
||||||
|
|
||||||
# --- Menu-driven session request -------------------------------------------
|
# --- menu-driven session request -------------------------------------------
|
||||||
|
|
||||||
# Set by the main menu's Host/Join buttons before switching to the multiplayer
|
# Set by the main menu's Host/Join buttons before switching to the multiplayer
|
||||||
# scene; consumed once that scene's world is ready to listen for session
|
# scene; consumed once that scene is ready to listen for session signals (avoids
|
||||||
# signals (avoids a race between change_scene_to_file and connection callbacks).
|
# a race between change_scene_to_file and the connection callbacks).
|
||||||
var pending_action := ""
|
var pending_action := ""
|
||||||
var pending_ip := ""
|
var pending_ip := ""
|
||||||
|
|
||||||
@@ -467,18 +182,29 @@ func consume_pending_session() -> void:
|
|||||||
_handle_cmdline()
|
_handle_cmdline()
|
||||||
|
|
||||||
|
|
||||||
# --- Session signal handlers ----------------------------------------------
|
## Returns the reason the last session ended (if any) and clears it. The menu
|
||||||
|
## pulls this on its own _ready() rather than depending on a live signal
|
||||||
|
## connection to a panel that does not exist yet when the session ends.
|
||||||
|
func take_status() -> String:
|
||||||
|
var s := last_status
|
||||||
|
last_status = ""
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
# --- session signal handlers ----------------------------------------------
|
||||||
|
|
||||||
func _on_peer_connected(peer_id: int) -> void:
|
func _on_peer_connected(peer_id: int) -> void:
|
||||||
log_line("peer_connected: %d" % peer_id)
|
log_line("peer_connected: %d" % peer_id)
|
||||||
# Only the server reacts by materialising that peer's player.
|
|
||||||
if is_server():
|
if is_server():
|
||||||
_on_player_present(peer_id)
|
player_joined.emit(peer_id)
|
||||||
|
|
||||||
func _on_peer_disconnected(peer_id: int) -> void:
|
func _on_peer_disconnected(peer_id: int) -> void:
|
||||||
log_line("peer_disconnected: %d" % peer_id)
|
log_line("peer_disconnected: %d" % peer_id)
|
||||||
if is_server():
|
if is_server():
|
||||||
_on_player_absent(peer_id)
|
# Anything still in their hand would otherwise stay frozen on every
|
||||||
|
# remaining peer, waiting on an authority that has gone.
|
||||||
|
NetGrab.reclaim_from(peer_id)
|
||||||
|
player_left.emit(peer_id)
|
||||||
|
|
||||||
func _on_connected_to_server() -> void:
|
func _on_connected_to_server() -> void:
|
||||||
log_line("connected_to_server (my id=%d)" % multiplayer.get_unique_id())
|
log_line("connected_to_server (my id=%d)" % multiplayer.get_unique_id())
|
||||||
@@ -497,18 +223,7 @@ func _on_server_disconnected() -> void:
|
|||||||
session_ended.emit()
|
session_ended.emit()
|
||||||
|
|
||||||
|
|
||||||
# Player materialise/dematerialise. Phase 2 wires these to the PlayersSpawner;
|
# --- command-line driven test bootstrap -----------------------------------
|
||||||
# for now they announce presence so the transport layer is independently testable.
|
|
||||||
func _on_player_present(peer_id: int) -> void:
|
|
||||||
log_line("player_present: %d" % peer_id)
|
|
||||||
player_joined.emit(peer_id)
|
|
||||||
|
|
||||||
func _on_player_absent(peer_id: int) -> void:
|
|
||||||
log_line("player_absent: %d" % peer_id)
|
|
||||||
player_left.emit(peer_id)
|
|
||||||
|
|
||||||
|
|
||||||
# --- Command-line driven test bootstrap -----------------------------------
|
|
||||||
|
|
||||||
func _handle_cmdline() -> void:
|
func _handle_cmdline() -> void:
|
||||||
var args := OS.get_cmdline_user_args()
|
var args := OS.get_cmdline_user_args()
|
||||||
@@ -524,7 +239,7 @@ func _handle_cmdline() -> void:
|
|||||||
join(addr)
|
join(addr)
|
||||||
|
|
||||||
|
|
||||||
# --- Logging ---------------------------------------------------------------
|
# --- logging ---------------------------------------------------------------
|
||||||
|
|
||||||
func _open_log() -> void:
|
func _open_log() -> void:
|
||||||
var dir := OS.get_environment("TEMP")
|
var dir := OS.get_environment("TEMP")
|
||||||
@@ -536,6 +251,7 @@ func _open_log() -> void:
|
|||||||
_log_file = FileAccess.open(path, FileAccess.WRITE)
|
_log_file = FileAccess.open(path, FileAccess.WRITE)
|
||||||
log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id())
|
log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id())
|
||||||
|
|
||||||
|
|
||||||
func log_line(s: String) -> void:
|
func log_line(s: String) -> void:
|
||||||
var id := 0
|
var id := 0
|
||||||
var p := multiplayer.multiplayer_peer
|
var p := multiplayer.multiplayer_peer
|
||||||
|
|||||||
+71
-73
@@ -1,81 +1,79 @@
|
|||||||
|
extends Node
|
||||||
class_name WorldLayout
|
class_name WorldLayout
|
||||||
|
|
||||||
## Data-driven description of what's in the multiplayer world. The server (or
|
## Reads the kitchen a scene file authored and describes it as data, so the
|
||||||
## the single machine, when offline) spawns every entry through
|
## server can respawn it as replicated objects.
|
||||||
## NetworkManager.spawn_item() instead of baking these into the scene file, so
|
|
||||||
## a joining client receives them from the server rather than assuming its own
|
|
||||||
## copy of the scene matches. Swapping the contents of these two functions is
|
|
||||||
## the only change needed for a future varying/procedural layout.
|
|
||||||
##
|
##
|
||||||
## Positions below are transcribed verbatim from the previous baked layout in
|
## The authored nodes are a template, not the live world. Baking them into the
|
||||||
## Scenes/multiPlayer.tscn so the starting world is unchanged.
|
## scene would mean each peer ran its own unsynced copy; spawning them means the
|
||||||
|
## client receives the server's, through the same path as a mid-session join.
|
||||||
|
|
||||||
static func get_stations() -> Array[Dictionary]:
|
## Directories whose scenes count as world content, and the base type to look for
|
||||||
return [
|
## in each. Anything instanced from these is picked up automatically — adding a
|
||||||
{"scene": "res://Stations/Hob.tscn", "name": "Hob",
|
## new station or item needs no change here.
|
||||||
"xform": Transform3D(Basis(), Vector3(0.29336345, 0.8981018, -1.484)), "props": {}},
|
const SOURCES := [
|
||||||
{"scene": "res://Stations/BurgerBunsDispenser.tscn", "name": "BurgerBunsDispenser",
|
{"dir": "res://Stations/", "type": "StaticBody3D"},
|
||||||
"xform": Transform3D(Basis(), Vector3(-1.832131, 0.40028095, -1.4813508)), "props": {}},
|
{"dir": "res://Items/", "type": "XRToolsPickable"},
|
||||||
{"scene": "res://Stations/sink.tscn", "name": "Sink",
|
{"dir": "res://Containers/", "type": "XRToolsPickable"},
|
||||||
"xform": Transform3D(Basis(), Vector3(1.3140475, 0.9061539, -1.4941733)), "props": {}},
|
]
|
||||||
{"scene": "res://Stations/dirt_station.tscn", "name": "DirtStation",
|
|
||||||
"xform": Transform3D(Basis(), Vector3(2.0864775, 0.8981018, -1.244947)), "props": {}},
|
|
||||||
{"scene": "res://Stations/Counter.tscn", "name": "Counter",
|
|
||||||
"xform": Transform3D(Basis(), Vector3(-0.7124918, 0.90304357, -1.4886917)), "props": {}},
|
|
||||||
{"scene": "res://Stations/Counter.tscn", "name": "Counter2",
|
|
||||||
"xform": Transform3D(Vector3(-4.371139e-08, 0.0, 1.0), Vector3(0.0, 1.0, 0.0), Vector3(-1.0, 0.0, -4.371139e-08), Vector3(-1.7648025, 0.90304357, -0.4458799)), "props": {}},
|
|
||||||
{"scene": "res://Stations/Counter.tscn", "name": "Counter3",
|
|
||||||
"xform": Transform3D(Vector3(-4.371139e-08, 0.0, 1.0), Vector3(0.0, 1.0, 0.0), Vector3(-1.0, 0.0, -4.371139e-08), Vector3(-1.7648025, 0.90304357, 0.55367994)), "props": {}},
|
|
||||||
{"scene": "res://Stations/Counter.tscn", "name": "Counter4",
|
|
||||||
"xform": Transform3D(Vector3(-4.371139e-08, 0.0, 1.0), Vector3(0.0, 1.0, 0.0), Vector3(-1.0, 0.0, -4.371139e-08), Vector3(-1.7648025, 0.90304357, 1.5539298)), "props": {}},
|
|
||||||
{"scene": "res://Stations/table.tscn", "name": "Table",
|
|
||||||
"xform": Transform3D(Basis(), Vector3(1.633146, 0.9030438, 1.1343781)),
|
|
||||||
"props": {
|
|
||||||
"initial_thinking_time": 8.0,
|
|
||||||
"initial_primary_time": 40.0,
|
|
||||||
"initial_friend_time": 3.0,
|
|
||||||
"initial_eating_time": 3.0,
|
|
||||||
}},
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
static func get_items() -> Array[Dictionary]:
|
## Every authored node in the current scene, stations first.
|
||||||
var items: Array[Dictionary] = []
|
##
|
||||||
|
## Order matters: stations have to exist before items, so an item spawning on top
|
||||||
items.append(_item("res://Items/BurgerBuns.tscn", "BurgerBuns", Vector3(-1.8162017, 1.6081157, -1.4714175)))
|
## of one lands in a snap zone that is already there.
|
||||||
items.append(_item("res://Items/BurgerBuns.tscn", "BurgerBuns2", Vector3(-1.3596323, 1.4110342, -0.22482127)))
|
func get_authored_nodes() -> Array[Node]:
|
||||||
|
var found: Array[Node] = []
|
||||||
items.append(_item("res://Containers/plate.tscn", "Plate", Vector3(-1.5717233, 1.5454081, 1.5373346)))
|
for source in SOURCES:
|
||||||
items.append(_item("res://Containers/plate.tscn", "Plate2", Vector3(-1.5730225, 1.499024, 0.59790254)))
|
for node in _instances_of(source["dir"], source["type"]):
|
||||||
items.append(_item("res://Containers/plate.tscn", "Plate3", Vector3(-1.5818124, 1.499024, -0.4634577)))
|
if not found.has(node):
|
||||||
|
found.append(node)
|
||||||
items.append(_item("res://Items/burger.tscn", "burger", Vector3(0.6888188, 1.4195822, -1.7110313)))
|
return found
|
||||||
items.append(_item("res://Items/burger.tscn", "burger2", Vector3(0.6931299, 1.5300478, -1.71225)))
|
|
||||||
items.append(_item("res://Items/burger.tscn", "burger3", Vector3(0.69027674, 1.4969791, -1.7210286)))
|
|
||||||
items.append(_item("res://Items/burger.tscn", "burger4", Vector3(0.69134104, 1.4543622, -1.7210286)))
|
|
||||||
|
|
||||||
items.append(_item("res://Items/hamburger.tscn", "Hamburger", Vector3(-1.9352558, 1.623975, 1.2447833)))
|
|
||||||
items.append(_item("res://Items/hamburger.tscn", "Hamburger2", Vector3(-1.9857153, 1.5329368, 0.9472374)))
|
|
||||||
items.append(_item("res://Items/hamburger.tscn", "Hamburger3", Vector3(-1.7201865, 1.5329367, 0.14773655)))
|
|
||||||
|
|
||||||
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger", Vector3(-0.30671906, 1.4131018, -1.0928738)))
|
|
||||||
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger2", Vector3(-0.30671906, 1.4496142, -1.0928738)))
|
|
||||||
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger3", Vector3(-1.3344773, 1.4398065, -0.7096845)))
|
|
||||||
items.append(_item("res://Items/cooked_burger.tscn", "CookedBurger4", Vector3(-0.30671906, 1.525444, -1.0928738)))
|
|
||||||
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject", Vector3(0.6225724, 1.4792972, -1.0473135)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject2", Vector3(0.6359743, 1.4639391, -1.1673055)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject3", Vector3(0.5142721, 1.4792972, -1.0473135)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject4", Vector3(0.5276739, 1.4639391, -1.1673055)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject5", Vector3(0.73287535, 1.4792972, -1.0473135)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject6", Vector3(0.7462772, 1.4639391, -1.1673055)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject7", Vector3(-1.3556751, 1.4792972, 0.28881657)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject8", Vector3(-1.3422732, 1.4639391, 0.16882455)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject9", Vector3(-1.4639754, 1.4792972, 0.28881657)))
|
|
||||||
items.append(_item("res://Items/PickupCube.tscn", "PickableObject10", Vector3(-1.4505737, 1.4639391, 0.16882455)))
|
|
||||||
|
|
||||||
return items
|
|
||||||
|
|
||||||
|
|
||||||
static func _item(scene: String, name: String, pos: Vector3) -> Dictionary:
|
func _instances_of(dir: String, type: String) -> Array[Node]:
|
||||||
return {"scene": scene, "name": name, "xform": Transform3D(Basis(), pos), "props": {}}
|
var scenes := []
|
||||||
|
for file in ResourceLoader.list_directory(dir):
|
||||||
|
scenes.append(dir + file)
|
||||||
|
var found: Array[Node] = []
|
||||||
|
for node in get_tree().root.find_children("*", type, true, false):
|
||||||
|
if node.scene_file_path in scenes:
|
||||||
|
found.append(node)
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
## Turns authored nodes into spawn descriptions.
|
||||||
|
func describe(nodes: Array[Node]) -> Array[Dictionary]:
|
||||||
|
var data: Array[Dictionary] = []
|
||||||
|
for node in nodes:
|
||||||
|
data.append(_describe_one(node))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
func _describe_one(node: Node) -> Dictionary:
|
||||||
|
return {
|
||||||
|
"scene": node.scene_file_path,
|
||||||
|
"name": node.name,
|
||||||
|
# Global, not local: the copies are respawned under one content root, so
|
||||||
|
# an authored node nested inside another (JonScene parents Counter5 under
|
||||||
|
# Counter3) would otherwise land in the wrong place.
|
||||||
|
"xform": node.global_transform,
|
||||||
|
"props": _authored_props(node),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Only authored configuration — @export vars, the ones the editor exposes. Plain
|
||||||
|
# script variables are live runtime state, and replaying those into a fresh
|
||||||
|
# instance re-runs their setters before the node is in the tree, where any setter
|
||||||
|
# touching an @onready reference blows up.
|
||||||
|
func _authored_props(node: Node) -> Dictionary:
|
||||||
|
var props := {}
|
||||||
|
var script: Script = node.get_script() as Script
|
||||||
|
if not script:
|
||||||
|
return props
|
||||||
|
for prop in script.get_script_property_list():
|
||||||
|
var prop_name := str(prop["name"])
|
||||||
|
if not (prop["usage"] & PROPERTY_USAGE_EDITOR) or prop_name.begins_with("_"):
|
||||||
|
continue
|
||||||
|
props[prop_name] = node.get(prop_name)
|
||||||
|
return props
|
||||||
|
|||||||
+78
-43
@@ -1,71 +1,106 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
## Networked representation of one connected player. The owning peer's local
|
## Networked representation of one connected player.
|
||||||
## XR rig (found via the "local_xr_origin" group) drives Head/LeftHand/
|
##
|
||||||
## RightHand each frame; the MultiplayerSynchronizer replicates those
|
## The owning peer's local XR rig drives the three transforms below every frame;
|
||||||
## transforms so every other peer sees a matching avatar.
|
## the generated NetSync replicates them, and every other peer applies them to
|
||||||
|
## the avatar's head and hands. They are ordinary script variables rather than an
|
||||||
|
## authored replication config, so the avatar goes through exactly the same
|
||||||
|
## convention as every other replicated object.
|
||||||
|
|
||||||
@onready var _head: Node3D = $Head
|
@onready var _head: Node3D = $Head
|
||||||
@onready var _left_hand: Node3D = $LeftHand
|
@onready var _left_hand: Node3D = $LeftHand
|
||||||
@onready var _right_hand: Node3D = $RightHand
|
@onready var _right_hand: Node3D = $RightHand
|
||||||
|
|
||||||
|
## Replicated pose. Underscore-free by design — that is what marks a variable as
|
||||||
|
## replicated (see NetReplication). Written by the owning peer, applied by
|
||||||
|
## everyone else through the setters.
|
||||||
|
var head_xform: Transform3D = Transform3D.IDENTITY: set = _set_head_xform
|
||||||
|
var left_xform: Transform3D = Transform3D.IDENTITY: set = _set_left_xform
|
||||||
|
var right_xform: Transform3D = Transform3D.IDENTITY: set = _set_right_xform
|
||||||
|
|
||||||
var _local_camera: Node3D
|
var _local_camera: Node3D
|
||||||
var _local_left_hand: Node3D
|
var _local_left_hand: Node3D
|
||||||
var _local_right_hand: Node3D
|
var _local_right_hand: Node3D
|
||||||
|
|
||||||
|
|
||||||
func _enter_tree() -> void:
|
func _enter_tree() -> void:
|
||||||
# MultiplayerSynchronizer resolves authority when this node enters the
|
# The avatar's name is the peer id it belongs to. Authority has to be claimed
|
||||||
# tree, so authority must be set here rather than in _ready(), or the
|
# here rather than in _ready(): NetReplication reads it when it attaches the
|
||||||
# first synced frames go the wrong direction.
|
# synchronizers, which happens as this node enters the tree.
|
||||||
set_multiplayer_authority(str(name).to_int())
|
set_multiplayer_authority(str(name).to_int())
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
# LeftHand/RightHand are the full godot-xr-tools glove scenes, whose root
|
# LeftHand/RightHand are the full godot-xr-tools glove scenes, whose root
|
||||||
# node carries hand.gd (XRToolsHand). That script sets top_level = true
|
# carries hand.gd. That script sets top_level = true and every physics frame
|
||||||
# and every physics frame does global_transform = get_parent().
|
# repositions itself to track a live XRController3D parent. Here the parent is
|
||||||
# global_transform * offset — i.e. it actively repositions itself to
|
# just this avatar, not a controller, so leaving it running fights both the
|
||||||
# track a live XRController3D parent. Here the parent is just this
|
# local copy below and the replicated values on other peers — and mostly wins,
|
||||||
# NetPlayer node, not a controller, so left running it fights (and mostly
|
# since it runs every physics tick regardless of our _process. That was the
|
||||||
# wins, since it runs every physics tick regardless of our own _process)
|
# "hands stuck near the origin, only occasionally correct" symptom. Nothing
|
||||||
# against both the local authority's transform copy below and the
|
# else in that script matters here; _controller is always null without a real
|
||||||
# replicated values on other peers — the exact "hands stuck near origin,
|
# controller ancestor, so the grip animation already no-ops.
|
||||||
# only occasionally correct" symptom. Disable it everywhere; nothing else
|
|
||||||
# in that script matters here since _controller is always null without a
|
|
||||||
# real controller ancestor (grip/trigger animation already no-ops).
|
|
||||||
_left_hand.set_physics_process(false)
|
_left_hand.set_physics_process(false)
|
||||||
_right_hand.set_physics_process(false)
|
_right_hand.set_physics_process(false)
|
||||||
|
|
||||||
if is_multiplayer_authority():
|
if not is_multiplayer_authority():
|
||||||
var origin := get_tree().get_first_node_in_group("local_xr_origin")
|
|
||||||
if origin:
|
|
||||||
_local_camera = origin.get_node_or_null("XRCamera3D")
|
|
||||||
_local_left_hand = origin.get_node_or_null("XRControllerLeftHand")
|
|
||||||
_local_right_hand = origin.get_node_or_null("XRControllerRightHand")
|
|
||||||
# Every peer's rig is baked at the same spot in the scene; spread
|
|
||||||
# joiners out along X so they don't start stacked on top of each
|
|
||||||
# other. Peer ids from ENet are large effectively-random 32-bit
|
|
||||||
# numbers, so this must be bounded, AND kept well within the
|
|
||||||
# floor's actual footprint (15x15, so ~7.5 units from center) —
|
|
||||||
# a previous version used up to 12 units and could place a
|
|
||||||
# joining player off the edge of the floor.
|
|
||||||
var peer_id := str(name).to_int()
|
|
||||||
if peer_id != 1:
|
|
||||||
var slot := absi(peer_id) % 4
|
|
||||||
origin.position += Vector3((slot - 1.5) * 1.2, 0, 0)
|
|
||||||
# Don't render your own floating head/hands from the inside.
|
|
||||||
_head.visible = false
|
|
||||||
_left_hand.visible = false
|
|
||||||
_right_hand.visible = false
|
|
||||||
else:
|
|
||||||
set_process(false)
|
set_process(false)
|
||||||
|
# State that arrived in the spawn packet was applied before these @onready
|
||||||
|
# references existed, so render it now.
|
||||||
|
_apply_pose()
|
||||||
|
return
|
||||||
|
|
||||||
|
var origin := get_tree().get_first_node_in_group("local_xr_origin")
|
||||||
|
if origin:
|
||||||
|
_local_camera = origin.get_node_or_null("XRCamera3D")
|
||||||
|
_local_left_hand = origin.get_node_or_null("XRControllerLeftHand")
|
||||||
|
_local_right_hand = origin.get_node_or_null("XRControllerRightHand")
|
||||||
|
# Every peer's rig is baked at the same spot, so spread joiners along X
|
||||||
|
# rather than starting them stacked on top of each other. ENet peer ids
|
||||||
|
# are large effectively-random 32-bit numbers, so this has to be bounded
|
||||||
|
# AND kept well inside the floor's footprint — an earlier version used up
|
||||||
|
# to 12 units and could drop a joining player off the edge.
|
||||||
|
var peer_id := str(name).to_int()
|
||||||
|
if peer_id != 1:
|
||||||
|
var slot := absi(peer_id) % 4
|
||||||
|
origin.position += Vector3((slot - 1.5) * 1.2, 0, 0)
|
||||||
|
# Don't render your own floating head and hands from the inside.
|
||||||
|
_head.visible = false
|
||||||
|
_left_hand.visible = false
|
||||||
|
_right_hand.visible = false
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
if _local_camera:
|
if _local_camera:
|
||||||
_head.global_transform = _local_camera.global_transform
|
head_xform = _local_camera.global_transform
|
||||||
if _local_left_hand:
|
if _local_left_hand:
|
||||||
_left_hand.global_transform = _local_left_hand.global_transform
|
left_xform = _local_left_hand.global_transform
|
||||||
if _local_right_hand:
|
if _local_right_hand:
|
||||||
_right_hand.global_transform = _local_right_hand.global_transform
|
right_xform = _local_right_hand.global_transform
|
||||||
|
|
||||||
|
|
||||||
|
# The setters are null-guarded because a spawn-replicated property fires its
|
||||||
|
# setter BEFORE the node is in the tree, when @onready references are still null.
|
||||||
|
func _set_head_xform(value: Transform3D) -> void:
|
||||||
|
head_xform = value
|
||||||
|
if _head:
|
||||||
|
_head.global_transform = value
|
||||||
|
|
||||||
|
|
||||||
|
func _set_left_xform(value: Transform3D) -> void:
|
||||||
|
left_xform = value
|
||||||
|
if _left_hand:
|
||||||
|
_left_hand.global_transform = value
|
||||||
|
|
||||||
|
|
||||||
|
func _set_right_xform(value: Transform3D) -> void:
|
||||||
|
right_xform = value
|
||||||
|
if _right_hand:
|
||||||
|
_right_hand.global_transform = value
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_pose() -> void:
|
||||||
|
_head.global_transform = head_xform
|
||||||
|
_left_hand.global_transform = left_xform
|
||||||
|
_right_hand.global_transform = right_xform
|
||||||
|
|||||||
+1
-26
@@ -1,4 +1,4 @@
|
|||||||
[gd_scene load_steps=5 format=3]
|
[gd_scene load_steps=4 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://Player/net_player.gd" id="1_np001"]
|
[ext_resource type="Script" path="res://Player/net_player.gd" id="1_np001"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bq86r4yll8po" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/left_fullglove_low.tscn" id="2_np002"]
|
[ext_resource type="PackedScene" uid="uid://bq86r4yll8po" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/left_fullglove_low.tscn" id="2_np002"]
|
||||||
@@ -8,26 +8,6 @@
|
|||||||
radius = 0.09
|
radius = 0.09
|
||||||
height = 0.22
|
height = 0.22
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np005"]
|
|
||||||
properties/0/path = NodePath("Head:position")
|
|
||||||
properties/0/spawn = false
|
|
||||||
properties/0/replication_mode = 1
|
|
||||||
properties/1/path = NodePath("Head:quaternion")
|
|
||||||
properties/1/spawn = false
|
|
||||||
properties/1/replication_mode = 1
|
|
||||||
properties/2/path = NodePath("LeftHand:position")
|
|
||||||
properties/2/spawn = false
|
|
||||||
properties/2/replication_mode = 1
|
|
||||||
properties/3/path = NodePath("LeftHand:quaternion")
|
|
||||||
properties/3/spawn = false
|
|
||||||
properties/3/replication_mode = 1
|
|
||||||
properties/4/path = NodePath("RightHand:position")
|
|
||||||
properties/4/spawn = false
|
|
||||||
properties/4/replication_mode = 1
|
|
||||||
properties/5/path = NodePath("RightHand:quaternion")
|
|
||||||
properties/5/spawn = false
|
|
||||||
properties/5/replication_mode = 1
|
|
||||||
|
|
||||||
[node name="NetPlayer" type="Node3D"]
|
[node name="NetPlayer" type="Node3D"]
|
||||||
script = ExtResource("1_np001")
|
script = ExtResource("1_np001")
|
||||||
|
|
||||||
@@ -37,8 +17,3 @@ mesh = SubResource("CapsuleMesh_np004")
|
|||||||
[node name="LeftHand" parent="." instance=ExtResource("2_np002")]
|
[node name="LeftHand" parent="." instance=ExtResource("2_np002")]
|
||||||
|
|
||||||
[node name="RightHand" parent="." instance=ExtResource("3_np003")]
|
[node name="RightHand" parent="." instance=ExtResource("3_np003")]
|
||||||
|
|
||||||
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
|
|
||||||
root_path = NodePath("..")
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np005")
|
|
||||||
replication_interval = 0.033
|
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
extends Node
|
||||||
|
class_name DespawningItem
|
||||||
|
|
||||||
|
@export var time_to_despawn: float = 8
|
||||||
|
|
||||||
|
# The minimum speed required to reset despawn timer.
|
||||||
|
@export var minimum_speed_square: float = 0.1
|
||||||
|
|
||||||
|
|
||||||
|
@onready var _pickable: XRToolsPickable = get_parent() as XRToolsPickable
|
||||||
|
@onready var _rigid: RigidBody3D = get_parent() as RigidBody3D
|
||||||
|
|
||||||
|
|
||||||
|
var _time_left: float = time_to_despawn
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
if not _pickable:
|
||||||
|
push_error("DespawningItem must be a grand child of an XRToolsPickable.")
|
||||||
|
return
|
||||||
|
if not _rigid:
|
||||||
|
push_error("DespawningItem must be a grand child of an RigidBody3D.")
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
func _process(delta: float) -> void:
|
||||||
|
# Despawning is a world decision, so only the owner of world logic runs the
|
||||||
|
# clock. Every peer used to run its own copy: the countdown drifted between
|
||||||
|
# them and the blink below toggled `visible` locally, so the same item was
|
||||||
|
# shown on one peer and hidden on the other. Non-owners just follow the
|
||||||
|
# server, which removes the item for everyone when its time is up.
|
||||||
|
if not NetworkManager.owns_world():
|
||||||
|
return
|
||||||
|
|
||||||
|
# if we're held or moving, reset timer and return
|
||||||
|
if _pickable.get_picked_up_by() or _rigid.linear_velocity.length_squared() > pow(minimum_speed_square,2):
|
||||||
|
_time_left = time_to_despawn
|
||||||
|
_set_visible(true)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
# Count down to zero and despawn
|
||||||
|
_time_left -= delta
|
||||||
|
if _time_left <= 0:
|
||||||
|
print("DespawningItem despawning!" , get_parent())
|
||||||
|
NetworkManager.despawn_item(get_parent())
|
||||||
|
# Stop counting: despawn_item() only queues the free, so without this we
|
||||||
|
# keep re-reporting the same item every frame until it actually goes.
|
||||||
|
set_process(false)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Make item blink in and out before despawning
|
||||||
|
if _time_left < time_to_despawn / 2:
|
||||||
|
_set_visible(int(_time_left * 5.0) % 2 == 0)
|
||||||
|
|
||||||
|
|
||||||
|
# `visible` is not a replicated property, so a blink driven only here would make
|
||||||
|
# the item flicker on the host and stay solid on clients. Until it is synced,
|
||||||
|
# only warn when there is nobody else to disagree with.
|
||||||
|
func _set_visible(value: bool) -> void:
|
||||||
|
if NetworkManager.is_online():
|
||||||
|
get_parent().visible = true
|
||||||
|
return
|
||||||
|
get_parent().visible = value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://deyna316ka4xs
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene format=3 uid="uid://b5ukku8i0hilb"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://deyna316ka4xs" path="res://Prefabs/despawning_item.gd" id="1_ayqk8"]
|
||||||
|
|
||||||
|
[node name="DespawningItem" type="Node" unique_id=303090111]
|
||||||
|
script = ExtResource("1_ayqk8")
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
[gd_scene format=3 uid="uid://mpapt5mkbuao"]
|
||||||
|
|
||||||
|
[sub_resource type="PhysicsMaterial" id="PhysicsMaterial_4m60m"]
|
||||||
|
friction = 0.0
|
||||||
|
bounce = 1.0
|
||||||
|
|
||||||
|
[sub_resource type="SphereShape3D" id="SphereShape3D_nn8tr"]
|
||||||
|
radius = 0.2752769
|
||||||
|
|
||||||
|
[node name="SlideOffDome" type="StaticBody3D" unique_id=1427609426]
|
||||||
|
collision_layer = 7
|
||||||
|
physics_material_override = SubResource("PhysicsMaterial_4m60m")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D2" type="CollisionShape3D" parent="." unique_id=513124640]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.99165964, 0)
|
||||||
|
shape = SubResource("SphereShape3D_nn8tr")
|
||||||
+44
-96
@@ -5,17 +5,15 @@
|
|||||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_gatbn"]
|
[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.tscn" id="4_3lufs"]
|
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" 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="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_kh26v"]
|
||||||
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="6_kwbxo"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="7_tejmp"]
|
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="7_tejmp"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="8_xcnr4"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="9_rwx83"]
|
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="9_rwx83"]
|
||||||
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="10_waew8"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="11_8wetd"]
|
|
||||||
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_ah4xa"]
|
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_ah4xa"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_46xi2"]
|
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_46xi2"]
|
||||||
[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="14_vftjt"]
|
[ext_resource type="PackedScene" uid="uid://ck5tuftqmyiue" path="res://Stations/plate_dispenser.tscn" id="14_h88sx"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cwnwo4i28upap" path="res://Stations/raw_burger_dispenser.tscn" id="15_m3h4l"]
|
||||||
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_tpcje"]
|
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_tpcje"]
|
||||||
[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_g2k6a"]
|
[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_g2k6a"]
|
||||||
|
[ext_resource type="Script" uid="uid://k8ywnvlhcic4" path="res://Scenes/testworldLoad.gd" id="16_m3h4l"]
|
||||||
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="17_gatbn"]
|
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="17_gatbn"]
|
||||||
[ext_resource type="PackedScene" uid="uid://xtcei2uvd5li" path="res://UI/game_over_panel.tscn" id="18_3lufs"]
|
[ext_resource type="PackedScene" uid="uid://xtcei2uvd5li" path="res://UI/game_over_panel.tscn" id="18_3lufs"]
|
||||||
[ext_resource type="Script" uid="uid://cwijoksyou1ey" path="res://Scenes/GameOvercontroler.gd" id="19_3lufs"]
|
[ext_resource type="Script" uid="uid://cwijoksyou1ey" path="res://Scenes/GameOvercontroler.gd" id="19_3lufs"]
|
||||||
@@ -43,11 +41,15 @@ size = Vector3(16, 10, 0.1)
|
|||||||
script = ExtResource("1_m3h4l")
|
script = ExtResource("1_m3h4l")
|
||||||
|
|
||||||
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_8pfaf")]
|
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_8pfaf")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||||
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_g2k6a")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 2.739665, 0.47583598, -1.1895752)
|
||||||
|
primary_duration = 35.0
|
||||||
|
|
||||||
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
|
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
||||||
|
|
||||||
@@ -55,116 +57,50 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
|||||||
shape = SubResource("BoxShape3D_vlqg6")
|
shape = SubResource("BoxShape3D_vlqg6")
|
||||||
|
|
||||||
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0002836883, 0.0006355047, -0.0054528117)
|
||||||
mesh = SubResource("BoxMesh_24d3s")
|
mesh = SubResource("BoxMesh_24d3s")
|
||||||
|
|
||||||
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_3lufs")]
|
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_3lufs")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.5, -1.484)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Hob2" parent="." unique_id=496572049 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Hob3" parent="." unique_id=421567982 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.2, 0.5, -1.2)
|
||||||
|
|
||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||||
environment = SubResource("Environment_bvwq1")
|
environment = SubResource("Environment_bvwq1")
|
||||||
|
|
||||||
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("6_kwbxo")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.4714175)
|
|
||||||
|
|
||||||
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_tejmp")]
|
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_tejmp")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.832131, 0.014894962, -1.4813508)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8, 0.015, -1.2)
|
||||||
|
|
||||||
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("8_xcnr4")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5717233, 1.5454081, 1.5373346)
|
|
||||||
|
|
||||||
[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("9_rwx83")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6888188, 1.4195822, -1.7110313)
|
|
||||||
|
|
||||||
[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("9_rwx83")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6931299, 1.5300478, -1.71225)
|
|
||||||
|
|
||||||
[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("9_rwx83")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69027674, 1.4969791, -1.7210286)
|
|
||||||
|
|
||||||
[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("9_rwx83")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69134104, 1.4543622, -1.7210286)
|
|
||||||
|
|
||||||
[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("10_waew8")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9352558, 1.623975, 1.2447833)
|
|
||||||
|
|
||||||
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.6225724, 1.4792972, -1.0473135)
|
|
||||||
|
|
||||||
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.6359743, 1.4639391, -1.1673055)
|
|
||||||
|
|
||||||
[node name="PickableObject3" parent="." unique_id=879935619 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.5142721, 1.4792972, -1.0473135)
|
|
||||||
|
|
||||||
[node name="PickableObject4" parent="." unique_id=198541902 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.5276739, 1.4639391, -1.1673055)
|
|
||||||
|
|
||||||
[node name="Hamburger2" parent="." unique_id=1349934579 instance=ExtResource("10_waew8")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9857153, 1.5329368, 0.9472374)
|
|
||||||
|
|
||||||
[node name="PickableObject5" parent="." unique_id=1021333509 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.73287535, 1.4792972, -1.0473135)
|
|
||||||
|
|
||||||
[node name="PickableObject6" parent="." unique_id=1268843669 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.7462772, 1.4639391, -1.1673055)
|
|
||||||
|
|
||||||
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_ah4xa")]
|
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_ah4xa")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3140475, 0.5, -1.4941733)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5997182, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Sink2" parent="." unique_id=1692612192 instance=ExtResource("12_ah4xa")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.1997182, 0.5, -1.2)
|
||||||
|
|
||||||
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_46xi2")]
|
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_46xi2")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.0864775, 0.5, -1.244947)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8000001, 0.5, -1.2)
|
||||||
|
|
||||||
[node name="Plate2" parent="." unique_id=356790445 instance=ExtResource("8_xcnr4")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5730225, 1.499024, 0.59790254)
|
|
||||||
|
|
||||||
[node name="CookedBurger" parent="." unique_id=1127807542 instance=ExtResource("14_vftjt")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4131018, -1.0928738)
|
|
||||||
|
|
||||||
[node name="CookedBurger2" parent="." unique_id=160088590 instance=ExtResource("14_vftjt")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4496142, -1.0928738)
|
|
||||||
|
|
||||||
[node name="CookedBurger3" parent="." unique_id=992183367 instance=ExtResource("14_vftjt")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344773, 1.4398065, -0.7096845)
|
|
||||||
|
|
||||||
[node name="CookedBurger4" parent="." unique_id=1837607086 instance=ExtResource("14_vftjt")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.525444, -1.0928738)
|
|
||||||
|
|
||||||
[node name="BurgerBuns2" parent="." unique_id=1210358958 instance=ExtResource("6_kwbxo")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3596323, 1.4110342, -0.22482127)
|
|
||||||
|
|
||||||
[node name="PickableObject7" parent="." unique_id=641653545 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.3556751, 1.4792972, 0.28881657)
|
|
||||||
|
|
||||||
[node name="PickableObject8" parent="." unique_id=1733819363 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.3422732, 1.4639391, 0.16882455)
|
|
||||||
|
|
||||||
[node name="PickableObject9" parent="." unique_id=12419746 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.4639754, 1.4792972, 0.28881657)
|
|
||||||
|
|
||||||
[node name="PickableObject10" parent="." unique_id=313160217 instance=ExtResource("11_8wetd")]
|
|
||||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.4505737, 1.4639391, 0.16882455)
|
|
||||||
|
|
||||||
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("15_tpcje")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7124918, 0.5, -1.4886917)
|
|
||||||
|
|
||||||
[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_tpcje")]
|
[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_tpcje")]
|
||||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.5, -0.4458799)
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, -0.6)
|
||||||
|
|
||||||
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_tpcje")]
|
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_tpcje")]
|
||||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.5, 0.55367994)
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, 0)
|
||||||
|
|
||||||
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_tpcje")]
|
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_tpcje")]
|
||||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.5, 1.5539298)
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, 0.6)
|
||||||
|
|
||||||
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_g2k6a")]
|
[node name="Counter5" parent="." unique_id=1487893288 instance=ExtResource("15_tpcje")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.139665, 0.9030438, 2.2267122)
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -0.6, 0.5, 1.2)
|
||||||
|
|
||||||
[node name="Hamburger3" parent="." unique_id=369573848 instance=ExtResource("10_waew8")]
|
[node name="Counter6" parent="." unique_id=1154936647 instance=ExtResource("15_tpcje")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7201865, 1.5329367, 0.14773655)
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0.5, 1.2)
|
||||||
|
|
||||||
[node name="Plate3" parent="." unique_id=2004250522 instance=ExtResource("8_xcnr4")]
|
[node name="Counter7" parent="." unique_id=990260272 instance=ExtResource("15_tpcje")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5818124, 1.499024, -0.4634577)
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0.6, 0.5, 1.2)
|
||||||
|
|
||||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=315740174]
|
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=315740174]
|
||||||
|
|
||||||
@@ -191,7 +127,19 @@ scene = ExtResource("18_3lufs")
|
|||||||
viewport_size = Vector2(900, 600)
|
viewport_size = Vector2(900, 600)
|
||||||
transparent = 0
|
transparent = 0
|
||||||
filter = false
|
filter = false
|
||||||
scene_properties_keys = PackedStringArray("main_menu_panel.gd")
|
scene_properties_keys = PackedStringArray("game_over_panel.gd")
|
||||||
|
|
||||||
[node name="controler" type="Node" parent="GameOverPanel" unique_id=1302491889]
|
[node name="controler" type="Node" parent="GameOverPanel" unique_id=1302491889]
|
||||||
script = ExtResource("19_3lufs")
|
script = ExtResource("19_3lufs")
|
||||||
|
|
||||||
|
[node name="PlateDispenser" parent="." unique_id=710538846 instance=ExtResource("14_h88sx")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8, 0, 1.2)
|
||||||
|
|
||||||
|
[node name="RawBurgerDispenser" parent="." unique_id=235630131 instance=ExtResource("15_m3h4l")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.2, 0, 1.2)
|
||||||
|
|
||||||
|
[node name="Node" type="Node" parent="." unique_id=1224933801]
|
||||||
|
script = ExtResource("16_m3h4l")
|
||||||
|
|
||||||
|
[node name="raw_burger" parent="." unique_id=1675596942 instance=ExtResource("9_rwx83")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 1.2, -0.6)
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
[gd_scene format=3 uid="uid://22shbqdvnwgo"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_m3h4l"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://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.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://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_ah4xa"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_46xi2"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://ck5tuftqmyiue" path="res://Stations/plate_dispenser.tscn" id="14_h88sx"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cwnwo4i28upap" path="res://Stations/raw_burger_dispenser.tscn" id="15_m3h4l"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_tpcje"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_g2k6a"]
|
||||||
|
[ext_resource type="Script" uid="uid://k8ywnvlhcic4" path="res://Scenes/testworldLoad.gd" id="16_m3h4l"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="17_gatbn"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://xtcei2uvd5li" path="res://UI/game_over_panel.tscn" id="18_3lufs"]
|
||||||
|
[ext_resource type="Script" uid="uid://cwijoksyou1ey" path="res://Scenes/GameOvercontroler.gd" id="19_3lufs"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||||
|
size = Vector3(16, 0.1, 16)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||||
|
material = ExtResource("3_gatbn")
|
||||||
|
size = Vector3(16, 0.1, 16)
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||||
|
background_mode = 2
|
||||||
|
sky = ExtResource("5_kh26v")
|
||||||
|
reflected_light_source = 2
|
||||||
|
ssr_enabled = true
|
||||||
|
ssao_enabled = true
|
||||||
|
ssil_enabled = true
|
||||||
|
sdfgi_enabled = true
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||||
|
size = Vector3(16, 10, 0.1)
|
||||||
|
|
||||||
|
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||||
|
script = ExtResource("1_m3h4l")
|
||||||
|
|
||||||
|
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_8pfaf")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||||
|
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=958841648]
|
||||||
|
shape = SubResource("BoxShape3D_vlqg6")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0002836883, 0.0006355047, -0.0054528117)
|
||||||
|
mesh = SubResource("BoxMesh_24d3s")
|
||||||
|
|
||||||
|
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Hob2" parent="." unique_id=496572049 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Hob3" parent="." unique_id=421567982 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.2, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||||
|
environment = SubResource("Environment_bvwq1")
|
||||||
|
|
||||||
|
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_tejmp")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8, 0.015, -1.2)
|
||||||
|
|
||||||
|
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_ah4xa")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5997182, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Sink2" parent="." unique_id=1692612192 instance=ExtResource("12_ah4xa")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.1997182, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_46xi2")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8000001, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, -0.6)
|
||||||
|
|
||||||
|
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, 0)
|
||||||
|
|
||||||
|
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, 0.6)
|
||||||
|
|
||||||
|
[node name="Counter5" parent="." unique_id=1487893288 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -0.6, 0.5, 1.2)
|
||||||
|
|
||||||
|
[node name="Counter6" parent="." unique_id=1154936647 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0.5, 1.2)
|
||||||
|
|
||||||
|
[node name="Counter7" parent="." unique_id=990260272 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0.6, 0.5, 1.2)
|
||||||
|
|
||||||
|
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_g2k6a")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 2.739665, 0.47583598, -1.1895752)
|
||||||
|
|
||||||
|
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=315740174]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=33059670]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.689, 8)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D2" type="CollisionShape3D" parent="StaticBody3D" unique_id=2018792171]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.689, -8)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D3" type="CollisionShape3D" parent="StaticBody3D" unique_id=757662929]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -8, 4.689, 0)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D4" type="CollisionShape3D" parent="StaticBody3D" unique_id=1468386997]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 8, 4.689, 0)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="GameOverPanel" parent="." unique_id=221361954 instance=ExtResource("17_gatbn")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.46454135, 3.4496074, -1.832967)
|
||||||
|
visible = false
|
||||||
|
scene = ExtResource("18_3lufs")
|
||||||
|
viewport_size = Vector2(900, 600)
|
||||||
|
transparent = 0
|
||||||
|
filter = false
|
||||||
|
scene_properties_keys = PackedStringArray("game_over_panel.gd")
|
||||||
|
|
||||||
|
[node name="controler" type="Node" parent="GameOverPanel" unique_id=1302491889]
|
||||||
|
script = ExtResource("19_3lufs")
|
||||||
|
|
||||||
|
[node name="PlateDispenser" parent="." unique_id=710538846 instance=ExtResource("14_h88sx")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8, 0, 1.2)
|
||||||
|
|
||||||
|
[node name="RawBurgerDispenser" parent="." unique_id=235630131 instance=ExtResource("15_m3h4l")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.2, 0, 1.2)
|
||||||
|
|
||||||
|
[node name="Node" type="Node" parent="." unique_id=1224933801]
|
||||||
|
script = ExtResource("16_m3h4l")
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
[gd_scene format=3 uid="uid://22shbqdvnwgo"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_m3h4l"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://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.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://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_ah4xa"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_46xi2"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://ck5tuftqmyiue" path="res://Stations/plate_dispenser.tscn" id="14_h88sx"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://cwnwo4i28upap" path="res://Stations/raw_burger_dispenser.tscn" id="15_m3h4l"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_tpcje"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_g2k6a"]
|
||||||
|
[ext_resource type="Script" uid="uid://k8ywnvlhcic4" path="res://Scenes/testworldLoad.gd" id="16_m3h4l"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="17_gatbn"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://xtcei2uvd5li" path="res://UI/game_over_panel.tscn" id="18_3lufs"]
|
||||||
|
[ext_resource type="Script" uid="uid://cwijoksyou1ey" path="res://Scenes/GameOvercontroler.gd" id="19_3lufs"]
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||||
|
size = Vector3(16, 0.1, 16)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||||
|
material = ExtResource("3_gatbn")
|
||||||
|
size = Vector3(16, 0.1, 16)
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||||
|
background_mode = 2
|
||||||
|
sky = ExtResource("5_kh26v")
|
||||||
|
reflected_light_source = 2
|
||||||
|
ssr_enabled = true
|
||||||
|
ssao_enabled = true
|
||||||
|
ssil_enabled = true
|
||||||
|
sdfgi_enabled = true
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||||
|
size = Vector3(16, 10, 0.1)
|
||||||
|
|
||||||
|
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||||
|
script = ExtResource("1_m3h4l")
|
||||||
|
|
||||||
|
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_8pfaf")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.6, 0)
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||||
|
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=958841648]
|
||||||
|
shape = SubResource("BoxShape3D_vlqg6")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.0002836883, 0.0006355047, -0.0054528117)
|
||||||
|
mesh = SubResource("BoxMesh_24d3s")
|
||||||
|
|
||||||
|
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Hob2" parent="." unique_id=496572049 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.6, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Hob3" parent="." unique_id=421567982 instance=ExtResource("4_3lufs")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.2, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||||
|
environment = SubResource("Environment_bvwq1")
|
||||||
|
|
||||||
|
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_tejmp")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8, 0.015, -1.2)
|
||||||
|
|
||||||
|
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_ah4xa")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.5997182, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Sink2" parent="." unique_id=1692612192 instance=ExtResource("12_ah4xa")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.1997182, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_46xi2")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.8000001, 0.5, -1.2)
|
||||||
|
|
||||||
|
[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, -0.6)
|
||||||
|
|
||||||
|
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, 0)
|
||||||
|
|
||||||
|
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.8, 0.5, 0.6)
|
||||||
|
|
||||||
|
[node name="Counter5" parent="." unique_id=1487893288 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, -0.6, 0.5, 1.2)
|
||||||
|
|
||||||
|
[node name="Counter6" parent="." unique_id=1154936647 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0, 0.5, 1.2)
|
||||||
|
|
||||||
|
[node name="Counter7" parent="." unique_id=990260272 instance=ExtResource("15_tpcje")]
|
||||||
|
transform = Transform3D(-1, 0, -8.742278e-08, 0, 1, 0, 8.742278e-08, 0, -1, 0.6, 0.5, 1.2)
|
||||||
|
|
||||||
|
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_g2k6a")]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 2.739665, 0.47583598, -1.1895752)
|
||||||
|
|
||||||
|
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=315740174]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=33059670]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.689, 8)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D2" type="CollisionShape3D" parent="StaticBody3D" unique_id=2018792171]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.689, -8)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D3" type="CollisionShape3D" parent="StaticBody3D" unique_id=757662929]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -8, 4.689, 0)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D4" type="CollisionShape3D" parent="StaticBody3D" unique_id=1468386997]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 8, 4.689, 0)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="GameOverPanel" parent="." unique_id=221361954 instance=ExtResource("17_gatbn")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.46454135, 3.4496074, -1.832967)
|
||||||
|
visible = false
|
||||||
|
scene = ExtResource("18_3lufs")
|
||||||
|
viewport_size = Vector2(900, 600)
|
||||||
|
transparent = 0
|
||||||
|
filter = false
|
||||||
|
scene_properties_keys = PackedStringArray("game_over_panel.gd")
|
||||||
|
|
||||||
|
[node name="controler" type="Node" parent="GameOverPanel" unique_id=1302491889]
|
||||||
|
script = ExtResource("19_3lufs")
|
||||||
|
|
||||||
|
[node name="PlateDispenser" parent="." unique_id=710538846 instance=ExtResource("14_h88sx")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8, 0, 1.2)
|
||||||
|
|
||||||
|
[node name="RawBurgerDispenser" parent="." unique_id=235630131 instance=ExtResource("15_m3h4l")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.2, 0, 1.2)
|
||||||
|
|
||||||
|
[node name="Node" type="Node" parent="." unique_id=1224933801]
|
||||||
|
script = ExtResource("16_m3h4l")
|
||||||
+19
-7
@@ -1,6 +1,6 @@
|
|||||||
[gd_scene format=3 uid="uid://do6mrslyqe5qe"]
|
[gd_scene format=3 uid="uid://do6mrslyqe5qe"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_57ppd"]
|
[ext_resource type="Script" uid="uid://d1cefhe3yyyds" path="res://Scenes/multiplayer_world.gd" id="1_57ppd"]
|
||||||
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_o1b3t"]
|
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_o1b3t"]
|
||||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_irf4n"]
|
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_irf4n"]
|
||||||
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_ctden"]
|
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_ctden"]
|
||||||
@@ -45,9 +45,19 @@ script = ExtResource("1_57ppd")
|
|||||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||||
environment = SubResource("Environment_bvwq1")
|
environment = SubResource("Environment_bvwq1")
|
||||||
|
|
||||||
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_o1b3t")]
|
[node name="XROrigin3D" parent="." unique_id=2055526621 groups=["local_xr_origin"] instance=ExtResource("2_o1b3t")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9667189, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9667189, 0)
|
||||||
|
|
||||||
|
[node name="WorldContent" type="Node3D" parent="." unique_id=262398468]
|
||||||
|
|
||||||
|
[node name="Players" type="Node3D" parent="." unique_id=1772076868]
|
||||||
|
|
||||||
|
[node name="ItemsSpawner" type="MultiplayerSpawner" parent="." unique_id=2051771581]
|
||||||
|
spawn_path = NodePath("../WorldContent")
|
||||||
|
|
||||||
|
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=619815427]
|
||||||
|
spawn_path = NodePath("../Players")
|
||||||
|
|
||||||
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||||
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||||
|
|
||||||
@@ -81,8 +91,7 @@ shape = SubResource("BoxShape3D_24d3s")
|
|||||||
|
|
||||||
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_di04w")]
|
[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_di04w")]
|
||||||
transform = Transform3D(-1, 0, 8.742277e-08, 0, 1, 0, -8.742277e-08, 0, -1, 3.6294794, 0.54177135, 0.0973109)
|
transform = Transform3D(-1, 0, 8.742277e-08, 0, 1, 0, -8.742277e-08, 0, -1, 3.6294794, 0.54177135, 0.0973109)
|
||||||
thinking_duration = 35.0
|
primary_duration = 35.0
|
||||||
primary_duration = 5.0
|
|
||||||
|
|
||||||
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_ctden")]
|
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_ctden")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.35077858, 0.50655985, -1.2833805)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.35077858, 0.50655985, -1.2833805)
|
||||||
@@ -105,15 +114,15 @@ transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.5
|
|||||||
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_di04w")]
|
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_di04w")]
|
||||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.5477214, 0.5115016, 0.49258912)
|
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.5477214, 0.5115016, 0.49258912)
|
||||||
|
|
||||||
|
[node name="Counter5" parent="Counter3" unique_id=200341072 instance=ExtResource("15_di04w")]
|
||||||
|
transform = Transform3D(-1, 0, 4.371139e-08, 0, 1, 0, -4.371139e-08, 0, -1, -0.12681937, 2.3841858e-07, 3.538899)
|
||||||
|
|
||||||
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("9_sq0s2")]
|
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("9_sq0s2")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5242664, 0.008738995, 1.1073059)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5242664, 0.008738995, 1.1073059)
|
||||||
|
|
||||||
[node name="RawBurgerDispenser" parent="." unique_id=235630131 instance=ExtResource("11_ctden")]
|
[node name="RawBurgerDispenser" parent="." unique_id=235630131 instance=ExtResource("11_ctden")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.8896122, 0.008738995, 1.0872328)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.8896122, 0.008738995, 1.0872328)
|
||||||
|
|
||||||
[node name="CubeSideDispenser2" parent="." unique_id=200950572 instance=ExtResource("12_57ppd")]
|
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.47788578, 0.008738995, 1.1293952)
|
|
||||||
|
|
||||||
[node name="PlateDispenser" parent="." unique_id=710538846 instance=ExtResource("13_irf4n")]
|
[node name="PlateDispenser" parent="." unique_id=710538846 instance=ExtResource("13_irf4n")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.26631236, 0.008738995, 1.087036)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.26631236, 0.008738995, 1.087036)
|
||||||
|
|
||||||
@@ -127,3 +136,6 @@ scene_properties_keys = PackedStringArray("game_over_panel.gd")
|
|||||||
|
|
||||||
[node name="controler" type="Node" parent="GameOverPanel" unique_id=803158176]
|
[node name="controler" type="Node" parent="GameOverPanel" unique_id=803158176]
|
||||||
script = ExtResource("16_n8fyw")
|
script = ExtResource("16_n8fyw")
|
||||||
|
|
||||||
|
[node name="CubeSideDispenser2" parent="." unique_id=200950572 instance=ExtResource("12_57ppd")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.35154933, 0, 1.1119425)
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_4m60m"]
|
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_4m60m"]
|
||||||
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="2_nn8tr"]
|
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="2_nn8tr"]
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="3_ahn1e"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="3_ahn1e"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://mpapt5mkbuao" path="res://Prefabs/slide_off_dome.tscn" id="4_nn8tr"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
||||||
radius = 0.3
|
radius = 0.3
|
||||||
|
|
||||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_24d3s"]
|
[sub_resource type="CylinderShape3D" id="CylinderShape3D_24d3s"]
|
||||||
|
radius = 0.29541016
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
||||||
albedo_color = Color(0.1764706, 1, 1, 1)
|
albedo_color = Color(0.1764706, 1, 1, 1)
|
||||||
@@ -29,15 +31,18 @@ snap_mode = 1
|
|||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=182042233]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=182042233]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.15291119, 0)
|
||||||
shape = SubResource("SphereShape3D_xmbo2")
|
shape = SubResource("SphereShape3D_xmbo2")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1974757539]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1974757539]
|
||||||
shape = SubResource("CylinderShape3D_24d3s")
|
shape = SubResource("CylinderShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="SlideOffDome" parent="." unique_id=1427609426 instance=ExtResource("4_nn8tr")]
|
||||||
|
|
||||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1172420592]
|
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1172420592]
|
||||||
|
|
||||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1073546772]
|
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1073546772]
|
||||||
radius = 0.4
|
radius = 0.3
|
||||||
sides = 32
|
sides = 32
|
||||||
|
|
||||||
[node name="Cubes" type="Node3D" parent="." unique_id=100532970]
|
[node name="Cubes" type="Node3D" parent="." unique_id=100532970]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_g30gi"]
|
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_g30gi"]
|
||||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_75ecy"]
|
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_75ecy"]
|
||||||
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"]
|
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"]
|
||||||
|
[ext_resource type="Script" uid="uid://biu4qr3nr1eny" path="res://test/mp_test_driver.gd" id="99_mptst"]
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||||
size = Vector3(15, 0.1, 15)
|
size = Vector3(15, 0.1, 15)
|
||||||
@@ -54,7 +55,6 @@ environment = SubResource("Environment_bvwq1")
|
|||||||
spawn_path = NodePath("../WorldContent")
|
spawn_path = NodePath("../WorldContent")
|
||||||
|
|
||||||
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
||||||
_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
|
|
||||||
spawn_path = NodePath("../Players")
|
spawn_path = NodePath("../Players")
|
||||||
|
|
||||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
||||||
@@ -74,3 +74,6 @@ shape = SubResource("BoxShape3D_arao0")
|
|||||||
[node name="CollisionShape3D4" type="CollisionShape3D" parent="StaticBody3D" unique_id=1431367494]
|
[node name="CollisionShape3D4" type="CollisionShape3D" parent="StaticBody3D" unique_id=1431367494]
|
||||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 7.502467, 9.197384, -0.018813023)
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 7.502467, 9.197384, -0.018813023)
|
||||||
shape = SubResource("BoxShape3D_arao0")
|
shape = SubResource("BoxShape3D_arao0")
|
||||||
|
|
||||||
|
[node name="TestDriver" type="Node" parent="."]
|
||||||
|
script = ExtResource("99_mptst")
|
||||||
|
|||||||
+75
-47
@@ -1,26 +1,26 @@
|
|||||||
extends Node3D
|
extends Node3D
|
||||||
|
|
||||||
## World script for the multiplayer scene. Consumes the host/join request set
|
## World script for the multiplayer scene. Starts the host/join the menu asked
|
||||||
## by the main menu, spawns the world's stations/items on whichever machine
|
## for, turns the authored kitchen into replicated objects, gives every connected
|
||||||
## owns the world (server, or the local player when offline), spawns/despawns
|
## peer an avatar, and returns to the menu when the session ends.
|
||||||
## a player avatar per connected peer, and returns to the menu if the session
|
|
||||||
## ends.
|
|
||||||
##
|
##
|
||||||
## The world's actual content (stations, items) is NOT baked into this scene —
|
## The world's contents are NOT left baked into the scene file. Whatever the
|
||||||
## it's spawned at runtime from Net/world_layout.gd via NetworkManager, so a
|
## scene authors is treated as a template: the server spawns real copies of it
|
||||||
## joining client receives it from the server (MultiplayerSpawner replays
|
## through NetWorld and every peer drops its own authored originals. A client
|
||||||
## existing spawns to late joiners) instead of relying on its own local copy
|
## therefore shows the server's world rather than assuming its local copy of the
|
||||||
## matching.
|
## scene matches — which is also exactly the path a player joining mid-session
|
||||||
|
## takes.
|
||||||
|
|
||||||
const PLAYER_SCENE := preload("res://Player/net_player.tscn")
|
const PLAYER_SCENE := "res://Player/net_player.tscn"
|
||||||
|
|
||||||
## Whether to spawn the full WorldLayout on the machine that owns the world.
|
## Whether to build the full kitchen on the machine that owns the world. The real
|
||||||
## The real game scene wants this; focused debug scenes (test/) bake their own
|
## game scene wants this; focused debug scenes bake their own handful of objects
|
||||||
## handful of stations and items instead and turn it off, so the thing under
|
## and turn it off, so the thing under test is not sharing the world with a
|
||||||
## test isn't sharing the world with a second copy of the whole kitchen.
|
## second copy of the whole kitchen.
|
||||||
@export var populate_from_layout: bool = true
|
@export var populate_from_layout: bool = true
|
||||||
|
|
||||||
var xr_interface: XRInterface
|
var xr_interface: XRInterface
|
||||||
|
var _net_world: NetWorld
|
||||||
var _populated := false
|
var _populated := false
|
||||||
|
|
||||||
|
|
||||||
@@ -30,34 +30,38 @@ func _ready() -> void:
|
|||||||
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
||||||
get_viewport().use_xr = true
|
get_viewport().use_xr = true
|
||||||
|
|
||||||
NetworkManager.register_world(self, $PlayersSpawner, $ItemsSpawner)
|
# Both roots go through the same generic replication hook, so a player avatar
|
||||||
|
# is just another replicated object — nothing about it is special-cased.
|
||||||
|
_net_world = NetWorld.new()
|
||||||
|
_net_world.name = "NetWorld"
|
||||||
|
add_child(_net_world)
|
||||||
|
_net_world.setup($ItemsSpawner, $WorldContent)
|
||||||
|
$PlayersSpawner.add_spawnable_scene(PLAYER_SCENE)
|
||||||
|
$Players.child_entered_tree.connect(_on_player_entered)
|
||||||
|
|
||||||
|
NetworkManager.register_world(self, _net_world)
|
||||||
NetworkManager.player_joined.connect(_on_player_joined)
|
NetworkManager.player_joined.connect(_on_player_joined)
|
||||||
NetworkManager.player_left.connect(_on_player_left)
|
NetworkManager.player_left.connect(_on_player_left)
|
||||||
NetworkManager.session_started.connect(_on_session_started)
|
NetworkManager.session_started.connect(_on_session_started)
|
||||||
NetworkManager.session_ended.connect(_on_session_ended)
|
NetworkManager.session_ended.connect(_on_session_ended)
|
||||||
NetworkManager.connection_failed.connect(_on_connection_failed)
|
NetworkManager.connection_failed.connect(_on_connection_failed)
|
||||||
|
|
||||||
# world_ready() is what actually calls host()/join() (or the cmdline
|
# world_ready() is what actually calls host()/join() (or the command-line
|
||||||
# equivalent). Populating before this point is wrong for EVERY case, not
|
# equivalent). Populating before this point would be wrong for every case, not
|
||||||
# just offline: is_online() is still false until host()/join() runs, so
|
# just offline: is_online() is still false until host()/join() runs, so
|
||||||
# owns_world() would read true for a joining client too, and it would
|
# owns_world() reads true for a joining client too and it would build its own
|
||||||
# build its own local copy instead of receiving the server's via the
|
# copy instead of receiving the server's. host() emits session_started
|
||||||
# spawner. host() emits session_started synchronously, which populates
|
# synchronously, which populates via _on_session_started; the call after
|
||||||
# via _on_session_started below; the explicit call after world_ready()
|
# world_ready() only covers the case where neither ran — this scene opened
|
||||||
# only matters for the case where neither host() nor join() ran (no
|
# directly, offline.
|
||||||
# pending session, no cmdline args) — running this scene directly offline.
|
|
||||||
NetworkManager.world_ready()
|
NetworkManager.world_ready()
|
||||||
_populate_world_if_owner()
|
_populate_world_if_owner()
|
||||||
|
|
||||||
get_tree().create_timer(3.0).timeout.connect(_log_world_state)
|
|
||||||
|
|
||||||
|
func _on_player_entered(node: Node) -> void:
|
||||||
# Temporary-ish sanity check: confirms WorldContent actually ended up
|
if node is MultiplayerSynchronizer:
|
||||||
# populated on this peer (whether by spawning it or by receiving it via
|
return
|
||||||
# replication), so a silent replication failure shows up in the net log
|
NetReplication.attach(node)
|
||||||
# instead of just an empty-looking world.
|
|
||||||
func _log_world_state() -> void:
|
|
||||||
NetworkManager.log_line("World state: WorldContent=%d children, Players=%d children" % [$WorldContent.get_child_count(), $Players.get_child_count()])
|
|
||||||
|
|
||||||
|
|
||||||
func _exit_tree() -> void:
|
func _exit_tree() -> void:
|
||||||
@@ -65,36 +69,60 @@ func _exit_tree() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _on_session_started(_is_server: bool) -> void:
|
func _on_session_started(_is_server: bool) -> void:
|
||||||
NetworkManager.gate_existing_stations()
|
|
||||||
_populate_world_if_owner()
|
_populate_world_if_owner()
|
||||||
|
|
||||||
|
|
||||||
## Spawns the world's stations/items exactly once, on the machine that owns
|
## Turns the authored template into replicated objects, exactly once, on the
|
||||||
## world logic (server or offline). Safe to call multiple times/entry points.
|
## machine that owns world logic. Safe to call from several entry points.
|
||||||
func _populate_world_if_owner() -> void:
|
func _populate_world_if_owner() -> void:
|
||||||
if not NetworkManager.owns_world() or _populated or not populate_from_layout:
|
if _populated or not populate_from_layout:
|
||||||
return
|
return
|
||||||
|
# WorldLayout reads the live scene tree to find what to replicate, so it has
|
||||||
|
# to be an instance sitting in that tree.
|
||||||
|
var layout := WorldLayout.new()
|
||||||
|
add_child(layout)
|
||||||
|
var authored := layout.get_authored_nodes()
|
||||||
|
|
||||||
|
# Every peer drops its authored copies. The client would otherwise show its
|
||||||
|
# local originals on top of the server's replicated ones, and the two sets
|
||||||
|
# would drift apart because only the server's are synced.
|
||||||
|
if not NetworkManager.owns_world():
|
||||||
|
NetworkManager.log_line("Clearing %d authored nodes; the server's copies replace them" % authored.size())
|
||||||
|
_remove_authored(authored)
|
||||||
|
layout.queue_free()
|
||||||
|
return
|
||||||
|
|
||||||
_populated = true
|
_populated = true
|
||||||
GameManager.meals_in_play = ["hamburger"]
|
GameManager.meals_in_play = ["hamburger"]
|
||||||
var stations := WorldLayout.get_stations()
|
var objects := layout.describe(authored)
|
||||||
var items := WorldLayout.get_items()
|
layout.queue_free()
|
||||||
NetworkManager.log_line("Populating world: %d stations, %d items" % [stations.size(), items.size()])
|
_remove_authored(authored)
|
||||||
for d in stations:
|
NetworkManager.log_line("Populating world: %d objects" % objects.size())
|
||||||
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
for d in objects:
|
||||||
for d in items:
|
|
||||||
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
||||||
NetworkManager.log_line("World populated")
|
NetworkManager.log_line("World populated")
|
||||||
|
|
||||||
|
|
||||||
## Only the server (or the single offline machine) materialises player
|
# Freed immediately rather than queue_free()d, so the names are released before
|
||||||
## avatars; MultiplayerSpawner replicates the result to everyone else,
|
# the replicated copies are spawned under the same ones.
|
||||||
## including late joiners.
|
func _remove_authored(nodes: Array[Node]) -> void:
|
||||||
|
for node in nodes:
|
||||||
|
if is_instance_valid(node):
|
||||||
|
node.get_parent().remove_child(node)
|
||||||
|
node.free()
|
||||||
|
|
||||||
|
|
||||||
|
## Only the server (or the single offline machine) creates avatars;
|
||||||
|
## MultiplayerSpawner replicates the result to everyone else, late joiners
|
||||||
|
## included.
|
||||||
func _on_player_joined(peer_id: int) -> void:
|
func _on_player_joined(peer_id: int) -> void:
|
||||||
if not NetworkManager.owns_world() or $Players.has_node(str(peer_id)):
|
if not NetworkManager.owns_world() or $Players.has_node(str(peer_id)):
|
||||||
return
|
return
|
||||||
var p := PLAYER_SCENE.instantiate()
|
var p: Node = load(PLAYER_SCENE).instantiate()
|
||||||
|
# The name is the peer id, which is how net_player.gd knows whose avatar it
|
||||||
|
# is on every peer — MultiplayerSpawner replicates the name.
|
||||||
p.name = str(peer_id)
|
p.name = str(peer_id)
|
||||||
$Players.add_child(p, true)
|
$Players.add_child(p)
|
||||||
NetworkManager.log_line("Spawned avatar for peer %d" % peer_id)
|
NetworkManager.log_line("Spawned avatar for peer %d" % peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,84 @@
|
|||||||
|
[gd_scene format=3 uid="uid://dbgu4r127ke5v"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_1ymas"]
|
||||||
|
[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://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.tscn" id="5_gfdi7"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="6_fmmg3"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="7_jnxsa"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="8_xl5nn"]
|
||||||
|
|
||||||
|
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||||
|
background_mode = 2
|
||||||
|
sky = ExtResource("2_1jq86")
|
||||||
|
ambient_light_source = 3
|
||||||
|
ambient_light_color = Color(1, 1, 1, 1)
|
||||||
|
ambient_light_sky_contribution = 0.9
|
||||||
|
reflected_light_source = 2
|
||||||
|
ssr_enabled = true
|
||||||
|
ssao_enabled = true
|
||||||
|
ssil_enabled = true
|
||||||
|
sdfgi_enabled = true
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||||
|
size = Vector3(20, 0.1, 20)
|
||||||
|
|
||||||
|
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||||
|
material = ExtResource("4_kpgm6")
|
||||||
|
size = Vector3(20, 0.1, 20)
|
||||||
|
|
||||||
|
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||||
|
size = Vector3(20, 10, 0.1)
|
||||||
|
|
||||||
|
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||||
|
script = ExtResource("1_1ymas")
|
||||||
|
|
||||||
|
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||||
|
environment = SubResource("Environment_bvwq1")
|
||||||
|
|
||||||
|
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("3_bryd6")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.9667189, 0)
|
||||||
|
|
||||||
|
[node name="DirectionalLight3D" type="DirectionalLight3D" parent="." unique_id=1376644384]
|
||||||
|
transform = Transform3D(-0.8660254, -0.43301278, 0.24999999, 0.3022632, -0.05510214, 0.9516306, -0.39829266, 0.8997021, 0.17860365, 0, 0, 0)
|
||||||
|
|
||||||
|
[node name="Floor" type="StaticBody3D" parent="." unique_id=122279514]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.05, 0)
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="Floor" unique_id=958841648]
|
||||||
|
shape = SubResource("BoxShape3D_vlqg6")
|
||||||
|
|
||||||
|
[node name="MeshInstance3D" type="MeshInstance3D" parent="Floor/CollisionShape3D" unique_id=1939997056]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661)
|
||||||
|
mesh = SubResource("BoxMesh_24d3s")
|
||||||
|
|
||||||
|
[node name="InvisibleWalls" type="StaticBody3D" parent="." unique_id=315740174]
|
||||||
|
|
||||||
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="InvisibleWalls" unique_id=33059670]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.688614, 8.838269)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D2" type="CollisionShape3D" parent="InvisibleWalls" unique_id=2018792171]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.688614, -10.243342)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D3" type="CollisionShape3D" parent="InvisibleWalls" unique_id=757662929]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -9.807113, 4.688614, -0.018813243)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="CollisionShape3D4" type="CollisionShape3D" parent="InvisibleWalls" unique_id=1468386997]
|
||||||
|
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 9.934778, 4.297072, -0.018813023)
|
||||||
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("5_gfdi7")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.35077858, 0.50655985, -1.2833805)
|
||||||
|
|
||||||
|
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("6_fmmg3")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.24685252, 0.51461196, -1.2828919)
|
||||||
|
|
||||||
|
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("7_jnxsa")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.94890714, 0.5115015, -1.3041471)
|
||||||
|
|
||||||
|
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("8_xl5nn")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.87560004, 1.5534955, -1.2869046)
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
|
||||||
|
# Called when the node enters the scene tree for the first time.
|
||||||
|
func _ready() -> void:
|
||||||
|
print(get_stations())
|
||||||
|
print(get_items())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
func get_node_data(node):
|
||||||
|
# {"scene": "res://Stations/Hob.tscn", "name": "Hob",
|
||||||
|
# "xform": Transform3D(Basis(), Vector3(0.29336345, 0.8981018, -1.484)), "props": {}},
|
||||||
|
var data = {}
|
||||||
|
data["scene"] = node.scene_file_path
|
||||||
|
data["name"] = node.name
|
||||||
|
data["xform"] = node.transform
|
||||||
|
data["props"] = {}
|
||||||
|
var scrip = node.get_script()
|
||||||
|
if scrip:
|
||||||
|
for i in scrip.get_script_property_list():
|
||||||
|
if i.usage & PROPERTY_USAGE_STORAGE:
|
||||||
|
data["props"][i["name"]] = node.get(i["name"])
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
func get_stations() -> Array[Dictionary]:
|
||||||
|
var Stations = ResourceLoader.list_directory("res://Stations/")
|
||||||
|
|
||||||
|
var Stations2 = []
|
||||||
|
for i in Stations:
|
||||||
|
Stations2.append("res://Stations/" + i)
|
||||||
|
Stations = Stations2
|
||||||
|
|
||||||
|
var Stations_nodes = []
|
||||||
|
for i in get_tree().root.find_children("*", "StaticBody3D", true, false):
|
||||||
|
if i.scene_file_path in Stations:
|
||||||
|
Stations_nodes.append(i)
|
||||||
|
|
||||||
|
var data: Array[Dictionary] = []
|
||||||
|
for i in Stations_nodes:
|
||||||
|
data.append(get_node_data(i))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
func get_items() -> Array[Dictionary]:
|
||||||
|
var Items = ResourceLoader.list_directory("res://Items/")
|
||||||
|
|
||||||
|
var Items2 = []
|
||||||
|
for i in Items:
|
||||||
|
Items2.append("res://Items/" + i)
|
||||||
|
Items = Items2
|
||||||
|
|
||||||
|
var Items_nodes = []
|
||||||
|
for i in get_tree().root.find_children("*", "XRToolsPickable", true, false):
|
||||||
|
if i.scene_file_path in Items:
|
||||||
|
Items_nodes.append(i)
|
||||||
|
|
||||||
|
var data: Array[Dictionary] = []
|
||||||
|
for i in Items_nodes:
|
||||||
|
data.append(get_node_data(i))
|
||||||
|
return data
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://k8ywnvlhcic4
|
||||||
Binary file not shown.
@@ -0,0 +1,19 @@
|
|||||||
|
[remap]
|
||||||
|
|
||||||
|
importer="mp3"
|
||||||
|
type="AudioStreamMP3"
|
||||||
|
uid="uid://clkjwcawdqvta"
|
||||||
|
path="res://.godot/imported/money.mp3-95c717b8402507d80974f4f4287b0b63.mp3str"
|
||||||
|
|
||||||
|
[deps]
|
||||||
|
|
||||||
|
source_file="res://Sounds/money.mp3"
|
||||||
|
dest_files=["res://.godot/imported/money.mp3-95c717b8402507d80974f4f4287b0b63.mp3str"]
|
||||||
|
|
||||||
|
[params]
|
||||||
|
|
||||||
|
loop=false
|
||||||
|
loop_offset=0.0
|
||||||
|
bpm=0.0
|
||||||
|
beat_count=0
|
||||||
|
bar_beats=4
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_myl3s"]
|
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_myl3s"]
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_p30r1"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_p30r1"]
|
||||||
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="2_1q74o"]
|
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="2_1q74o"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://mpapt5mkbuao" path="res://Prefabs/slide_off_dome.tscn" id="4_rf1b2"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
||||||
radius = 0.3
|
radius = 0.3
|
||||||
@@ -31,6 +32,8 @@ shape = SubResource("SphereShape3D_xmbo2")
|
|||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1470079357]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1470079357]
|
||||||
shape = SubResource("CylinderShape3D_24d3s")
|
shape = SubResource("CylinderShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="SlideOffDome" parent="." unique_id=1427609426 instance=ExtResource("4_rf1b2")]
|
||||||
|
|
||||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1746426494]
|
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=1746426494]
|
||||||
|
|
||||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=858311379]
|
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=858311379]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[gd_scene format=3 uid="uid://efaec6ymgabo"]
|
[gd_scene format=3 uid="uid://efaec6ymgabo"]
|
||||||
|
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_dlkho"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_dlkho"]
|
||||||
[ext_resource type="AudioStream" uid="uid://claw3d2wnyr6a" path="res://Sounds/220212__gameaudio__ping-bing.wav" id="2_0aadn"]
|
[ext_resource type="AudioStream" uid="uid://bqtk1adk8umbx" path="res://Sounds/220197__gameaudio__click-basic.wav" id="2_0aadn"]
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||||
size = Vector3(0.6, 1, 0.6)
|
size = Vector3(0.6, 1, 0.6)
|
||||||
@@ -36,6 +36,8 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.002, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.002, 0)
|
||||||
shape = SubResource("BoxShape3D_1ntq7")
|
shape = SubResource("BoxShape3D_1ntq7")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="XRToolsSnapZone" unique_id=1786831079]
|
||||||
|
|
||||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=451757283]
|
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=451757283]
|
||||||
use_collision = true
|
use_collision = true
|
||||||
|
|
||||||
|
|||||||
+95
-64
@@ -2,7 +2,10 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://bsn8vhv5adxdo" path="res://Stations/hob.gd" id="1_7jc4g"]
|
[ext_resource type="Script" uid="uid://bsn8vhv5adxdo" path="res://Stations/hob.gd" id="1_7jc4g"]
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_er1dp"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="2_er1dp"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://bqtk1adk8umbx" path="res://Sounds/220197__gameaudio__click-basic.wav" id="3_6oyg1"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bxbocxdaayvwx" path="res://UI/progress_bar.tscn" id="3_7uuqv"]
|
[ext_resource type="PackedScene" uid="uid://bxbocxdaayvwx" path="res://UI/progress_bar.tscn" id="3_7uuqv"]
|
||||||
|
[ext_resource type="Material" uid="uid://d1djvskv4n4m3" path="res://Textures/hob_off.tres" id="4_8qpxk"]
|
||||||
|
[ext_resource type="Material" uid="uid://db7c7w4apwti7" path="res://Textures/hob_on.tres" id="6_m7l4u"]
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_kdxnr"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_kdxnr"]
|
||||||
size = Vector3(0.6, 1, 0.6)
|
size = Vector3(0.6, 1, 0.6)
|
||||||
@@ -10,112 +13,133 @@ size = Vector3(0.6, 1, 0.6)
|
|||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_7uuqv"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_7uuqv"]
|
||||||
size = Vector3(0.6, 0.12802735, 0.6)
|
size = Vector3(0.6, 0.12802735, 0.6)
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_di04w"]
|
|
||||||
albedo_color = Color(0, 0, 0, 1)
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_gkb3v"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_gkb3v"]
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_hob"]
|
|
||||||
properties/0/path = NodePath(".:time_cooked")
|
|
||||||
properties/0/spawn = true
|
|
||||||
properties/0/replication_mode = 1
|
|
||||||
properties/1/path = NodePath(".:cooking_result_time")
|
|
||||||
properties/1/spawn = true
|
|
||||||
properties/1/replication_mode = 1
|
|
||||||
|
|
||||||
[sub_resource type="Animation" id="Animation_7uuqv"]
|
[sub_resource type="Animation" id="Animation_7uuqv"]
|
||||||
length = 0.001
|
length = 0.001
|
||||||
tracks/0/type = "bezier"
|
tracks/0/type = "value"
|
||||||
tracks/0/imported = false
|
tracks/0/imported = false
|
||||||
tracks/0/enabled = true
|
tracks/0/enabled = true
|
||||||
tracks/0/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:r")
|
tracks/0/path = NodePath("CSGCombiner3D/CSGTorus3D3:material")
|
||||||
tracks/0/interp = 1
|
tracks/0/interp = 1
|
||||||
tracks/0/loop_wrap = true
|
tracks/0/loop_wrap = true
|
||||||
tracks/0/keys = {
|
tracks/0/keys = {
|
||||||
"handle_modes": PackedInt32Array(0),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0)
|
"update": 1,
|
||||||
|
"values": [ExtResource("4_8qpxk")]
|
||||||
}
|
}
|
||||||
tracks/1/type = "bezier"
|
tracks/1/type = "value"
|
||||||
tracks/1/imported = false
|
tracks/1/imported = false
|
||||||
tracks/1/enabled = true
|
tracks/1/enabled = true
|
||||||
tracks/1/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:g")
|
tracks/1/path = NodePath("CSGCombiner3D/CSGTorus3D2:material")
|
||||||
tracks/1/interp = 1
|
tracks/1/interp = 1
|
||||||
tracks/1/loop_wrap = true
|
tracks/1/loop_wrap = true
|
||||||
tracks/1/keys = {
|
tracks/1/keys = {
|
||||||
"handle_modes": PackedInt32Array(0),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0)
|
"update": 1,
|
||||||
|
"values": [ExtResource("4_8qpxk")]
|
||||||
}
|
}
|
||||||
tracks/2/type = "bezier"
|
tracks/2/type = "value"
|
||||||
tracks/2/imported = false
|
tracks/2/imported = false
|
||||||
tracks/2/enabled = true
|
tracks/2/enabled = true
|
||||||
tracks/2/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:b")
|
tracks/2/path = NodePath("CSGCombiner3D/CSGTorus3D:material")
|
||||||
tracks/2/interp = 1
|
tracks/2/interp = 1
|
||||||
tracks/2/loop_wrap = true
|
tracks/2/loop_wrap = true
|
||||||
tracks/2/keys = {
|
tracks/2/keys = {
|
||||||
"handle_modes": PackedInt32Array(0),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0)
|
"update": 1,
|
||||||
|
"values": [ExtResource("4_8qpxk")]
|
||||||
}
|
}
|
||||||
tracks/3/type = "bezier"
|
tracks/3/type = "value"
|
||||||
tracks/3/imported = false
|
tracks/3/imported = false
|
||||||
tracks/3/enabled = true
|
tracks/3/enabled = true
|
||||||
tracks/3/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:a")
|
tracks/3/path = NodePath("CSGCombiner3D/CSGTorus3D3:material:emission_energy_multiplier")
|
||||||
tracks/3/interp = 1
|
tracks/3/interp = 1
|
||||||
tracks/3/loop_wrap = true
|
tracks/3/loop_wrap = true
|
||||||
tracks/3/keys = {
|
tracks/3/keys = {
|
||||||
"handle_modes": PackedInt32Array(0),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0)
|
"update": 0,
|
||||||
|
"values": [0.0]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("OmniLight3D:light_energy")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0),
|
||||||
|
"transitions": PackedFloat32Array(1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [0.0]
|
||||||
}
|
}
|
||||||
|
|
||||||
[sub_resource type="Animation" id="Animation_6oyg1"]
|
[sub_resource type="Animation" id="Animation_6oyg1"]
|
||||||
resource_name = "hob"
|
resource_name = "hob"
|
||||||
loop_mode = 1
|
loop_mode = 1
|
||||||
tracks/0/type = "bezier"
|
tracks/0/type = "value"
|
||||||
tracks/0/imported = false
|
tracks/0/imported = false
|
||||||
tracks/0/enabled = true
|
tracks/0/enabled = true
|
||||||
tracks/0/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:r")
|
tracks/0/path = NodePath("CSGCombiner3D/CSGTorus3D3:material")
|
||||||
tracks/0/interp = 1
|
tracks/0/interp = 1
|
||||||
tracks/0/loop_wrap = true
|
tracks/0/loop_wrap = true
|
||||||
tracks/0/keys = {
|
tracks/0/keys = {
|
||||||
"handle_modes": PackedInt32Array(0, 2, 2),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(0.5, -0.25, 0, 0.25, 0, 0.8, -0.083333336, 0, 0, 0, 0.5, -0.083333336, 0, 0, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0, 0.5, 1)
|
"update": 1,
|
||||||
|
"values": [ExtResource("6_m7l4u")]
|
||||||
}
|
}
|
||||||
tracks/1/type = "bezier"
|
tracks/1/type = "value"
|
||||||
tracks/1/imported = false
|
tracks/1/imported = false
|
||||||
tracks/1/enabled = true
|
tracks/1/enabled = true
|
||||||
tracks/1/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:g")
|
tracks/1/path = NodePath("CSGCombiner3D/CSGTorus3D2:material")
|
||||||
tracks/1/interp = 1
|
tracks/1/interp = 1
|
||||||
tracks/1/loop_wrap = true
|
tracks/1/loop_wrap = true
|
||||||
tracks/1/keys = {
|
tracks/1/keys = {
|
||||||
"handle_modes": PackedInt32Array(0, 2, 2),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 0, -0.083333336, 0, 0, 0, 0, -0.083333336, 0, 0, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0, 0.5, 1)
|
"update": 1,
|
||||||
|
"values": [ExtResource("6_m7l4u")]
|
||||||
}
|
}
|
||||||
tracks/2/type = "bezier"
|
tracks/2/type = "value"
|
||||||
tracks/2/imported = false
|
tracks/2/imported = false
|
||||||
tracks/2/enabled = true
|
tracks/2/enabled = true
|
||||||
tracks/2/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:b")
|
tracks/2/path = NodePath("CSGCombiner3D/CSGTorus3D:material")
|
||||||
tracks/2/interp = 1
|
tracks/2/interp = 1
|
||||||
tracks/2/loop_wrap = true
|
tracks/2/loop_wrap = true
|
||||||
tracks/2/keys = {
|
tracks/2/keys = {
|
||||||
"handle_modes": PackedInt32Array(0, 2, 2),
|
"times": PackedFloat32Array(0),
|
||||||
"points": PackedFloat32Array(0, -0.25, 0, 0.25, 0, 0, -0.083333336, 0, 0, 0, 0, -0.083333336, 0, 0, 0),
|
"transitions": PackedFloat32Array(1),
|
||||||
"times": PackedFloat32Array(0, 0.5, 1)
|
"update": 1,
|
||||||
|
"values": [ExtResource("6_m7l4u")]
|
||||||
}
|
}
|
||||||
tracks/3/type = "bezier"
|
tracks/3/type = "value"
|
||||||
tracks/3/imported = false
|
tracks/3/imported = false
|
||||||
tracks/3/enabled = true
|
tracks/3/enabled = true
|
||||||
tracks/3/path = NodePath("CSGCombiner3D/CSGTorus3D:material:albedo_color:a")
|
tracks/3/path = NodePath("CSGCombiner3D/CSGTorus3D3:material:emission_energy_multiplier")
|
||||||
tracks/3/interp = 1
|
tracks/3/interp = 1
|
||||||
tracks/3/loop_wrap = true
|
tracks/3/loop_wrap = true
|
||||||
tracks/3/keys = {
|
tracks/3/keys = {
|
||||||
"handle_modes": PackedInt32Array(0, 2, 2),
|
"times": PackedFloat32Array(0, 0.5, 1),
|
||||||
"points": PackedFloat32Array(1, -0.25, 0, 0.25, 0, 1, -0.083333336, 0, 0, 0, 1, -0.083333336, 0, 0, 0),
|
"transitions": PackedFloat32Array(1, 1, 1),
|
||||||
"times": PackedFloat32Array(0, 0.5, 1)
|
"update": 0,
|
||||||
|
"values": [1.0, 0.0, 1.0]
|
||||||
|
}
|
||||||
|
tracks/4/type = "value"
|
||||||
|
tracks/4/imported = false
|
||||||
|
tracks/4/enabled = true
|
||||||
|
tracks/4/path = NodePath("OmniLight3D:light_energy")
|
||||||
|
tracks/4/interp = 1
|
||||||
|
tracks/4/loop_wrap = true
|
||||||
|
tracks/4/keys = {
|
||||||
|
"times": PackedFloat32Array(0, 0.5, 1),
|
||||||
|
"transitions": PackedFloat32Array(1, 1, 1),
|
||||||
|
"update": 0,
|
||||||
|
"values": [1.0, 0.0, 1.0]
|
||||||
}
|
}
|
||||||
|
|
||||||
[sub_resource type="AnimationLibrary" id="AnimationLibrary_ac5f3"]
|
[sub_resource type="AnimationLibrary" id="AnimationLibrary_ac5f3"]
|
||||||
@@ -130,11 +154,12 @@ script = ExtResource("1_7jc4g")
|
|||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1283846023]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=1283846023]
|
||||||
shape = SubResource("BoxShape3D_kdxnr")
|
shape = SubResource("BoxShape3D_kdxnr")
|
||||||
|
|
||||||
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=538157739]
|
[node name="XRToolsSnapZone" type="Area3D" parent="." unique_id=538157739 groups=["station"]]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0)
|
||||||
collision_layer = 65536
|
collision_layer = 65536
|
||||||
collision_mask = 65540
|
collision_mask = 65540
|
||||||
script = ExtResource("2_er1dp")
|
script = ExtResource("2_er1dp")
|
||||||
|
stash_sound = ExtResource("3_6oyg1")
|
||||||
snap_mode = 1
|
snap_mode = 1
|
||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
@@ -142,6 +167,9 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.000975132, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.000975132, 0)
|
||||||
shape = SubResource("BoxShape3D_7uuqv")
|
shape = SubResource("BoxShape3D_7uuqv")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="XRToolsSnapZone" unique_id=1991627595]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.022546828, -0.5270121, 0.36528283)
|
||||||
|
|
||||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=70901698]
|
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=70901698]
|
||||||
|
|
||||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1111505172]
|
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1111505172]
|
||||||
@@ -160,21 +188,21 @@ transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.
|
|||||||
inner_radius = 0.09153879
|
inner_radius = 0.09153879
|
||||||
outer_radius = 0.120084204
|
outer_radius = 0.120084204
|
||||||
sides = 32
|
sides = 32
|
||||||
material = SubResource("StandardMaterial3D_di04w")
|
material = ExtResource("4_8qpxk")
|
||||||
|
|
||||||
[node name="CSGTorus3D" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=1901978664]
|
|
||||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
|
||||||
inner_radius = 0.20426142
|
|
||||||
outer_radius = 0.2321898
|
|
||||||
sides = 32
|
|
||||||
material = SubResource("StandardMaterial3D_di04w")
|
|
||||||
|
|
||||||
[node name="CSGTorus3D2" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=24541894]
|
[node name="CSGTorus3D2" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=24541894]
|
||||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
||||||
inner_radius = 0.15036294
|
inner_radius = 0.15036294
|
||||||
outer_radius = 0.17592031
|
outer_radius = 0.17592031
|
||||||
sides = 32
|
sides = 32
|
||||||
material = SubResource("StandardMaterial3D_di04w")
|
material = ExtResource("4_8qpxk")
|
||||||
|
|
||||||
|
[node name="CSGTorus3D" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=1901978664]
|
||||||
|
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.5003869, -7.827557e-05)
|
||||||
|
inner_radius = 0.20426142
|
||||||
|
outer_radius = 0.2321898
|
||||||
|
sides = 32
|
||||||
|
material = ExtResource("4_8qpxk")
|
||||||
|
|
||||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1107239311]
|
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1107239311]
|
||||||
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.4498226, -7.827557e-05)
|
transform = Transform3D(0.8943019, 0, 0, 0, 0.8943019, 0, 0, 0, 0.8943019, 0, 0.4498226, -7.827557e-05)
|
||||||
@@ -223,11 +251,14 @@ size = Vector3(0.55, 0.835, 0.072)
|
|||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.003479004, -0.45215607, 0.24655426)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.003479004, -0.45215607, 0.24655426)
|
||||||
size = Vector3(0.55, 0.096, 0.14)
|
size = Vector3(0.55, 0.096, 0.14)
|
||||||
|
|
||||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=768992259]
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_hob")
|
|
||||||
|
|
||||||
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("3_7uuqv")]
|
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("3_7uuqv")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.74736404, -0.09216304)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.74736404, -0.09216304)
|
||||||
|
|
||||||
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1525460719]
|
[node name="AnimationPlayer" type="AnimationPlayer" parent="." unique_id=1525460719]
|
||||||
libraries/ = SubResource("AnimationLibrary_ac5f3")
|
libraries/ = SubResource("AnimationLibrary_ac5f3")
|
||||||
|
|
||||||
|
[node name="OmniLight3D" type="OmniLight3D" parent="." unique_id=1618457623]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5053487, 0)
|
||||||
|
light_color = Color(1, 0.13725491, 0, 1)
|
||||||
|
light_energy = 0.0
|
||||||
|
omni_range = 0.5
|
||||||
|
|||||||
@@ -0,0 +1,4 @@
|
|||||||
|
[gd_scene format=3 uid="uid://cwwnbx5uat3fw"]
|
||||||
|
|
||||||
|
[node name="CSGBox3D" type="CSGBox3D" unique_id=725518829]
|
||||||
|
|
||||||
@@ -13,8 +13,8 @@ albedo_color = Color(0.45262197, 0.26770625, 0.2601587, 1)
|
|||||||
material = SubResource("StandardMaterial3D_qn1ku")
|
material = SubResource("StandardMaterial3D_qn1ku")
|
||||||
size = Vector3(0.5, 1, 0.5)
|
size = Vector3(0.5, 1, 0.5)
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_mep2a"]
|
||||||
radius = 0.3
|
size = Vector3(0.81640625, 0.5635376, 0.79351807)
|
||||||
|
|
||||||
[node name="DirtStation" type="StaticBody3D" unique_id=160842153 groups=["station"]]
|
[node name="DirtStation" type="StaticBody3D" unique_id=160842153 groups=["station"]]
|
||||||
script = ExtResource("1_hc1d4")
|
script = ExtResource("1_hc1d4")
|
||||||
@@ -39,5 +39,5 @@ snap_mode = 1
|
|||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1398712192]
|
[node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1398712192]
|
||||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0.008666992, -0.11009073, -0.009353638)
|
||||||
shape = SubResource("SphereShape3D_dlkho")
|
shape = SubResource("BoxShape3D_mep2a")
|
||||||
|
|||||||
+47
-13
@@ -6,24 +6,63 @@ const RECIPE_MANAGER = preload("res://RecipeManager.gd")
|
|||||||
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
||||||
@onready var progress_bar: ProgressBar3D = $ProgressBar3D
|
@onready var progress_bar: ProgressBar3D = $ProgressBar3D
|
||||||
|
|
||||||
var time_cooked = 0
|
## Cooking state. All three are replicated (see Hob.tscn's Sync node) and each
|
||||||
var cooking_result: String
|
## refreshes the display when it changes, which is what makes the hob look alive
|
||||||
var cooking_result_time = 0
|
## on a client: only the world owner runs the station's _process and its snap
|
||||||
|
## zone, so a client never reaches the code below by itself and would otherwise
|
||||||
|
## show a hob that never lights up.
|
||||||
|
var time_cooked: float = 0.0: set = _set_time_cooked
|
||||||
|
var cooking_result: String = "": set = _set_cooking_result
|
||||||
|
var cooking_result_time: float = 0.0: set = _set_cooking_result_time
|
||||||
|
|
||||||
|
|
||||||
|
func _set_time_cooked(value: float) -> void:
|
||||||
|
if is_equal_approx(time_cooked, value):
|
||||||
|
return
|
||||||
|
time_cooked = value
|
||||||
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_cooking_result(value: String) -> void:
|
||||||
|
if cooking_result == value:
|
||||||
|
return
|
||||||
|
cooking_result = value
|
||||||
|
_refresh_display()
|
||||||
|
# The flames follow whether we're cooking, on every peer.
|
||||||
|
_play_animation("hob" if not cooking_result.is_empty() else "RESET")
|
||||||
|
|
||||||
|
|
||||||
|
func _set_cooking_result_time(value: float) -> void:
|
||||||
|
if is_equal_approx(cooking_result_time, value):
|
||||||
|
return
|
||||||
|
cooking_result_time = value
|
||||||
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if not progress_bar or progress_bar is not ProgressBar3D:
|
if not progress_bar or progress_bar is not ProgressBar3D:
|
||||||
push_error("Hob missing reference to progressbar, or wrong type:", progress_bar)
|
push_error("Hob missing reference to progressbar, or wrong type:", progress_bar)
|
||||||
snap_zone.has_picked_up.connect(_on_object_picked_up)
|
snap_zone.has_picked_up.connect(_on_object_picked_up)
|
||||||
snap_zone.has_dropped.connect(_on_object_dropped)
|
snap_zone.has_dropped.connect(_on_object_dropped)
|
||||||
_refresh_progress_bar()
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
func _refresh_progress_bar() -> void:
|
func _play_animation(name: String) -> void:
|
||||||
|
# Replicated setters can fire before the node is in the tree (spawn payload),
|
||||||
|
# when the @onready children don't exist yet.
|
||||||
|
var player := get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||||
|
if player:
|
||||||
|
player.play(name)
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh_display() -> void:
|
||||||
|
# Guarded for the same reason as _play_animation.
|
||||||
|
if not progress_bar:
|
||||||
|
return
|
||||||
var progress := 0.0
|
var progress := 0.0
|
||||||
if cooking_result and cooking_result_time > 0:
|
if cooking_result and cooking_result_time > 0:
|
||||||
progress = clampf(float(time_cooked) / cooking_result_time, 0.0, 1.0)
|
progress = clampf(float(time_cooked) / cooking_result_time, 0.0, 1.0)
|
||||||
|
|
||||||
#print("time_cooked: %s / cooking_result_time: %s = progress: %s" % [time_cooked, cooking_result_time, progress])
|
|
||||||
progress_bar.set_progress(progress)
|
progress_bar.set_progress(progress)
|
||||||
progress_bar.set_bar_visible(not cooking_result.is_empty())
|
progress_bar.set_bar_visible(not cooking_result.is_empty())
|
||||||
progress_bar.override_fill_color(Color.RED if cooking_result == "charcoal" else Color.GREEN)
|
progress_bar.override_fill_color(Color.RED if cooking_result == "charcoal" else Color.GREEN)
|
||||||
@@ -44,25 +83,20 @@ func _on_object_picked_up(_item) -> void:
|
|||||||
cooking_result = result
|
cooking_result = result
|
||||||
cooking_result_time = RECIPE_MANAGER.get_cooking_time(_food_item.id)
|
cooking_result_time = RECIPE_MANAGER.get_cooking_time(_food_item.id)
|
||||||
print("Hob: set cooking_result ", cooking_result)
|
print("Hob: set cooking_result ", cooking_result)
|
||||||
_refresh_progress_bar()
|
# The setters above already refresh the display and start the flames.
|
||||||
$AnimationPlayer.play("hob")
|
|
||||||
|
|
||||||
|
|
||||||
func _on_object_dropped() -> void:
|
func _on_object_dropped() -> void:
|
||||||
print("Hob: object drop ")
|
print("Hob: object drop ")
|
||||||
$AnimationPlayer.play("RESET")
|
|
||||||
time_cooked = 0
|
time_cooked = 0
|
||||||
cooking_result = ""
|
cooking_result = ""
|
||||||
cooking_result_time = 0
|
cooking_result_time = 0
|
||||||
_refresh_progress_bar()
|
|
||||||
|
|
||||||
|
|
||||||
func convert_held_to_item(_item: String) -> void:
|
func convert_held_to_item(_item: String) -> void:
|
||||||
if not NetworkManager.owns_world():
|
if not NetworkManager.owns_world():
|
||||||
return
|
return
|
||||||
print("Hob converting ", _item)
|
print("Hob converting ", _item)
|
||||||
$AnimationPlayer.play("RESET")
|
|
||||||
|
|
||||||
|
|
||||||
var old_pickable = snap_zone.picked_up_object
|
var old_pickable = snap_zone.picked_up_object
|
||||||
if not old_pickable:
|
if not old_pickable:
|
||||||
@@ -86,7 +120,7 @@ func _process(delta: float) -> void:
|
|||||||
if cooking_result:
|
if cooking_result:
|
||||||
#print("Hob cooking_result: ", cooking_result)
|
#print("Hob cooking_result: ", cooking_result)
|
||||||
time_cooked += cook_speed * delta
|
time_cooked += cook_speed * delta
|
||||||
_refresh_progress_bar()
|
_refresh_display()
|
||||||
if time_cooked >= cooking_result_time:
|
if time_cooked >= cooking_result_time:
|
||||||
time_cooked = 0
|
time_cooked = 0
|
||||||
convert_held_to_item(cooking_result)
|
convert_held_to_item(cooking_result)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_jm0ik"]
|
[ext_resource type="Script" uid="uid://0y6wnjcsum6" path="res://Stations/item_dispenser.gd" id="1_jm0ik"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="2_tfo2i"]
|
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="2_tfo2i"]
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="3_lr065"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="3_lr065"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://mpapt5mkbuao" path="res://Prefabs/slide_off_dome.tscn" id="4_tfo2i"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
||||||
radius = 0.3
|
radius = 0.3
|
||||||
@@ -28,6 +29,9 @@ shape = SubResource("SphereShape3D_xmbo2")
|
|||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=831688237]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=831688237]
|
||||||
shape = SubResource("CylinderShape3D_24d3s")
|
shape = SubResource("CylinderShape3D_24d3s")
|
||||||
|
|
||||||
|
[node name="SlideOffDome" parent="." unique_id=1427609426 instance=ExtResource("4_tfo2i")]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.061363697, 0)
|
||||||
|
|
||||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=696108271]
|
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=696108271]
|
||||||
|
|
||||||
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1339763385]
|
[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1339763385]
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="2_mep2a"]
|
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="2_mep2a"]
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="3_3xuvi"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="3_3xuvi"]
|
||||||
[ext_resource type="Texture2D" uid="uid://cexxfyw03hr81" path="res://Textures/1.png" id="4_1f5le"]
|
[ext_resource type="Texture2D" uid="uid://cexxfyw03hr81" path="res://Textures/1.png" id="4_1f5le"]
|
||||||
|
[ext_resource type="PackedScene" uid="uid://mpapt5mkbuao" path="res://Prefabs/slide_off_dome.tscn" id="5_mep2a"]
|
||||||
|
|
||||||
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
[sub_resource type="SphereShape3D" id="SphereShape3D_xmbo2"]
|
||||||
radius = 0.3
|
radius = 0.3
|
||||||
@@ -29,6 +30,8 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=2105824734]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=2105824734]
|
||||||
shape = SubResource("SphereShape3D_xmbo2")
|
shape = SubResource("SphereShape3D_xmbo2")
|
||||||
|
|
||||||
|
[node name="SlideOffDome" parent="." unique_id=1427609426 instance=ExtResource("5_mep2a")]
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=378049928]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=378049928]
|
||||||
shape = SubResource("CylinderShape3D_24d3s")
|
shape = SubResource("CylinderShape3D_24d3s")
|
||||||
|
|
||||||
|
|||||||
+38
-12
@@ -6,10 +6,41 @@ const plate_wash_time: float = 3.0
|
|||||||
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
@onready var snap_zone: XRToolsSnapZone = $XRToolsSnapZone
|
||||||
@onready var progress_bar: ProgressBar3D = $ProgressBar3D
|
@onready var progress_bar: ProgressBar3D = $ProgressBar3D
|
||||||
|
|
||||||
var time_washed: float = 0.0
|
## Washing state. Both are replicated (see sink.tscn's Sync node) and refresh the
|
||||||
var is_washing: bool = false
|
## display when they change. Only the world owner runs this station's _process
|
||||||
|
## and snap zone, so without this a client never updates its bar — it stayed
|
||||||
|
## frozen on screen long after the plate came out clean.
|
||||||
|
var time_washed: float = 0.0: set = _set_time_washed
|
||||||
|
var is_washing: bool = false: set = _set_is_washing
|
||||||
var plate: PlateController = null
|
var plate: PlateController = null
|
||||||
|
|
||||||
|
|
||||||
|
func _set_time_washed(value: float) -> void:
|
||||||
|
if is_equal_approx(time_washed, value):
|
||||||
|
return
|
||||||
|
time_washed = value
|
||||||
|
_refresh_progress_bar()
|
||||||
|
|
||||||
|
|
||||||
|
func _set_is_washing(value: bool) -> void:
|
||||||
|
if is_washing == value:
|
||||||
|
return
|
||||||
|
is_washing = value
|
||||||
|
_refresh_progress_bar()
|
||||||
|
_set_effects_playing(is_washing)
|
||||||
|
|
||||||
|
|
||||||
|
# Water, bubbles and the animation follow the washing state on every peer, not
|
||||||
|
# just the one running the logic.
|
||||||
|
func _set_effects_playing(playing: bool) -> void:
|
||||||
|
var player := get_node_or_null("AnimationPlayer") as AnimationPlayer
|
||||||
|
if player:
|
||||||
|
player.play("working" if playing else "RESET")
|
||||||
|
for effect in ["water", "bubbles", "GPUParticles3D"]:
|
||||||
|
var node := get_node_or_null(effect) as Node3D
|
||||||
|
if node:
|
||||||
|
node.visible = playing
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
if not progress_bar or progress_bar is not ProgressBar3D:
|
if not progress_bar or progress_bar is not ProgressBar3D:
|
||||||
push_error("Sink missing reference to progressbar, or wrong type:", progress_bar)
|
push_error("Sink missing reference to progressbar, or wrong type:", progress_bar)
|
||||||
@@ -21,6 +52,8 @@ func _ready() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _refresh_progress_bar() -> void:
|
func _refresh_progress_bar() -> void:
|
||||||
|
if not progress_bar:
|
||||||
|
return
|
||||||
progress_bar.set_progress(clampf(float(time_washed) / plate_wash_time, 0.0, 1.0))
|
progress_bar.set_progress(clampf(float(time_washed) / plate_wash_time, 0.0, 1.0))
|
||||||
progress_bar.set_bar_visible(is_washing)
|
progress_bar.set_bar_visible(is_washing)
|
||||||
|
|
||||||
@@ -29,12 +62,8 @@ func _on_object_picked_up(_item) -> void:
|
|||||||
print("Sink: object picked up: ", _item)
|
print("Sink: object picked up: ", _item)
|
||||||
plate = _item.get_node_or_null("PlateController") as PlateController
|
plate = _item.get_node_or_null("PlateController") as PlateController
|
||||||
if plate:
|
if plate:
|
||||||
|
# The setter starts the effects and shows the bar, here and on clients.
|
||||||
is_washing = plate.is_dirty
|
is_washing = plate.is_dirty
|
||||||
if is_washing:
|
|
||||||
$AnimationPlayer.play("working")
|
|
||||||
$bubbles.show()
|
|
||||||
$water.show()
|
|
||||||
$GPUParticles3D.show()
|
|
||||||
|
|
||||||
|
|
||||||
func _on_object_dropped() -> void:
|
func _on_object_dropped() -> void:
|
||||||
@@ -46,13 +75,10 @@ func _reset_sink():
|
|||||||
time_washed = 0
|
time_washed = 0
|
||||||
is_washing = false
|
is_washing = false
|
||||||
plate = null
|
plate = null
|
||||||
$AnimationPlayer.play("RESET")
|
|
||||||
$water.hide()
|
|
||||||
$bubbles.hide()
|
|
||||||
$GPUParticles3D.hide()
|
|
||||||
_refresh_progress_bar()
|
|
||||||
|
|
||||||
func complete_washing():
|
func complete_washing():
|
||||||
|
if not plate:
|
||||||
|
return
|
||||||
plate.is_dirty = false
|
plate.is_dirty = false
|
||||||
_reset_sink()
|
_reset_sink()
|
||||||
print("Sink washing complete!")
|
print("Sink washing complete!")
|
||||||
|
|||||||
+1
-9
@@ -15,11 +15,6 @@ stereo = true
|
|||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_ai6d4"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_ai6d4"]
|
||||||
size = Vector3(0.5, 0.128, 0.5)
|
size = Vector3(0.5, 0.128, 0.5)
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_sink"]
|
|
||||||
properties/0/path = NodePath(".:progress")
|
|
||||||
properties/0/spawn = true
|
|
||||||
properties/0/replication_mode = 1
|
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0gbf8"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0gbf8"]
|
||||||
|
|
||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bvwq1"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_bvwq1"]
|
||||||
@@ -146,7 +141,7 @@ tracks/5/path = NodePath("AnimationPlayer/AudioStreamPlayer3D:playing")
|
|||||||
tracks/5/interp = 1
|
tracks/5/interp = 1
|
||||||
tracks/5/loop_wrap = true
|
tracks/5/loop_wrap = true
|
||||||
tracks/5/keys = {
|
tracks/5/keys = {
|
||||||
"times": PackedFloat32Array(0, 1, 1.7),
|
"times": PackedFloat32Array(0, 0.8494, 1.7),
|
||||||
"transitions": PackedFloat32Array(1, 1, 1),
|
"transitions": PackedFloat32Array(1, 1, 1),
|
||||||
"update": 1,
|
"update": 1,
|
||||||
"values": [false, true, false]
|
"values": [false, true, false]
|
||||||
@@ -231,9 +226,6 @@ transform = Transform3D(1, 0, 0, 0, 0.9367828, -0.34991136, 0, 0.34991136, 0.936
|
|||||||
operation = 2
|
operation = 2
|
||||||
size = Vector3(1, 1.2053223, 0.352417)
|
size = Vector3(1, 1.2053223, 0.352417)
|
||||||
|
|
||||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=1601242574]
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_sink")
|
|
||||||
|
|
||||||
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("4_1pinr")]
|
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("4_1pinr")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8164611, -0.035980098)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.8164611, -0.035980098)
|
||||||
|
|
||||||
|
|||||||
+104
-56
@@ -3,17 +3,19 @@ extends StaticBody3D
|
|||||||
|
|
||||||
const GAME_MANAGER = preload("res://GameManager.gd")
|
const GAME_MANAGER = preload("res://GameManager.gd")
|
||||||
|
|
||||||
@export var thinking_duration: float = 6
|
@export var thinking_duration: float = 3.0
|
||||||
@export var primary_duration: float = 23
|
@export var primary_duration: float = 60.0
|
||||||
@export var friend_duration: float = 10
|
@export var friend_duration: float = 10.0
|
||||||
@export var eating_duration: float = 4
|
@export var eating_duration: float = 4.0
|
||||||
|
@export var money_sound: AudioStream
|
||||||
|
|
||||||
@onready var label_3d: Label3D = $Label3D
|
@onready var label_3d: Label3D = $Label3D
|
||||||
@onready var label_3d_time: Label3D = $Label3DTime
|
@onready var label_3d_time: Label3D = $Label3DTime
|
||||||
@onready var snap_zones: Array[XRToolsSnapZone] = []
|
@onready var snap_zones: Array[XRToolsSnapZone] = []
|
||||||
@onready var progress_bar: ProgressBar3D = $ProgressBar3D
|
@onready var progress_bar: ProgressBar3D = $ProgressBar3D
|
||||||
|
@onready var audio_player: AudioStreamPlayer3D = $AudioStreamPlayer3D
|
||||||
|
|
||||||
const lbl_thinking: String = "..."
|
const lbl_thinking: String = "Thinking"
|
||||||
const lbl_gameover: String = "Game Over!"
|
const lbl_gameover: String = "Game Over!"
|
||||||
const lbl_eating: String = "Eating..."
|
const lbl_eating: String = "Eating..."
|
||||||
|
|
||||||
@@ -27,16 +29,21 @@ enum TableState {
|
|||||||
EATING
|
EATING
|
||||||
}
|
}
|
||||||
|
|
||||||
## State is server-authoritative and synced (see table.tscn's Sync node);
|
## State is server-authoritative and replicated. Transitions only ever run where
|
||||||
## state transitions only ever run where NetworkManager.owns_world() is true
|
## NetworkManager.owns_world() is true — the whole station's _process is gated
|
||||||
## (the whole station's _process is gated off elsewhere for non-owners, see
|
## off for non-owners (NetWorld._gate) — and the setters below just refresh the
|
||||||
## NetworkManager._gate_station). The setters below just refresh the display,
|
## display, so the server (via _setState) and clients (via incoming sync) show
|
||||||
## so both the server (via _setState) and clients (via incoming sync) show
|
## the same thing. Use _setState(), never assign `state` directly.
|
||||||
## the same text. Use _setState(), never assign _state directly.
|
##
|
||||||
var _state: TableState = TableState.EMPTY: set = _set_state
|
## These are deliberately not underscore-prefixed: that prefix is what marks a
|
||||||
var _state_time: float = 0.0: set = _set_state_time
|
## variable as private and unreplicated (see NetReplication), and this is exactly
|
||||||
var _state_duration: float = 0.0 # set in _set_state_time
|
## the state that has to reach every peer. state_duration is replicated for the
|
||||||
var _unsatisfied_orders: Array[String] = []: set = _set_unsatisfied_orders
|
## same reason — the progress bar divides by it, and on a client that never ran a
|
||||||
|
## transition it would otherwise still be zero.
|
||||||
|
var state: TableState = TableState.EMPTY: set = _set_state
|
||||||
|
var state_time: float = 0.0: set = _set_state_time
|
||||||
|
var state_duration: float = 0.0 # set in _setState alongside state_time
|
||||||
|
var unsatisfied_orders: Array[String] = []: set = _set_unsatisfied_orders
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -45,9 +52,14 @@ func _ready() -> void:
|
|||||||
if not label_3d_time:
|
if not label_3d_time:
|
||||||
push_error("Table is missing reference to Label3DTime")
|
push_error("Table is missing reference to Label3DTime")
|
||||||
if not progress_bar or progress_bar is not ProgressBar3D:
|
if not progress_bar or progress_bar is not ProgressBar3D:
|
||||||
push_error("Hob missing reference to progressbar, or wrong type:", progress_bar)
|
push_error("Table missing reference to progressbar, or wrong type:", progress_bar)
|
||||||
progress_bar.y_billboard = true
|
progress_bar.y_billboard = true
|
||||||
|
# Render whatever state already arrived from the server before we were in
|
||||||
|
# the tree (see the guard in _refresh_display).
|
||||||
|
_refresh_display.call_deferred()
|
||||||
|
if not audio_player:
|
||||||
|
push_error("Table is missing reference to AudioStreamPlayer3D")
|
||||||
|
|
||||||
# Get snap_zones for plates, store in array snap_zones
|
# Get snap_zones for plates, store in array snap_zones
|
||||||
for child in get_children():
|
for child in get_children():
|
||||||
var snap_zone_node = child as XRToolsSnapZone
|
var snap_zone_node = child as XRToolsSnapZone
|
||||||
@@ -61,11 +73,14 @@ func _ready() -> void:
|
|||||||
for snap_zone_node in snap_zones:
|
for snap_zone_node in snap_zones:
|
||||||
snap_zone_node.has_picked_up.connect(_on_object_picked_up)
|
snap_zone_node.has_picked_up.connect(_on_object_picked_up)
|
||||||
snap_zone_node.has_dropped.connect(_on_object_dropped)
|
snap_zone_node.has_dropped.connect(_on_object_dropped)
|
||||||
|
set_process(true)
|
||||||
_setState(TableState.EMPTY)
|
_setState(TableState.EMPTY)
|
||||||
|
|
||||||
|
|
||||||
func _on_object_picked_up(_item) -> void:
|
func _on_object_picked_up(_item) -> void:
|
||||||
print("Table: object picked up: ", _item)
|
print("Table: object picked up: ", _item)
|
||||||
|
if state == TableState.EATING:
|
||||||
|
return
|
||||||
_absorb_item_if_correct(_item)
|
_absorb_item_if_correct(_item)
|
||||||
|
|
||||||
|
|
||||||
@@ -88,26 +103,40 @@ func _absorb_item_if_correct(_item: Node) -> void:
|
|||||||
# Absorm items from the plate we want
|
# Absorm items from the plate we want
|
||||||
for food_item in plate.container.contained_items:
|
for food_item in plate.container.contained_items:
|
||||||
print("Table: held a plate with FoodItem: ", food_item.id)
|
print("Table: held a plate with FoodItem: ", food_item.id)
|
||||||
if food_item.id in _unsatisfied_orders:
|
if food_item.id in unsatisfied_orders:
|
||||||
print("Table: held FoodItem is in unsatisfied orders, removing it")
|
print("Table: held FoodItem is in unsatisfied orders, removing it")
|
||||||
_unsatisfied_orders.erase(food_item.id)
|
# Reassign rather than mutate in place. This property has a setter that
|
||||||
GAME_MANAGER.money += food_item.sell_value
|
# refreshes the label, and mutating an array never fires it — so the
|
||||||
_setState(TableState.WAITING_FRIEND)
|
# peer that actually served the food would be the one peer whose table
|
||||||
|
# still showed the order outstanding. duplicate() also preserves the
|
||||||
|
# Array[String] typing the property requires.
|
||||||
|
var remaining := unsatisfied_orders.duplicate()
|
||||||
|
remaining.erase(food_item.id)
|
||||||
|
unsatisfied_orders = remaining
|
||||||
|
if unsatisfied_orders.size() > 0 and state != TableState.EATING:
|
||||||
|
_setState(TableState.WAITING_FRIEND)
|
||||||
|
|
||||||
# Table has everything it wants. Start eating
|
# Table has everything it wants. Start eating
|
||||||
if _unsatisfied_orders.is_empty():
|
elif unsatisfied_orders.is_empty() and state != TableState.EATING:
|
||||||
_setState(TableState.EATING)
|
GAME_MANAGER.money += food_item.sell_value
|
||||||
|
audio_player.stream = money_sound
|
||||||
|
audio_player.play()
|
||||||
|
_setState(TableState.EATING)
|
||||||
|
|
||||||
|
|
||||||
func place_order() -> void:
|
func place_order() -> void:
|
||||||
print("Table: place_order()")
|
print("Table: place_order()")
|
||||||
_unsatisfied_orders.append(GAME_MANAGER.get_random_meal())
|
var new_orders = unsatisfied_orders.duplicate()
|
||||||
_unsatisfied_orders.append(GAME_MANAGER.get_random_meal())
|
new_orders.append(GAME_MANAGER.get_random_meal())
|
||||||
|
new_orders.append(GAME_MANAGER.get_random_meal())
|
||||||
|
_set_unsatisfied_orders(new_orders)
|
||||||
|
|
||||||
|
|
||||||
func satisfyAllOrders() -> void:
|
func satisfyAllOrders() -> void:
|
||||||
print("Table: satisfyAllOrders()")
|
print("Table: satisfyAllOrders()")
|
||||||
_unsatisfied_orders.clear()
|
# Assigned, not cleared in place, for the same reason as in
|
||||||
|
# _absorb_item_if_correct: clear() would not fire the setter.
|
||||||
|
unsatisfied_orders = []
|
||||||
clearAllPlates()
|
clearAllPlates()
|
||||||
_setState(TableState.EMPTY)
|
_setState(TableState.EMPTY)
|
||||||
|
|
||||||
@@ -124,35 +153,49 @@ func clearAllPlates() -> void:
|
|||||||
plate.container.clear()
|
plate.container.clear()
|
||||||
plate.is_dirty = true
|
plate.is_dirty = true
|
||||||
|
|
||||||
|
|
||||||
|
func _set_snap_zones_enabled(value: bool) -> void:
|
||||||
|
for zone in snap_zones:
|
||||||
|
zone.enabled = value
|
||||||
|
|
||||||
|
|
||||||
## Server-only state transition: sets the new state's timer and assigns
|
## Server-only state transition: sets the new state's timer and assigns
|
||||||
## _state (whose setter refreshes the display on every peer).
|
## state (whose setter refreshes the display on every peer).
|
||||||
func _setState(newState: TableState) -> void:
|
func _setState(newState: TableState) -> void:
|
||||||
print("Table set _state: ", TableState.keys()[newState])
|
print("Table set state: ", TableState.keys()[newState])
|
||||||
|
|
||||||
match newState:
|
match newState:
|
||||||
TableState.EMPTY:
|
TableState.EMPTY:
|
||||||
_state_time = 0.0
|
state_time = 5.0
|
||||||
_state_duration = 0.0
|
state_duration = 5.0
|
||||||
TableState.THINKING:
|
TableState.THINKING:
|
||||||
_state_time = eating_duration
|
state_time = thinking_duration
|
||||||
_state_duration = eating_duration # Can't be done in _set_state(), will set duration inside timer
|
state_duration = thinking_duration # Can't be done in _set_state(), will set duration inside timer
|
||||||
TableState.WAITING_PRIMARY:
|
TableState.WAITING_PRIMARY:
|
||||||
_state_time = primary_duration
|
state_time = primary_duration
|
||||||
_state_duration = primary_duration
|
state_duration = primary_duration
|
||||||
TableState.WAITING_FRIEND:
|
TableState.WAITING_FRIEND:
|
||||||
_state_time = friend_duration
|
state_time = friend_duration
|
||||||
_state_duration = friend_duration
|
state_duration = friend_duration
|
||||||
TableState.EATING:
|
TableState.EATING:
|
||||||
_state_time = eating_duration
|
state_time = eating_duration
|
||||||
_state_duration = eating_duration
|
state_duration = eating_duration
|
||||||
|
_set_snap_zones_enabled(false)
|
||||||
|
|
||||||
_state = newState
|
state = newState
|
||||||
|
|
||||||
|
|
||||||
## Pure presentation, driven off the current (locally authoritative or
|
## Pure presentation, driven off the current (locally authoritative or
|
||||||
## synced-from-server) state. Runs on every peer.
|
## synced-from-server) state. Runs on every peer.
|
||||||
func _refresh_display() -> void:
|
func _refresh_display() -> void:
|
||||||
match _state:
|
# state, state_time and unsatisfied_orders are replicated with spawn=true,
|
||||||
|
# and MultiplayerSpawner applies a spawn payload BEFORE the node enters the
|
||||||
|
# tree — so these setters fire while the @onready children below are still
|
||||||
|
# null. Bail out until _ready() has resolved them; _ready() calls back in
|
||||||
|
# once it has, so nothing that arrived early is lost.
|
||||||
|
if not progress_bar or not label_3d or not label_3d_time:
|
||||||
|
return
|
||||||
|
match state:
|
||||||
TableState.EMPTY:
|
TableState.EMPTY:
|
||||||
progress_bar.set_bar_visible(false)
|
progress_bar.set_bar_visible(false)
|
||||||
label_3d.text = "empty"
|
label_3d.text = "empty"
|
||||||
@@ -160,64 +203,69 @@ func _refresh_display() -> void:
|
|||||||
progress_bar.set_bar_visible(false)
|
progress_bar.set_bar_visible(false)
|
||||||
label_3d.text = lbl_thinking
|
label_3d.text = lbl_thinking
|
||||||
TableState.WAITING_PRIMARY:
|
TableState.WAITING_PRIMARY:
|
||||||
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)]
|
label_3d.text = "%s\n%s" % [TableState.keys()[state], "\n".join(unsatisfied_orders)]
|
||||||
progress_bar.set_bar_visible(true)
|
progress_bar.set_bar_visible(true)
|
||||||
progress_bar.override_fill_color(Color.RED if _state_time < 10 else Color.YELLOW)
|
progress_bar.override_fill_color(Color.RED if state_time < 10 else Color.YELLOW)
|
||||||
TableState.WAITING_FRIEND:
|
TableState.WAITING_FRIEND:
|
||||||
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)]
|
label_3d.text = "%s\n%s" % [TableState.keys()[state], "\n".join(unsatisfied_orders)]
|
||||||
progress_bar.set_bar_visible(true)
|
progress_bar.set_bar_visible(true)
|
||||||
progress_bar.override_fill_color(Color.RED if _state_time < 5 else Color.YELLOW)
|
progress_bar.override_fill_color(Color.RED if state_time < 5 else Color.YELLOW)
|
||||||
TableState.EATING:
|
TableState.EATING:
|
||||||
label_3d.text = lbl_eating
|
label_3d.text = lbl_eating
|
||||||
progress_bar.set_bar_visible(false)
|
progress_bar.set_bar_visible(false)
|
||||||
|
|
||||||
label_3d_time.text = "%.1f" % _state_time
|
label_3d_time.text = "%.1f" % state_time
|
||||||
progress_bar.set_progress(clampf(float(_state_time) / _state_duration, 0.0, 1.0))
|
var progress := state_time / state_duration if state_duration > 0.0 else 0.0
|
||||||
|
progress_bar.set_progress(clampf(progress, 0.0, 1.0))
|
||||||
|
|
||||||
|
|
||||||
func _set_state(value: TableState) -> void:
|
func _set_state(value: TableState) -> void:
|
||||||
_state = value
|
state = value
|
||||||
_refresh_display()
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
func _set_state_time(value: float) -> void:
|
func _set_state_time(value: float) -> void:
|
||||||
_state_time = value
|
state_time = value
|
||||||
_refresh_display()
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
func _set_unsatisfied_orders(value: Array[String]) -> void:
|
func _set_unsatisfied_orders(value: Array[String]) -> void:
|
||||||
_unsatisfied_orders = value
|
unsatisfied_orders = value
|
||||||
_refresh_display()
|
_refresh_display()
|
||||||
|
|
||||||
|
|
||||||
func _process(delta: float) -> void:
|
func _process(delta: float) -> void:
|
||||||
# Wait for state to finish
|
# Wait for state to finish
|
||||||
if _state_time > 0:
|
if state_time > 0.0:
|
||||||
_state_time -= delta
|
state_time = max(0.0, state_time - delta)
|
||||||
return
|
return
|
||||||
|
|
||||||
if GAME_MANAGER.game_state == GAME_MANAGER.GameState.GAME_OVER:
|
if GAME_MANAGER.game_state == GAME_MANAGER.GameState.GAME_OVER:
|
||||||
return
|
return
|
||||||
|
|
||||||
# When state timer is finished, do this stuff before moving to next state
|
# When state timer is finished, do this stuff before moving to next state
|
||||||
match _state:
|
match state:
|
||||||
TableState.EMPTY:
|
TableState.EMPTY:
|
||||||
|
print("Table State EMPTY finish")
|
||||||
_setState(TableState.THINKING)
|
_setState(TableState.THINKING)
|
||||||
|
|
||||||
TableState.THINKING:
|
TableState.THINKING:
|
||||||
|
print("Table State THINKING finish")
|
||||||
place_order()
|
place_order()
|
||||||
_setState(TableState.WAITING_PRIMARY)
|
_setState(TableState.WAITING_PRIMARY)
|
||||||
|
|
||||||
|
|
||||||
TableState.WAITING_PRIMARY:
|
TableState.WAITING_PRIMARY:
|
||||||
|
print("Table State WAITING_PRIMARY finish")
|
||||||
label_3d.text = lbl_gameover
|
label_3d.text = lbl_gameover
|
||||||
GAME_MANAGER.game_over()
|
GAME_MANAGER.game_over()
|
||||||
|
|
||||||
TableState.WAITING_FRIEND:
|
TableState.WAITING_FRIEND:
|
||||||
|
print("Table State WAITING_FRIEND finish")
|
||||||
label_3d.text = lbl_gameover
|
label_3d.text = lbl_gameover
|
||||||
GAME_MANAGER.game_over()
|
GAME_MANAGER.game_over()
|
||||||
|
|
||||||
|
|
||||||
TableState.EATING:
|
TableState.EATING:
|
||||||
|
print("Table State EATING finish")
|
||||||
clearAllPlates()
|
clearAllPlates()
|
||||||
|
_set_snap_zones_enabled(true)
|
||||||
_setState(TableState.EMPTY)
|
_setState(TableState.EMPTY)
|
||||||
|
|||||||
+24
-15
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
[ext_resource type="Script" uid="uid://caeikc7e3igkd" path="res://Stations/table.gd" id="1_2vcpj"]
|
[ext_resource type="Script" uid="uid://caeikc7e3igkd" path="res://Stations/table.gd" id="1_2vcpj"]
|
||||||
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_8j1nt"]
|
[ext_resource type="Script" uid="uid://cquqe4f1m1sw6" path="res://addons/godot-xr-tools/objects/snap_zone.gd" id="1_8j1nt"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://clkjwcawdqvta" path="res://Sounds/money.mp3" id="2_jslhm"]
|
||||||
|
[ext_resource type="AudioStream" uid="uid://daiv8ubwdbidf" path="res://Sounds/220195__gameaudio__click-wooden-1.wav" id="3_0s6ir"]
|
||||||
[ext_resource type="PackedScene" uid="uid://bxbocxdaayvwx" path="res://UI/progress_bar.tscn" id="3_kjf1i"]
|
[ext_resource type="PackedScene" uid="uid://bxbocxdaayvwx" path="res://UI/progress_bar.tscn" id="3_kjf1i"]
|
||||||
|
|
||||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||||
@@ -13,19 +15,9 @@ radius = 0.6
|
|||||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
||||||
albedo_color = Color(0.31, 0.21576, 0.1333, 1)
|
albedo_color = Color(0.31, 0.21576, 0.1333, 1)
|
||||||
|
|
||||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_table"]
|
|
||||||
properties/0/path = NodePath(".:_state")
|
|
||||||
properties/0/spawn = true
|
|
||||||
properties/0/replication_mode = 1
|
|
||||||
properties/1/path = NodePath(".:_state_time")
|
|
||||||
properties/1/spawn = true
|
|
||||||
properties/1/replication_mode = 1
|
|
||||||
properties/2/path = NodePath(".:_unsatisfied_orders")
|
|
||||||
properties/2/spawn = true
|
|
||||||
properties/2/replication_mode = 1
|
|
||||||
|
|
||||||
[node name="Table" type="StaticBody3D" unique_id=1863572470 groups=["station"]]
|
[node name="Table" type="StaticBody3D" unique_id=1863572470 groups=["station"]]
|
||||||
script = ExtResource("1_2vcpj")
|
script = ExtResource("1_2vcpj")
|
||||||
|
money_sound = ExtResource("2_jslhm")
|
||||||
|
|
||||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=228053941]
|
[node name="CollisionShape3D" type="CollisionShape3D" parent="." unique_id=228053941]
|
||||||
shape = SubResource("BoxShape3D_24d3s")
|
shape = SubResource("BoxShape3D_24d3s")
|
||||||
@@ -35,6 +27,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.31874347, 0.5270121, 0)
|
|||||||
collision_layer = 65536
|
collision_layer = 65536
|
||||||
collision_mask = 65540
|
collision_mask = 65540
|
||||||
script = ExtResource("1_8j1nt")
|
script = ExtResource("1_8j1nt")
|
||||||
|
stash_sound = ExtResource("3_0s6ir")
|
||||||
snap_mode = 1
|
snap_mode = 1
|
||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
@@ -42,11 +35,15 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||||
shape = SubResource("SphereShape3D_dlkho")
|
shape = SubResource("SphereShape3D_dlkho")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="XRToolsSnapZone" unique_id=2135860710]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.013531357, -0.015141487, 0.012176305)
|
||||||
|
|
||||||
[node name="XRToolsSnapZone2" type="Area3D" parent="." unique_id=694448387 groups=["station"]]
|
[node name="XRToolsSnapZone2" type="Area3D" parent="." unique_id=694448387 groups=["station"]]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.34755045, 0.5270121, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.34755045, 0.5270121, 0)
|
||||||
collision_layer = 65536
|
collision_layer = 65536
|
||||||
collision_mask = 65540
|
collision_mask = 65540
|
||||||
script = ExtResource("1_8j1nt")
|
script = ExtResource("1_8j1nt")
|
||||||
|
stash_sound = ExtResource("3_0s6ir")
|
||||||
snap_mode = 1
|
snap_mode = 1
|
||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
@@ -54,11 +51,15 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||||
shape = SubResource("SphereShape3D_dlkho")
|
shape = SubResource("SphereShape3D_dlkho")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="XRToolsSnapZone2" unique_id=709529410]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.013531357, -0.015141487, 0.012176305)
|
||||||
|
|
||||||
[node name="XRToolsSnapZone3" type="Area3D" parent="." unique_id=1734026785 groups=["station"]]
|
[node name="XRToolsSnapZone3" type="Area3D" parent="." unique_id=1734026785 groups=["station"]]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.012355864, 0.5270121, 0.33038777)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.012355864, 0.5270121, 0.33038777)
|
||||||
collision_layer = 65536
|
collision_layer = 65536
|
||||||
collision_mask = 65540
|
collision_mask = 65540
|
||||||
script = ExtResource("1_8j1nt")
|
script = ExtResource("1_8j1nt")
|
||||||
|
stash_sound = ExtResource("3_0s6ir")
|
||||||
snap_mode = 1
|
snap_mode = 1
|
||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
@@ -66,11 +67,15 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||||
shape = SubResource("SphereShape3D_dlkho")
|
shape = SubResource("SphereShape3D_dlkho")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="XRToolsSnapZone3" unique_id=2000860662]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.013531357, -0.015141487, 0.012176305)
|
||||||
|
|
||||||
[node name="XRToolsSnapZone4" type="Area3D" parent="." unique_id=1358457948 groups=["station"]]
|
[node name="XRToolsSnapZone4" type="Area3D" parent="." unique_id=1358457948 groups=["station"]]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.022546828, 0.5270121, -0.36528283)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.022546828, 0.5270121, -0.36528283)
|
||||||
collision_layer = 65536
|
collision_layer = 65536
|
||||||
collision_mask = 65540
|
collision_mask = 65540
|
||||||
script = ExtResource("1_8j1nt")
|
script = ExtResource("1_8j1nt")
|
||||||
|
stash_sound = ExtResource("3_0s6ir")
|
||||||
snap_mode = 1
|
snap_mode = 1
|
||||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||||
|
|
||||||
@@ -78,11 +83,15 @@ metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
|||||||
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
transform = Transform3D(0.5, 0, 0, 0, 0.5, 0, 0, 0, 0.5, 0, -0.000975132, 0)
|
||||||
shape = SubResource("SphereShape3D_dlkho")
|
shape = SubResource("SphereShape3D_dlkho")
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="XRToolsSnapZone4" unique_id=790672192]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.013531357, -0.015141487, 0.012176305)
|
||||||
|
|
||||||
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=2085635675]
|
[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=2085635675]
|
||||||
|
use_collision = true
|
||||||
|
|
||||||
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=411048765]
|
[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=411048765]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.46137518, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.46137518, 0)
|
||||||
size = Vector3(0.6, 0.077, 0.6)
|
size = Vector3(1, 0.077, 1)
|
||||||
material = SubResource("StandardMaterial3D_24d3s")
|
material = SubResource("StandardMaterial3D_24d3s")
|
||||||
|
|
||||||
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2093582402]
|
[node name="CSGBox3D2" type="CSGBox3D" parent="CSGCombiner3D" unique_id=2093582402]
|
||||||
@@ -169,9 +178,9 @@ pixel_size = 0.003
|
|||||||
billboard = 2
|
billboard = 2
|
||||||
text = "20.1s"
|
text = "20.1s"
|
||||||
|
|
||||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=2000411505]
|
|
||||||
replication_config = SubResource("SceneReplicationConfig_np_table")
|
|
||||||
|
|
||||||
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("3_kjf1i")]
|
[node name="ProgressBar3D" parent="." unique_id=654673176 instance=ExtResource("3_kjf1i")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.3676775, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.3676775, 0)
|
||||||
y_billboard = true
|
y_billboard = true
|
||||||
|
|
||||||
|
[node name="AudioStreamPlayer3D" type="AudioStreamPlayer3D" parent="." unique_id=724610642]
|
||||||
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.2935008, 0)
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://d1djvskv4n4m3"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0, 0, 0, 1)
|
||||||
|
emission_energy_multiplier = 0.0
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_resource type="StandardMaterial3D" format=3 uid="uid://db7c7w4apwti7"]
|
||||||
|
|
||||||
|
[resource]
|
||||||
|
albedo_color = Color(0.6553854, 0, 0, 1)
|
||||||
|
emission_enabled = true
|
||||||
|
emission = Color(1, 0.27450982, 0, 1)
|
||||||
@@ -96,7 +96,7 @@ func _on_john_pressed() -> void:
|
|||||||
|
|
||||||
func _on_host_pressed() -> void:
|
func _on_host_pressed() -> void:
|
||||||
NetworkManager.request_host()
|
NetworkManager.request_host()
|
||||||
get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE)
|
get_tree().change_scene_to_file.call_deferred("res://Scenes/JonScene.tscn")
|
||||||
|
|
||||||
|
|
||||||
func _on_join_pressed() -> void:
|
func _on_join_pressed() -> void:
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ func set_bar_visible(value: bool) -> void:
|
|||||||
progress_bar.visible = value
|
progress_bar.visible = value
|
||||||
|
|
||||||
|
|
||||||
|
func is_bar_visible() -> bool:
|
||||||
|
return progress_bar != null and progress_bar.visible
|
||||||
|
|
||||||
|
|
||||||
func override_fill_color(color: Color) -> void:
|
func override_fill_color(color: Color) -> void:
|
||||||
var style_box := StyleBoxFlat.new()
|
var style_box := StyleBoxFlat.new()
|
||||||
style_box.bg_color = color
|
style_box.bg_color = color
|
||||||
|
|||||||
@@ -48,4 +48,5 @@ show_laser = 2
|
|||||||
show_target = true
|
show_target = true
|
||||||
|
|
||||||
[node name="PlayerBody" parent="." unique_id=1444632058 instance=ExtResource("4_6xr3x")]
|
[node name="PlayerBody" parent="." unique_id=1444632058 instance=ExtResource("4_6xr3x")]
|
||||||
|
collision_mask = 1017
|
||||||
player_calibrate_height = false
|
player_calibrate_height = false
|
||||||
|
|||||||
@@ -437,7 +437,8 @@ func _pick_up_object(target: Node3D) -> void:
|
|||||||
# Pick up our target. Note, target may do instant drop_and_free
|
# Pick up our target. Note, target may do instant drop_and_free
|
||||||
picked_up_ranged = not _object_in_grab_area.has(target)
|
picked_up_ranged = not _object_in_grab_area.has(target)
|
||||||
picked_up_object = target
|
picked_up_object = target
|
||||||
target.pick_up(self)
|
if target and target.has_method("pick_up"):
|
||||||
|
target.pick_up(self)
|
||||||
|
|
||||||
# If object picked up then emit signal
|
# If object picked up then emit signal
|
||||||
if is_instance_valid(picked_up_object):
|
if is_instance_valid(picked_up_object):
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# High-level replication rebuild
|
||||||
|
|
||||||
|
Rebuild VRyHungry's multiplayer on Godot's stock `MultiplayerSpawner` and
|
||||||
|
`MultiplayerSynchronizer`, so that adding a new object to the game requires no
|
||||||
|
networking code at all. Replaces the bespoke spawn payloads, per-scene
|
||||||
|
replication configs, and grab-authority handshake in place today.
|
||||||
|
|
||||||
|
Baseline being replaced: `logs/mptest_report.txt`, 146/146 checks passing as of
|
||||||
|
2026-07-28 19:47. That is the bar the rebuild has to clear.
|
||||||
|
|
||||||
|
## The constraint that shapes everything
|
||||||
|
|
||||||
|
`MultiplayerSpawner` replicates **node creation, name, and deletion** — nothing
|
||||||
|
more. All state comes from `MultiplayerSynchronizer` plus a
|
||||||
|
`SceneReplicationConfig`. There is no engine switch for "sync everything
|
||||||
|
regardless of what it is".
|
||||||
|
|
||||||
|
The way to get there anyway: a `SceneReplicationConfig` is a plain `Resource`,
|
||||||
|
so it can be **built in code from a convention** and attached at runtime. One
|
||||||
|
helper then covers every object in the game, and nothing has to be authored
|
||||||
|
per scene.
|
||||||
|
|
||||||
|
## Verified engine behaviour
|
||||||
|
|
||||||
|
Established by `test/spike/`, run against Godot 4.7.stable. These are load
|
||||||
|
bearing — the design below is only correct because these hold.
|
||||||
|
|
||||||
|
| # | Question | Result |
|
||||||
|
|---|----------|--------|
|
||||||
|
| Q1 | Does default auto-spawn carry the spawn properties of a synchronizer attached at **runtime**, not baked into the `.tscn`? | **Yes.** A peer joining 4s after the spawn received `pos=(5,6,7) health=42 label=hello`. |
|
||||||
|
| Q2 | Do `ON_CHANGE` properties reach a late joiner, and do later changes propagate? | **Yes**, both. |
|
||||||
|
| Q3 | Can `set_visibility_for(peer, false)` suppress one peer's updates without despawning the node there? | **No.** The node is despawned and respawned on every visibility flip. Unusable — it would make a held object vanish from the holder's hand. |
|
||||||
|
| Q4 | Can handing one of two synchronizers to a client let it drive the transform locally while server-owned state keeps arriving? | **Yes.** Client showed its own `(1,1,1)` and ignored the server's competing `(-9,-9,-9)`; the server saw `(1,1,1)`; server-owned `health=123` still arrived. |
|
||||||
|
|
||||||
|
Two traps worth recording. `set_visibility_for` is only an *override* on top of
|
||||||
|
`public_visibility`, which defaults to `true` — calling it alone does nothing.
|
||||||
|
And the spike's first version chained relative timers, which drifted enough that
|
||||||
|
the client's observations straddled the server's actions; it now runs both peers
|
||||||
|
on one absolute clock.
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
### Layer 1 — `Net/net_replication.gd`
|
||||||
|
|
||||||
|
The convention-based config builder. `NetReplication.attach(node)` gives a node
|
||||||
|
two generated synchronizers:
|
||||||
|
|
||||||
|
- **`NetSync`** — script state. Always server-owned; gameplay outcomes are the
|
||||||
|
server's to decide. `ON_CHANGE`.
|
||||||
|
- **`NetXform`** — `position` + `quaternion`. Server-owned while the object is
|
||||||
|
loose, handed to a peer for the duration of a hold. `ALWAYS`.
|
||||||
|
|
||||||
|
What gets replicated, by convention:
|
||||||
|
|
||||||
|
- a `Node3D` replicates `position` and `quaternion` (never `scale` — nothing in
|
||||||
|
this game animates scale as gameplay state)
|
||||||
|
- every script variable, on the node and any scripted descendant, whose name
|
||||||
|
does not start with `_` and whose declared type is serialisable
|
||||||
|
|
||||||
|
Underscore-prefixed vars are the opt-out, and they already mark exactly the
|
||||||
|
state that must not cross the wire: `@onready` references, cached lookups,
|
||||||
|
per-frame bookkeeping.
|
||||||
|
|
||||||
|
`ON_CHANGE` for state is a deliberate correctness choice, not just bandwidth.
|
||||||
|
Under `ALWAYS` the synchronizer *assigns* every property every tick on receiving
|
||||||
|
peers, so every replicated setter becomes a per-frame hot path — which is how
|
||||||
|
this project previously ended up rebuilding every plate's visuals 60 times a
|
||||||
|
second and rewriting four physics properties per item per tick.
|
||||||
|
|
||||||
|
### Layer 2 — `Net/net_world.gd`
|
||||||
|
|
||||||
|
- Spawnable scenes registered by **sorted** directory scan of `Items/`,
|
||||||
|
`Containers/`, `Stations/`, `Prefabs/`. Auto-spawn transmits an *index* into
|
||||||
|
that array, so the order must be identical on every peer or clients build the
|
||||||
|
wrong scene.
|
||||||
|
- `spawn()` = instantiate → set transform/name → `add_child` under the content
|
||||||
|
root. No `spawn_function`, no payload dictionary.
|
||||||
|
- `despawn()` = `queue_free()` on the server. The `_despawn_static_item` RPC
|
||||||
|
disappears: with nothing baked into the world scene, the spawner covers every
|
||||||
|
case.
|
||||||
|
- `NetReplication.attach` runs from the content root's `child_entered_tree` on
|
||||||
|
**both** peers. That is early enough for the synchronizers to enter the tree
|
||||||
|
inside the spawner's own `add_child`, which is what lets them pick up the
|
||||||
|
spawn payload (Q1).
|
||||||
|
- **Client gating becomes one generic rule** applied at the same hook: disable
|
||||||
|
`_process`, disable every `XRToolsSnapZone`, freeze `RigidBody3D`. This
|
||||||
|
replaces `_gate_station` + `gate_existing_stations` and applies to anything,
|
||||||
|
not just things that happen to be stations.
|
||||||
|
|
||||||
|
### Layer 3 — `Net/net_grab.gd` and `Net/net_stations.gd`
|
||||||
|
|
||||||
|
Two RPCs for the entire game: `request_grab(item)` and
|
||||||
|
`request_release(item, xform, lin, ang)`.
|
||||||
|
|
||||||
|
A client's hand picks the item up locally the instant the player grabs — no
|
||||||
|
round trip — and asks the server to confirm. The server grants by moving
|
||||||
|
`NetXform` authority to that peer (Q4), which is precisely what makes the local
|
||||||
|
copy stop applying inbound transform updates. On release, authority returns to
|
||||||
|
the server, which decides where the item actually ends up: server-authoritative
|
||||||
|
snapping into stations lives in `net_stations.gd`, generic over the `station`
|
||||||
|
group rather than per-station.
|
||||||
|
|
||||||
|
Deleted outright: `Net/net_pickable.gd` (209 lines), `net_held_by` and its
|
||||||
|
held-state juggling, `_set_item_authority`, the grant/reject/force-release
|
||||||
|
negotiation, and `_gate_station`.
|
||||||
|
|
||||||
|
### Layer 4 — tests
|
||||||
|
|
||||||
|
`test/mp_test_driver.gd` (1458 lines) splits into orchestration, steps,
|
||||||
|
asserts, snapshot, and report modules. The scenario list and the sync audit
|
||||||
|
carry over unchanged — that audit compares what is actually *rendered* on both
|
||||||
|
peers, which is what caught the plate-visuals desync that targeted assertions
|
||||||
|
missed. Assertions written against old internals (`net_held_by`, "the client is
|
||||||
|
the item's authority after grabbing") are rewritten against the new model.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- **Replacing a green suite.** Mitigated by running the headless suite after
|
||||||
|
each layer rather than only at the end.
|
||||||
|
- **The harness changes alongside the code it checks.** Mitigated by keeping
|
||||||
|
scenario list and audit logic byte-identical wherever possible, so it remains
|
||||||
|
an independent check rather than one shaped to fit the new code.
|
||||||
|
- **Blanket state replication picks up more vars than the old hand-authored
|
||||||
|
configs.** `ON_CHANGE` makes this cheap, but any setter reached this way still
|
||||||
|
has to be idempotent and null-guarded — a `spawn = true` property fires its
|
||||||
|
setter *before* the node is in the tree, so `@onready` refs are null there.
|
||||||
|
- **Client→client transform** relies on `SceneMultiplayer.server_relay` (on by
|
||||||
|
default) to forward a holder's updates to the other clients. Fine for a
|
||||||
|
listen-server, worth remembering if the topology ever changes.
|
||||||
@@ -14,7 +14,8 @@
|
|||||||
"854c7e1e-7521-4bcd-83ff-bb3fecec3142",
|
"854c7e1e-7521-4bcd-83ff-bb3fecec3142",
|
||||||
"4e2ca8d1-84e1-4913-ad8f-834007d52160",
|
"4e2ca8d1-84e1-4913-ad8f-834007d52160",
|
||||||
"dd822d14-88e5-4790-808e-7d2cf7f79133",
|
"dd822d14-88e5-4790-808e-7d2cf7f79133",
|
||||||
"784d5cd3-333a-40a9-b35f-3d0c73f00761"
|
"784d5cd3-333a-40a9-b35f-3d0c73f00761",
|
||||||
|
"9d7ecbc4-db2a-4d09-9f40-ceed3cf98c00"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -27,17 +28,21 @@
|
|||||||
{
|
{
|
||||||
"uuid": "fab8a81b-7ca9-4026-96bb-ed66ea58ef2e",
|
"uuid": "fab8a81b-7ca9-4026-96bb-ed66ea58ef2e",
|
||||||
"title": "Doing",
|
"title": "Doing",
|
||||||
"tasks": []
|
"tasks": [
|
||||||
|
"f8073eb2-8786-47b7-b63f-fa70c3f7115a"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"uuid": "0d743057-955a-473a-8123-a3f805505d5d",
|
"uuid": "0d743057-955a-473a-8123-a3f805505d5d",
|
||||||
"title": "Done",
|
"title": "Done",
|
||||||
"tasks": [
|
"tasks": [
|
||||||
"6942c45a-53a4-484f-9885-4f249cb4572b",
|
"caf459e5-bb88-47dd-bf15-254a3d70c30f",
|
||||||
"f8073eb2-8786-47b7-b63f-fa70c3f7115a",
|
|
||||||
"21bc19f1-ee3c-4bbb-ab9a-07ec6123a823",
|
"21bc19f1-ee3c-4bbb-ab9a-07ec6123a823",
|
||||||
"d93a31cd-d475-4c5a-87fc-983174b2594f",
|
"d93a31cd-d475-4c5a-87fc-983174b2594f",
|
||||||
|
"0dd511cd-592c-4334-afd8-c511bd36b7d0",
|
||||||
|
"6942c45a-53a4-484f-9885-4f249cb4572b",
|
||||||
"a4dd35ae-dc8e-42a3-9044-bf1cb1af281a",
|
"a4dd35ae-dc8e-42a3-9044-bf1cb1af281a",
|
||||||
|
"7883d015-f065-4b18-a2ab-e2a0daa9dc81",
|
||||||
"1c3b9543-cb67-431d-bf71-c8753e816776",
|
"1c3b9543-cb67-431d-bf71-c8753e816776",
|
||||||
"d9cebd68-792e-4d01-a916-7df5f128b4a8",
|
"d9cebd68-792e-4d01-a916-7df5f128b4a8",
|
||||||
"eee1990e-f957-4499-84b6-e68003fcb78e",
|
"eee1990e-f957-4499-84b6-e68003fcb78e",
|
||||||
@@ -241,6 +246,34 @@
|
|||||||
"description": "",
|
"description": "",
|
||||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||||
"steps": []
|
"steps": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "9d7ecbc4-db2a-4d09-9f40-ceed3cf98c00",
|
||||||
|
"title": "Add test that a hob can chain cook",
|
||||||
|
"description": "",
|
||||||
|
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||||
|
"steps": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "0dd511cd-592c-4334-afd8-c511bd36b7d0",
|
||||||
|
"title": "Plates should snap onto Meals in a station",
|
||||||
|
"description": "Trowing a plate onto a hamburger on a hob should result in a plated hamburger",
|
||||||
|
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||||
|
"steps": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "7883d015-f065-4b18-a2ab-e2a0daa9dc81",
|
||||||
|
"title": "Snap_zones should abosrb any item in range",
|
||||||
|
"description": "",
|
||||||
|
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||||
|
"steps": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uuid": "caf459e5-bb88-47dd-bf15-254a3d70c30f",
|
||||||
|
"title": "Station Snap_zones should cover the whole station",
|
||||||
|
"description": "",
|
||||||
|
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||||
|
"steps": []
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"layout": {
|
"layout": {
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
|
function_pickup.gd:
|
||||||
|
Add nil check in func _pick_up_object(target: Node3D)
|
||||||
|
440: if target and target.has_method("pick_up"):
|
||||||
|
target.pick_up(self)
|
||||||
@@ -20,6 +20,7 @@ config/icon="res://icon.svg"
|
|||||||
XRToolsUserSettings="*uid://bqgb8i74tm0t"
|
XRToolsUserSettings="*uid://bqgb8i74tm0t"
|
||||||
XRToolsRumbleManager="*uid://by853dk86g1qw"
|
XRToolsRumbleManager="*uid://by853dk86g1qw"
|
||||||
NetworkManager="*res://Net/network_manager.gd"
|
NetworkManager="*res://Net/network_manager.gd"
|
||||||
|
NetGrab="*res://Net/net_grab.gd"
|
||||||
GlobalKeyEvents="*uid://c60unagog5oi1"
|
GlobalKeyEvents="*uid://c60unagog5oi1"
|
||||||
Signals="*uid://fuux6cxfjwdc"
|
Signals="*uid://fuux6cxfjwdc"
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
extends Node
|
||||||
|
class_name MpAsserts
|
||||||
|
|
||||||
|
## The per-step checks. Every one is synchronous and side-effect free: it looks
|
||||||
|
## at the world as it currently is on THIS peer and returns a verdict.
|
||||||
|
##
|
||||||
|
## Most of these are run on both peers for the same step, which is the point —
|
||||||
|
## the server's authoritative outcome has to be what the client sees too.
|
||||||
|
|
||||||
|
var view: MpWorldView
|
||||||
|
var snapshot: MpSnapshot
|
||||||
|
var hand: XRToolsFunctionPickup
|
||||||
|
|
||||||
|
|
||||||
|
func setup(p_view: MpWorldView, p_snapshot: MpSnapshot, p_hand: XRToolsFunctionPickup) -> void:
|
||||||
|
view = p_view
|
||||||
|
snapshot = p_snapshot
|
||||||
|
hand = p_hand
|
||||||
|
|
||||||
|
|
||||||
|
## On the server "snapped" means the zone owns the object. On a client the snap
|
||||||
|
## is server-authoritative and never happens locally, so what must be true there
|
||||||
|
## is that the replicated object actually sits in the zone.
|
||||||
|
func snapped(item_name: String, station: String) -> Dictionary:
|
||||||
|
var item := view.find(item_name)
|
||||||
|
var zone := view.zone_of(station)
|
||||||
|
if not item:
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % item_name}
|
||||||
|
if not zone:
|
||||||
|
return {"ok": false, "detail": "station '%s' has no snap zone on this peer" % station}
|
||||||
|
var dist := item.global_position.distance_to(zone.global_position)
|
||||||
|
var problems: Array[String] = []
|
||||||
|
if dist > MpSteps.SNAP_TOLERANCE:
|
||||||
|
problems.append("%s is %.3fm from %s's zone (tolerance %.2f)"
|
||||||
|
% [item_name, dist, station, MpSteps.SNAP_TOLERANCE])
|
||||||
|
if NetworkManager.owns_world():
|
||||||
|
if zone.picked_up_object != item:
|
||||||
|
problems.append("%s's zone holds %s, not %s" % [station, zone.picked_up_object, item_name])
|
||||||
|
elif not item.is_picked_up():
|
||||||
|
problems.append("%s's zone claims %s but it has no grab driver (half-snapped)" % [station, item_name])
|
||||||
|
if problems.is_empty():
|
||||||
|
return {"ok": true, "detail": "%s is snapped into %s; %s" % [item_name, station, view.diag(item, hand)]}
|
||||||
|
return {"ok": false, "detail": "%s | %s" % [", ".join(problems), view.diag(item, hand)]}
|
||||||
|
|
||||||
|
|
||||||
|
func dirty(plate_name: String, want_dirty: bool) -> Dictionary:
|
||||||
|
var plate := view.find(plate_name)
|
||||||
|
if not plate:
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
||||||
|
var pc := plate.get_node_or_null("PlateController")
|
||||||
|
if not pc:
|
||||||
|
return {"ok": false, "detail": "'%s' has no PlateController" % plate_name}
|
||||||
|
if pc.is_dirty != want_dirty:
|
||||||
|
return {"ok": false, "detail": "%s.is_dirty is %s, expected %s; %s"
|
||||||
|
% [plate_name, pc.is_dirty, want_dirty, view.diag(plate, hand)]}
|
||||||
|
return {"ok": true, "detail": "%s.is_dirty == %s as expected" % [plate_name, want_dirty]}
|
||||||
|
|
||||||
|
|
||||||
|
## A consumed object must be gone on EVERY peer, not just the one that consumed it.
|
||||||
|
func gone(item_name: String) -> Dictionary:
|
||||||
|
var item := view.find(item_name)
|
||||||
|
if item and is_instance_valid(item):
|
||||||
|
return {"ok": false, "detail": "'%s' still exists on this peer at %s (it should have been consumed)"
|
||||||
|
% [item_name, item.global_position]}
|
||||||
|
return {"ok": true, "detail": "'%s' is gone, as expected" % item_name}
|
||||||
|
|
||||||
|
|
||||||
|
func food_exists(food_id: String) -> Dictionary:
|
||||||
|
var item := view.find_by_food_id(food_id)
|
||||||
|
if not item:
|
||||||
|
return {"ok": false, "detail": "no item with food id '%s' exists on this peer" % food_id}
|
||||||
|
return {"ok": true, "detail": "'%s' exists: %s at %s" % [food_id, item.name, item.global_position]}
|
||||||
|
|
||||||
|
|
||||||
|
func plate_contains(plate_name: String, food_id: String) -> Dictionary:
|
||||||
|
var plate := view.find(plate_name)
|
||||||
|
if not plate:
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
||||||
|
var pc := plate.get_node_or_null("PlateController")
|
||||||
|
if not pc:
|
||||||
|
return {"ok": false, "detail": "'%s' has no PlateController" % plate_name}
|
||||||
|
if not (food_id in pc.contained_ids):
|
||||||
|
return {"ok": false, "detail": "%s holds %s, expected it to contain '%s'"
|
||||||
|
% [plate_name, str(pc.contained_ids), food_id]}
|
||||||
|
return {"ok": true, "detail": "%s contains %s" % [plate_name, str(pc.contained_ids)]}
|
||||||
|
|
||||||
|
|
||||||
|
## Food shown on a plate is a cosmetic child of it, so it has to stay put when
|
||||||
|
## the plate is picked up and carried. If it drifts, the player sees the burger
|
||||||
|
## fly off the plate.
|
||||||
|
func plate_visuals(plate_name: String) -> Dictionary:
|
||||||
|
var plate := view.find(plate_name)
|
||||||
|
if not plate:
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
||||||
|
var off := snapshot.max_visual_offset(plate)
|
||||||
|
if off < 0.0:
|
||||||
|
return {"ok": false, "detail": "%s is not showing any food to check" % plate_name}
|
||||||
|
if off > MpSnapshot.MAX_VISUAL_OFFSET:
|
||||||
|
return {"ok": false, "detail": "%s's food has come off the plate: %.3fm away (limit %.2f). %s"
|
||||||
|
% [plate_name, off, MpSnapshot.MAX_VISUAL_OFFSET, snapshot.visual_diag(plate)]}
|
||||||
|
return {"ok": true, "detail": "%s's food is still on it (%.3fm from centre). %s"
|
||||||
|
% [plate_name, off, snapshot.visual_diag(plate)]}
|
||||||
|
|
||||||
|
|
||||||
|
## A station's progress bar must show the same thing to everyone: visible while
|
||||||
|
## the station is working, gone once it has finished. Only the world owner runs
|
||||||
|
## station logic, so a client can only get this right if the display is driven
|
||||||
|
## from replicated state.
|
||||||
|
func station_bar(station_name: String, want_visible: bool) -> Dictionary:
|
||||||
|
var station := view.find(station_name)
|
||||||
|
if not station:
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % station_name}
|
||||||
|
var bar := station.get_node_or_null("ProgressBar3D") as ProgressBar3D
|
||||||
|
if not bar:
|
||||||
|
return {"ok": false, "detail": "%s has no ProgressBar3D" % station_name}
|
||||||
|
var shown := bar.is_bar_visible()
|
||||||
|
var detail := "%s bar visible=%s progress=%.0f%%" % [station_name, shown, bar.get_progress()]
|
||||||
|
if "cooking_result" in station:
|
||||||
|
detail += " cooking='%s'" % station.cooking_result
|
||||||
|
if "is_washing" in station:
|
||||||
|
detail += " washing=%s" % station.is_washing
|
||||||
|
if shown != want_visible:
|
||||||
|
return {"ok": false, "detail": "expected %s's bar to be %s, but %s"
|
||||||
|
% [station_name, "visible" if want_visible else "hidden", detail]}
|
||||||
|
return {"ok": true, "detail": detail}
|
||||||
|
|
||||||
|
|
||||||
|
## A station that has had its object taken away must not still be holding it.
|
||||||
|
func zone_empty(station_name: String) -> Dictionary:
|
||||||
|
var zone := view.zone_of(station_name)
|
||||||
|
if not zone:
|
||||||
|
return {"ok": false, "detail": "station '%s' has no snap zone on this peer" % station_name}
|
||||||
|
if not NetworkManager.owns_world():
|
||||||
|
# Client zones are gated off entirely; they never hold anything.
|
||||||
|
return {"ok": true, "detail": "%s: client zones are gated, nothing to check" % station_name}
|
||||||
|
if is_instance_valid(zone.picked_up_object):
|
||||||
|
return {"ok": false, "detail": "%s's zone still holds %s after it was taken away"
|
||||||
|
% [station_name, zone.picked_up_object]}
|
||||||
|
return {"ok": true, "detail": "%s's zone is empty" % station_name}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dnhqfkoir57q6
|
||||||
@@ -0,0 +1,205 @@
|
|||||||
|
extends Node
|
||||||
|
class_name MpReport
|
||||||
|
|
||||||
|
## Everything the harness writes down: the ledger of checks, the run log, the
|
||||||
|
## on-screen overlay, and the per-step screenshots.
|
||||||
|
##
|
||||||
|
## Kept apart from the test logic so a step never has to think about where its
|
||||||
|
## output goes — it returns a verdict, and this decides how that is recorded.
|
||||||
|
|
||||||
|
## Lines kept in the on-screen overlay. Enough to show a whole step without the
|
||||||
|
## panel covering the kitchen underneath it.
|
||||||
|
const OVERLAY_LINES := 11
|
||||||
|
|
||||||
|
## Frames are captured at half the viewport's resolution: 60-odd full-size frames
|
||||||
|
## per peer is a lot of pixels to write and then re-encode.
|
||||||
|
const FRAME_SCALE := 0.5
|
||||||
|
|
||||||
|
## Every check, in order: {step, side, ok, detail}.
|
||||||
|
var results: Array[Dictionary] = []
|
||||||
|
|
||||||
|
var _log_file: FileAccess
|
||||||
|
var _log_path := ""
|
||||||
|
var _overlay: Label
|
||||||
|
var _overlay_lines: Array[String] = []
|
||||||
|
|
||||||
|
var _frames_enabled := false
|
||||||
|
var _frames_dir := ""
|
||||||
|
var _frame_index := 0
|
||||||
|
var _frame_labels: Array[String] = []
|
||||||
|
|
||||||
|
|
||||||
|
func setup(is_server: bool, overlay_parent: Node) -> void:
|
||||||
|
var role := "server" if is_server else "client"
|
||||||
|
_log_path = "res://logs/mptest_%s.log" % role
|
||||||
|
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path("res://logs"))
|
||||||
|
_log_file = FileAccess.open(_log_path, FileAccess.WRITE)
|
||||||
|
_build_overlay(overlay_parent)
|
||||||
|
|
||||||
|
|
||||||
|
# --- log -------------------------------------------------------------------
|
||||||
|
|
||||||
|
func log_line(s: String) -> void:
|
||||||
|
print("[MPTEST] %s" % s)
|
||||||
|
if _log_file:
|
||||||
|
_log_file.store_line(s)
|
||||||
|
_log_file.flush()
|
||||||
|
_push_overlay(s)
|
||||||
|
|
||||||
|
|
||||||
|
func banner(s: String) -> void:
|
||||||
|
log_line("")
|
||||||
|
log_line("======== %s ========" % s)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_overlay(parent: Node) -> void:
|
||||||
|
if not parent:
|
||||||
|
return
|
||||||
|
var layer := CanvasLayer.new()
|
||||||
|
parent.add_child(layer)
|
||||||
|
_overlay = Label.new()
|
||||||
|
_overlay.add_theme_font_size_override("font_size", 14)
|
||||||
|
_overlay.add_theme_color_override("font_color", Color.WHITE)
|
||||||
|
_overlay.add_theme_color_override("font_outline_color", Color.BLACK)
|
||||||
|
_overlay.add_theme_constant_override("outline_size", 6)
|
||||||
|
_overlay.set_anchors_preset(Control.PRESET_TOP_WIDE)
|
||||||
|
layer.add_child(_overlay)
|
||||||
|
|
||||||
|
|
||||||
|
func _push_overlay(s: String) -> void:
|
||||||
|
if not _overlay:
|
||||||
|
return
|
||||||
|
_overlay_lines.append(s)
|
||||||
|
while _overlay_lines.size() > OVERLAY_LINES:
|
||||||
|
_overlay_lines.pop_front()
|
||||||
|
_overlay.text = "\n".join(_overlay_lines)
|
||||||
|
|
||||||
|
|
||||||
|
# --- ledger ----------------------------------------------------------------
|
||||||
|
|
||||||
|
func record(step: String, side: String, res: Dictionary) -> void:
|
||||||
|
var ok: bool = res.get("ok", false)
|
||||||
|
var detail: String = str(res.get("detail", ""))
|
||||||
|
results.append({"step": step, "side": side, "ok": ok, "detail": detail})
|
||||||
|
log_line("%s [%s] %s" % ["PASS" if ok else "FAIL", side, step])
|
||||||
|
log_line(" %s" % detail)
|
||||||
|
|
||||||
|
|
||||||
|
func failed_count() -> int:
|
||||||
|
var failed := 0
|
||||||
|
for r in results:
|
||||||
|
if not r["ok"]:
|
||||||
|
failed += 1
|
||||||
|
return failed
|
||||||
|
|
||||||
|
|
||||||
|
func print_summary() -> void:
|
||||||
|
banner("RESULTS")
|
||||||
|
for r in results:
|
||||||
|
log_line("%-4s %-8s %s" % ["PASS" if r["ok"] else "FAIL", r["side"], r["step"]])
|
||||||
|
var failed := failed_count()
|
||||||
|
log_line("%d/%d checks passed" % [results.size() - failed, results.size()])
|
||||||
|
for r in results:
|
||||||
|
if not r["ok"]:
|
||||||
|
log_line("FAILURE %s [%s]: %s" % [r["step"], r["side"], r["detail"]])
|
||||||
|
|
||||||
|
|
||||||
|
## A standalone report written next to the logs, so a run can be read without
|
||||||
|
## scrolling the console — and so the in-editor runner can print it back.
|
||||||
|
func write_report() -> void:
|
||||||
|
var path := "res://logs/mptest_report.txt"
|
||||||
|
var f := FileAccess.open(path, FileAccess.WRITE)
|
||||||
|
if not f:
|
||||||
|
return
|
||||||
|
var failed := failed_count()
|
||||||
|
var passed := results.size() - failed
|
||||||
|
f.store_line("VRyHungry multiplayer test report")
|
||||||
|
f.store_line("run at %s" % Time.get_datetime_string_from_system())
|
||||||
|
f.store_line("")
|
||||||
|
f.store_line("RESULT: %s (%d passed, %d failed, %d total)"
|
||||||
|
% ["ALL CHECKS PASSED" if failed == 0 else "FAILED", passed, failed, results.size()])
|
||||||
|
f.store_line("")
|
||||||
|
if failed > 0:
|
||||||
|
f.store_line("--- failures ---")
|
||||||
|
for r in results:
|
||||||
|
if not r["ok"]:
|
||||||
|
f.store_line("FAIL [%s] %s" % [r["side"], r["step"]])
|
||||||
|
f.store_line(" %s" % r["detail"])
|
||||||
|
f.store_line("")
|
||||||
|
f.store_line("--- every check, in order ---")
|
||||||
|
for r in results:
|
||||||
|
f.store_line("%-4s %-6s %s" % ["PASS" if r["ok"] else "FAIL", r["side"], r["step"]])
|
||||||
|
f.close()
|
||||||
|
log_line("report written to %s" % ProjectSettings.globalize_path(path))
|
||||||
|
|
||||||
|
|
||||||
|
# --- per-step screenshots --------------------------------------------------
|
||||||
|
#
|
||||||
|
# One frame per peer per step, stitched into a side-by-side GIF afterwards (see
|
||||||
|
# test/make_gif.ps1). Seeing both peers' viewports next to each other for the
|
||||||
|
# same step is the fastest way to spot a visual desync: the log tells you
|
||||||
|
# something diverged, the GIF shows you what it looked like.
|
||||||
|
|
||||||
|
func setup_frames(enabled: bool, is_server: bool) -> void:
|
||||||
|
_frames_enabled = enabled
|
||||||
|
if not _frames_enabled:
|
||||||
|
return
|
||||||
|
if DisplayServer.get_name() == "headless":
|
||||||
|
_frames_enabled = false
|
||||||
|
log_line("frame capture disabled: a headless run has no rendered output to grab")
|
||||||
|
return
|
||||||
|
_frames_dir = "res://logs/mptest_frames_%s" % ("server" if is_server else "client")
|
||||||
|
DirAccess.make_dir_recursive_absolute(ProjectSettings.globalize_path(_frames_dir))
|
||||||
|
# Clear a previous run's frames, or the GIF splices the two together.
|
||||||
|
var dir := DirAccess.open(_frames_dir)
|
||||||
|
if dir:
|
||||||
|
for f in dir.get_files():
|
||||||
|
if f.ends_with(".png"):
|
||||||
|
dir.remove(f)
|
||||||
|
log_line("capturing a frame per step into %s" % ProjectSettings.globalize_path(_frames_dir))
|
||||||
|
|
||||||
|
|
||||||
|
func frames_enabled() -> bool:
|
||||||
|
return _frames_enabled
|
||||||
|
|
||||||
|
|
||||||
|
func next_frame_index() -> int:
|
||||||
|
_frame_index += 1
|
||||||
|
return _frame_index
|
||||||
|
|
||||||
|
|
||||||
|
func save_frame(index: int, label: String) -> void:
|
||||||
|
if not _frames_enabled:
|
||||||
|
return
|
||||||
|
_frame_index = index
|
||||||
|
# Wait for the frame to actually be drawn, or we capture whatever was in the
|
||||||
|
# buffer before this step's changes landed.
|
||||||
|
await RenderingServer.frame_post_draw
|
||||||
|
var tex := get_viewport().get_texture()
|
||||||
|
if not tex:
|
||||||
|
return
|
||||||
|
var img := tex.get_image()
|
||||||
|
if not img:
|
||||||
|
return
|
||||||
|
if FRAME_SCALE != 1.0:
|
||||||
|
img.resize(int(img.get_width() * FRAME_SCALE), int(img.get_height() * FRAME_SCALE),
|
||||||
|
Image.INTERPOLATE_BILINEAR)
|
||||||
|
# Index-only filenames so ffmpeg's image sequence reader picks them up as
|
||||||
|
# frame_%04d.png; the step name is already legible in the overlay.
|
||||||
|
var err := img.save_png("%s/frame_%04d.png" % [_frames_dir, index])
|
||||||
|
if err != OK:
|
||||||
|
log_line(" could not save frame %d (%s)" % [index, error_string(err)])
|
||||||
|
else:
|
||||||
|
_frame_labels.append("%04d %s" % [index, label])
|
||||||
|
|
||||||
|
|
||||||
|
## Written alongside the frames so a frame number can be traced back to the step
|
||||||
|
## that produced it.
|
||||||
|
func write_frame_index() -> void:
|
||||||
|
if not _frames_enabled or _frame_labels.is_empty():
|
||||||
|
return
|
||||||
|
var f := FileAccess.open("%s/frames.txt" % _frames_dir, FileAccess.WRITE)
|
||||||
|
if f:
|
||||||
|
for line in _frame_labels:
|
||||||
|
f.store_line(line)
|
||||||
|
f.close()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cc0fn6yfqc0ey
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
extends Node
|
||||||
|
class_name MpSnapshot
|
||||||
|
|
||||||
|
## The cross-peer sync audit: both peers describe everything they can see, and
|
||||||
|
## the two descriptions must match.
|
||||||
|
##
|
||||||
|
## This is what actually catches desyncs. Per-step assertions only look at the
|
||||||
|
## one thing the step touched, which misses the whole class of bug where some
|
||||||
|
## OTHER object quietly drifts out of sync — or where the data replicates
|
||||||
|
## perfectly but the thing on screen does not. So the description deliberately
|
||||||
|
## includes what is RENDERED, not just the replicated values behind it: a plate
|
||||||
|
## whose contained_ids arrived but whose visuals were never rebuilt looks empty
|
||||||
|
## to the player, and that has to count as a desync.
|
||||||
|
|
||||||
|
## How far apart the same object may be on two peers before it counts as a desync.
|
||||||
|
const POS_TOLERANCE := 0.08
|
||||||
|
## Physics keeps moving after a step (dropped objects fall, snaps settle), so
|
||||||
|
## give the peers this long to converge before calling a difference a failure.
|
||||||
|
const SETTLE_SEC := 3.0
|
||||||
|
## How far a cosmetic item on a plate may sit from the plate's origin. The plate
|
||||||
|
## is ~0.4m across and the furthest slot ~0.12m out, so past this it has come off.
|
||||||
|
const MAX_VISUAL_OFFSET := 0.3
|
||||||
|
## Local offset at which a cosmetic item counts as having moved from its slot.
|
||||||
|
const VISUAL_DRIFT_EPSILON := 0.05
|
||||||
|
|
||||||
|
## Stations whose on-screen state has to match on every peer.
|
||||||
|
const WATCHED_STATIONS := ["Hob", "Sink", "DirtStation", "Counter", "Counter2"]
|
||||||
|
|
||||||
|
var view: MpWorldView
|
||||||
|
var report: MpReport
|
||||||
|
|
||||||
|
## Named by the driver so a drift warning says which step was running.
|
||||||
|
var current_step := "(before any step)"
|
||||||
|
|
||||||
|
var _drift_reported := {}
|
||||||
|
|
||||||
|
|
||||||
|
func setup(p_view: MpWorldView, p_report: MpReport) -> void:
|
||||||
|
view = p_view
|
||||||
|
report = p_report
|
||||||
|
|
||||||
|
|
||||||
|
func take() -> Dictionary:
|
||||||
|
var out := {}
|
||||||
|
for child in view.pickables():
|
||||||
|
out[str(child.name)] = describe(child as Node3D)
|
||||||
|
for name in WATCHED_STATIONS:
|
||||||
|
var station := view.find(name)
|
||||||
|
if station:
|
||||||
|
out["station:" + name] = describe_station(station)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Everything about an object that should look identical on every peer.
|
||||||
|
func describe(item: Node3D) -> Dictionary:
|
||||||
|
var d := {"pos": item.global_position, "visible": item.visible}
|
||||||
|
var pc := item.get_node_or_null("PlateController")
|
||||||
|
if pc:
|
||||||
|
d["dirty"] = pc.is_dirty
|
||||||
|
d["contents"] = ",".join(pc.contained_ids)
|
||||||
|
var dirty_node := item.get_node_or_null("Dirty")
|
||||||
|
d["dirty_shown"] = dirty_node.visible if dirty_node else false
|
||||||
|
# The cosmetic copies the container builds from contained_ids — literally
|
||||||
|
# what the player sees sitting on the plate.
|
||||||
|
d["meals_shown"] = visual_count(item, "Container/MealContainer")
|
||||||
|
d["sides_shown"] = visual_count(item, "Container/SidesContainer")
|
||||||
|
# Food on a plate is a cosmetic child of the plate, so it must travel with
|
||||||
|
# it. Keys starting with "_" are per-peer diagnostics that compare() skips
|
||||||
|
# (floats will not match exactly across peers); the attached flag is
|
||||||
|
# asserted absolutely instead, because this can — and did — go wrong on
|
||||||
|
# both peers at once, which a diff would miss entirely.
|
||||||
|
var off := max_visual_offset(item)
|
||||||
|
d["_visual_offset"] = off
|
||||||
|
d["visuals_attached"] = off <= MAX_VISUAL_OFFSET
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
## What a station shows the player. Only the world owner runs a station's logic
|
||||||
|
## and snap zone, so its display has to be driven from replicated state — a
|
||||||
|
## client that never updates it shows a hob that never lights up, or a sink bar
|
||||||
|
## still on screen after the plate came out clean.
|
||||||
|
##
|
||||||
|
## Bar VISIBILITY is compared across peers; the progress value is diagnostic only
|
||||||
|
## ("_" prefix), because it changes every tick and the peers are legitimately a
|
||||||
|
## frame apart.
|
||||||
|
func describe_station(station: Node3D) -> Dictionary:
|
||||||
|
var d := {}
|
||||||
|
var bar := station.get_node_or_null("ProgressBar3D") as ProgressBar3D
|
||||||
|
d["bar_visible"] = bar.is_bar_visible() if bar else false
|
||||||
|
d["_bar_progress"] = snappedf(bar.get_progress(), 1.0) if bar else -1.0
|
||||||
|
if "cooking_result" in station:
|
||||||
|
d["cooking"] = str(station.cooking_result)
|
||||||
|
if "is_washing" in station:
|
||||||
|
d["washing"] = bool(station.is_washing)
|
||||||
|
return d
|
||||||
|
|
||||||
|
|
||||||
|
## Largest distance from a plate's origin to any cosmetic item it is displaying.
|
||||||
|
## -1 when the plate is showing nothing.
|
||||||
|
func max_visual_offset(item: Node3D) -> float:
|
||||||
|
var worst := -1.0
|
||||||
|
for path in ["Container/MealContainer", "Container/SidesContainer"]:
|
||||||
|
var root := item.get_node_or_null(path)
|
||||||
|
if not root:
|
||||||
|
continue
|
||||||
|
for c in root.get_children():
|
||||||
|
if c is Node3D and not c.is_queued_for_deletion():
|
||||||
|
worst = maxf(worst, item.global_position.distance_to((c as Node3D).global_position))
|
||||||
|
return snappedf(worst, 0.001)
|
||||||
|
|
||||||
|
|
||||||
|
## What the cosmetic children look like, for diagnosing why they moved.
|
||||||
|
func visual_diag(item: Node3D) -> String:
|
||||||
|
var parts: Array[String] = []
|
||||||
|
for path in ["Container/MealContainer", "Container/SidesContainer"]:
|
||||||
|
var root := item.get_node_or_null(path)
|
||||||
|
if not root:
|
||||||
|
continue
|
||||||
|
for c in root.get_children():
|
||||||
|
var s := "%s global=%s local=%s parent=%s (%.3fm from plate at %s)" % [
|
||||||
|
c.name, (c as Node3D).global_position, (c as Node3D).position,
|
||||||
|
c.get_parent().name,
|
||||||
|
item.global_position.distance_to((c as Node3D).global_position),
|
||||||
|
item.global_position]
|
||||||
|
if c is RigidBody3D:
|
||||||
|
s += " [RigidBody3D freeze=%s freeze_mode=%d layer=%d top_level=%s queued=%s]" % [
|
||||||
|
c.freeze, c.freeze_mode, c.collision_layer, c.top_level,
|
||||||
|
c.is_queued_for_deletion()]
|
||||||
|
parts.append(s)
|
||||||
|
return "; ".join(parts) if parts else "(nothing on the plate)"
|
||||||
|
|
||||||
|
|
||||||
|
func visual_count(item: Node3D, path: String) -> int:
|
||||||
|
var n := item.get_node_or_null(path)
|
||||||
|
if not n:
|
||||||
|
return -1
|
||||||
|
var count := 0
|
||||||
|
for c in n.get_children():
|
||||||
|
if not c.is_queued_for_deletion():
|
||||||
|
count += 1
|
||||||
|
return count
|
||||||
|
|
||||||
|
|
||||||
|
## Compares the server's view with the client's, returning the differences.
|
||||||
|
func compare(server: Dictionary, client: Dictionary) -> Array[String]:
|
||||||
|
var problems: Array[String] = []
|
||||||
|
for name in server:
|
||||||
|
if not client.has(name):
|
||||||
|
problems.append("'%s' exists on the server but NOT on the client" % name)
|
||||||
|
for name in client:
|
||||||
|
if not server.has(name):
|
||||||
|
problems.append("'%s' exists on the client but NOT on the server (ghost copy)" % name)
|
||||||
|
for name in server:
|
||||||
|
if not client.has(name):
|
||||||
|
continue
|
||||||
|
var s: Dictionary = server[name]
|
||||||
|
var c: Dictionary = client[name]
|
||||||
|
# Stations are compared on their displayed state, not a position.
|
||||||
|
if s.has("pos") and c.has("pos"):
|
||||||
|
var dist: float = (s["pos"] as Vector3).distance_to(c["pos"])
|
||||||
|
if dist > POS_TOLERANCE:
|
||||||
|
problems.append("%s is %.3fm apart (server %s vs client %s)" % [name, dist, s["pos"], c["pos"]])
|
||||||
|
for key in s:
|
||||||
|
# "_" keys are per-peer diagnostics, not things that must match.
|
||||||
|
if key == "pos" or key.begins_with("_"):
|
||||||
|
continue
|
||||||
|
if s[key] != c[key]:
|
||||||
|
problems.append("%s.%s: server=%s client=%s" % [name, key, s[key], c[key]])
|
||||||
|
# Absolute invariants, checked per peer. A cross-peer diff cannot catch a
|
||||||
|
# fault that happens identically on both sides.
|
||||||
|
for peer_name in ["server", "client"]:
|
||||||
|
var snap: Dictionary = server if peer_name == "server" else client
|
||||||
|
for name in snap:
|
||||||
|
var d: Dictionary = snap[name]
|
||||||
|
if d.has("visuals_attached") and not d["visuals_attached"]:
|
||||||
|
problems.append("on the %s, %s's food has come off the plate (%.3fm from it, limit %.2f)"
|
||||||
|
% [peer_name, name, d.get("_visual_offset", -1.0), MAX_VISUAL_OFFSET])
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
# --- live watch on plate visuals -------------------------------------------
|
||||||
|
#
|
||||||
|
# The per-step checks tell us food ended up off a plate, but not when or why.
|
||||||
|
# This watches every frame and reports the first frame a cosmetic item departs
|
||||||
|
# from its slot, along with what was happening to the plate at the time.
|
||||||
|
|
||||||
|
func _process(_delta: float) -> void:
|
||||||
|
if not view or not view.world:
|
||||||
|
return
|
||||||
|
for item in view.roots_children():
|
||||||
|
if not (item is Node3D) or not item.get_node_or_null("PlateController"):
|
||||||
|
continue
|
||||||
|
_watch_plate(item as Node3D)
|
||||||
|
|
||||||
|
|
||||||
|
func _watch_plate(plate: Node3D) -> void:
|
||||||
|
for path in ["Container/MealContainer", "Container/SidesContainer"]:
|
||||||
|
var holder := plate.get_node_or_null(path)
|
||||||
|
if not holder:
|
||||||
|
continue
|
||||||
|
for c in holder.get_children():
|
||||||
|
if not (c is Node3D) or c.is_queued_for_deletion():
|
||||||
|
continue
|
||||||
|
var drift: float = (c as Node3D).position.length()
|
||||||
|
var key := c.get_instance_id()
|
||||||
|
if drift <= VISUAL_DRIFT_EPSILON:
|
||||||
|
_drift_reported.erase(key)
|
||||||
|
continue
|
||||||
|
if _drift_reported.has(key):
|
||||||
|
continue
|
||||||
|
_drift_reported[key] = true
|
||||||
|
report.log_line("VISUAL DRIFT: %s on %s moved to local %s (%.3f from its slot) during '%s'" % [
|
||||||
|
c.name, plate.name, (c as Node3D).position, drift, current_step])
|
||||||
|
report.log_line(" plate: pos=%s freeze=%s held_by=%s xform_authority=%d" % [
|
||||||
|
plate.global_position, plate.freeze if plate is RigidBody3D else "-",
|
||||||
|
plate.get_picked_up_by() if plate.has_method("get_picked_up_by") else "-",
|
||||||
|
view.xform_authority(plate)])
|
||||||
|
if c is RigidBody3D:
|
||||||
|
report.log_line(" visual: freeze=%s mode=%d layer=%d top_level=%s sleeping=%s lin_vel=%s" % [
|
||||||
|
c.freeze, c.freeze_mode, c.collision_layer, c.top_level,
|
||||||
|
c.sleeping, c.linear_velocity])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bcxf7wu740dqs
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
extends Node
|
||||||
|
class_name MpSteps
|
||||||
|
|
||||||
|
## The simulated player actions: reach, grab, carry, drop.
|
||||||
|
##
|
||||||
|
## These drive the real godot-xr-tools pickup path rather than teleporting
|
||||||
|
## objects around, because the point is to test what a player actually does. XR
|
||||||
|
## gives no tracked controller headlessly, so XRToolsFunctionPickup._process
|
||||||
|
## bails out on `_controller.get_is_active()` — the grab is therefore driven by
|
||||||
|
## hand: position the hand, let the grab Area3D register what is in range,
|
||||||
|
## refresh the closest-object pick, then press grip.
|
||||||
|
##
|
||||||
|
## Every action returns {ok: bool, detail: String}.
|
||||||
|
|
||||||
|
## Distance under which an object counts as "at" a snap zone.
|
||||||
|
const SNAP_TOLERANCE := 0.12
|
||||||
|
## Cooking and washing both take ~3s of station time; allow generously for it.
|
||||||
|
const STATION_WORK_TIMEOUT := 20.0
|
||||||
|
|
||||||
|
var view: MpWorldView
|
||||||
|
var report: MpReport
|
||||||
|
var hand: XRToolsFunctionPickup
|
||||||
|
var controller: Node3D
|
||||||
|
|
||||||
|
var _hand_detached := false
|
||||||
|
|
||||||
|
|
||||||
|
func setup(p_view: MpWorldView, p_report: MpReport, p_hand: XRToolsFunctionPickup, p_controller: Node3D) -> void:
|
||||||
|
view = p_view
|
||||||
|
report = p_report
|
||||||
|
hand = p_hand
|
||||||
|
controller = p_controller
|
||||||
|
|
||||||
|
|
||||||
|
# --- waiting ---------------------------------------------------------------
|
||||||
|
|
||||||
|
func wait_frames(n: int) -> void:
|
||||||
|
for i in n:
|
||||||
|
await get_tree().physics_frame
|
||||||
|
|
||||||
|
|
||||||
|
## Poll a condition instead of sleeping a guessed duration, so a slow machine
|
||||||
|
## does not produce a spurious failure and a fast one does not waste seconds.
|
||||||
|
func wait_until(cond: Callable, what: String, timeout: float) -> bool:
|
||||||
|
var deadline := Time.get_ticks_msec() + int(timeout * 1000.0)
|
||||||
|
while Time.get_ticks_msec() < deadline:
|
||||||
|
if cond.call():
|
||||||
|
return true
|
||||||
|
await get_tree().process_frame
|
||||||
|
report.log_line(" timed out after %.0fs waiting for %s" % [timeout, what])
|
||||||
|
return false
|
||||||
|
|
||||||
|
|
||||||
|
## Puts the hand at a world position.
|
||||||
|
##
|
||||||
|
## The controller is a child of the XROrigin, whose PlayerBody keeps moving under
|
||||||
|
## gravity — that drags the hand off any position we place it at. top_level
|
||||||
|
## detaches it into world space. Done lazily on first use, so simply running the
|
||||||
|
## scene by hand leaves a real tracked controller alone.
|
||||||
|
func move_hand_to(pos: Vector3) -> void:
|
||||||
|
if not _hand_detached:
|
||||||
|
_hand_detached = true
|
||||||
|
controller.top_level = true
|
||||||
|
report.log_line(" (detached the test hand from the XR rig so it can be driven directly)")
|
||||||
|
controller.global_position = pos
|
||||||
|
controller.force_update_transform()
|
||||||
|
|
||||||
|
|
||||||
|
# --- actions ---------------------------------------------------------------
|
||||||
|
|
||||||
|
func grab(item: Node3D, label: String) -> Dictionary:
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % label}
|
||||||
|
report.log_line(" reaching for %s at %s" % [label, item.global_position])
|
||||||
|
move_hand_to(item.global_position)
|
||||||
|
await wait_frames(4)
|
||||||
|
hand._update_closest_object()
|
||||||
|
var closest := hand.closest_object
|
||||||
|
report.log_line(" hand at %s, closest grabbable = %s" % [hand.global_position, closest])
|
||||||
|
if not is_instance_valid(closest):
|
||||||
|
return {"ok": false, "detail": "hand found nothing to grab. %s" % view.diag(item, hand)}
|
||||||
|
report.log_line(" pressing grip")
|
||||||
|
hand._on_grip_pressed()
|
||||||
|
await wait_frames(4)
|
||||||
|
if not item.is_picked_up():
|
||||||
|
return {"ok": false, "detail": "grip pressed on %s but %s not picked up. %s"
|
||||||
|
% [closest.name, label, view.diag(item, hand)]}
|
||||||
|
if item.get_picked_up_by() != hand:
|
||||||
|
return {"ok": false, "detail": "%s is held by %s, not our hand. %s"
|
||||||
|
% [label, item.get_picked_up_by(), view.diag(item, hand)]}
|
||||||
|
# Grabbing is predicted locally and confirmed by the server handing us the
|
||||||
|
# object's NetXform. Until that lands the server is still driving it, so
|
||||||
|
# waiting here is what makes the following carry meaningful.
|
||||||
|
report.log_line(" holding %s; waiting for the server to hand over NetXform..." % label)
|
||||||
|
if not await wait_until(func(): return view.xform_authority(item) == multiplayer.get_unique_id(),
|
||||||
|
"NetXform handoff", 5.0):
|
||||||
|
return {"ok": false, "detail": "grabbed %s, but NetXform authority stayed with peer %d. %s"
|
||||||
|
% [label, view.xform_authority(item), view.diag(item, hand)]}
|
||||||
|
report.log_line(" NetXform is ours (peer %d)" % multiplayer.get_unique_id())
|
||||||
|
return {"ok": true, "detail": "grabbed %s via %s; %s" % [label, closest.name, view.diag(item, hand)]}
|
||||||
|
|
||||||
|
|
||||||
|
func drop(item: Node3D, label: String) -> Dictionary:
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % label}
|
||||||
|
if not item.is_picked_up():
|
||||||
|
return {"ok": false, "detail": "nothing to drop: %s is not held. %s" % [label, view.diag(item, hand)]}
|
||||||
|
report.log_line(" releasing grip (held by %s)" % item.get_picked_up_by())
|
||||||
|
hand._on_grip_release()
|
||||||
|
await wait_frames(8)
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return {"ok": true, "detail": "%s was consumed on release" % label}
|
||||||
|
# Explicitly typed: `item` is a Node3D here, so get_picked_up_by()'s return
|
||||||
|
# type is not known statically and := cannot infer it.
|
||||||
|
var by: Node = item.get_picked_up_by()
|
||||||
|
report.log_line(" after release %s is held by %s" % [label, by])
|
||||||
|
if by == hand:
|
||||||
|
return {"ok": false, "detail": "grip released but our hand still holds %s" % label}
|
||||||
|
# A station catching the object on release is intended — but only on the peer
|
||||||
|
# that owns world logic. A client's own snap zone doing it means that peer is
|
||||||
|
# running station logic it has no authority for.
|
||||||
|
if by is XRToolsSnapZone and not NetworkManager.owns_world():
|
||||||
|
return {"ok": false, "detail": "%s on this client grabbed %s out of our hand; station snap zones must only run on the world owner (zone enabled=%s)"
|
||||||
|
% [by.get_parent().name, label, by.enabled]}
|
||||||
|
if not await wait_until(func(): return not is_instance_valid(item) or view.xform_authority(item) == 1,
|
||||||
|
"NetXform to return to the server", 5.0):
|
||||||
|
return {"ok": false, "detail": "dropped %s, but NetXform authority stayed with peer %d"
|
||||||
|
% [label, view.xform_authority(item)]}
|
||||||
|
return {"ok": true, "detail": "dropped %s (now held by %s)" % [label, by]}
|
||||||
|
|
||||||
|
|
||||||
|
## Grab an object, carry it onto a station's snap zone, and let go there.
|
||||||
|
func place_in_zone(item: Node3D, label: String, station: String) -> Dictionary:
|
||||||
|
var zone := view.zone_of(station)
|
||||||
|
if not zone:
|
||||||
|
return {"ok": false, "detail": "station '%s' has no snap zone on this peer" % station}
|
||||||
|
var res := await grab(item, label)
|
||||||
|
if not res["ok"]:
|
||||||
|
return res
|
||||||
|
res = await carry_to(item, label, zone.global_position)
|
||||||
|
if not res["ok"]:
|
||||||
|
return res
|
||||||
|
res = await drop(item, label)
|
||||||
|
if not res["ok"]:
|
||||||
|
return res
|
||||||
|
report.log_line(" waiting for %s to settle onto %s..." % [label, station])
|
||||||
|
await wait_until(func(): return not is_instance_valid(item) \
|
||||||
|
or item.global_position.distance_to(zone.global_position) <= SNAP_TOLERANCE,
|
||||||
|
"%s to settle onto %s" % [label, station], 5.0)
|
||||||
|
return {"ok": true, "detail": "placed %s into %s; %s" % [label, station, view.diag(item, hand)]}
|
||||||
|
|
||||||
|
|
||||||
|
## Grab an object and carry it into another, holding it there. Used for the
|
||||||
|
## combine (buns into the cooked burger) and for putting food on a plate, both of
|
||||||
|
## which are triggered by an Area3D overlap rather than by dropping.
|
||||||
|
func carry_item_to_item(item: Node3D, label: String, target: Node3D, target_label: String) -> Dictionary:
|
||||||
|
if not is_instance_valid(target):
|
||||||
|
return {"ok": false, "detail": "target '%s' does not exist on this peer" % target_label}
|
||||||
|
var target_pos := target.global_position
|
||||||
|
var res := await grab(item, label)
|
||||||
|
if not res["ok"]:
|
||||||
|
return res
|
||||||
|
res = await carry_to(item, label, target_pos)
|
||||||
|
if not res["ok"] and is_instance_valid(item):
|
||||||
|
return res
|
||||||
|
# Hold it there a moment: the reaction happens on the server, and the object
|
||||||
|
# we are holding may be consumed by it.
|
||||||
|
for i in 60:
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
report.log_line(" %s was consumed (the reaction fired)" % label)
|
||||||
|
return {"ok": true, "detail": "%s consumed on contact with %s" % [label, target_label]}
|
||||||
|
await get_tree().physics_frame
|
||||||
|
# Both uses of this consume the carried object, so still holding it means the
|
||||||
|
# reaction never fired. Let go first, so the next step is not left fighting
|
||||||
|
# our hand.
|
||||||
|
if is_instance_valid(item) and item.is_picked_up():
|
||||||
|
hand._on_grip_release()
|
||||||
|
await wait_frames(8)
|
||||||
|
return {"ok": false, "detail": "%s reached %s but was never consumed - the reaction did not fire; %s"
|
||||||
|
% [label, target_label, view.diag(item, hand)]}
|
||||||
|
|
||||||
|
|
||||||
|
## Carry an object somewhere clear and put it down, freeing the station it was in.
|
||||||
|
func park(item: Node3D, label: String, pos: Vector3) -> Dictionary:
|
||||||
|
var res := await grab(item, label)
|
||||||
|
if not res["ok"]:
|
||||||
|
return res
|
||||||
|
res = await carry_to(item, label, pos)
|
||||||
|
if not res["ok"]:
|
||||||
|
return res
|
||||||
|
return await drop(item, label)
|
||||||
|
|
||||||
|
|
||||||
|
## Move the held object onto a world position. Closed-loop: the hand moves by
|
||||||
|
## whatever the object's remaining error is, since the grab point offsets the
|
||||||
|
## object from the hand by an amount not worth hard-coding.
|
||||||
|
func carry_to(item: Node3D, label: String, target: Vector3) -> Dictionary:
|
||||||
|
if not is_instance_valid(item) or not item.is_picked_up():
|
||||||
|
return {"ok": false, "detail": "cannot carry: %s is not held" % label}
|
||||||
|
report.log_line(" carrying %s from %s to %s" % [label, item.global_position, target])
|
||||||
|
for i in 120:
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return {"ok": true, "detail": "%s was consumed while being carried" % label}
|
||||||
|
var err := item.global_position - target
|
||||||
|
if err.length() <= 0.01:
|
||||||
|
break
|
||||||
|
controller.global_position -= err
|
||||||
|
await get_tree().physics_frame
|
||||||
|
await get_tree().physics_frame
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return {"ok": true, "detail": "%s was consumed while being carried" % label}
|
||||||
|
var dist := item.global_position.distance_to(target)
|
||||||
|
report.log_line(" %s is now %.3fm from the target" % [label, dist])
|
||||||
|
if dist > 0.05:
|
||||||
|
return {"ok": false, "detail": "could not carry %s to the target: still %.3fm away" % [label, dist]}
|
||||||
|
return {"ok": true, "detail": "carried %s to within %.3fm" % [label, dist]}
|
||||||
|
|
||||||
|
|
||||||
|
## Wait for a station to produce an object with the given food id (cooking the
|
||||||
|
## burger, or combining into a hamburger). Server-side: stations only run there.
|
||||||
|
func await_food(food_id: String) -> Dictionary:
|
||||||
|
report.log_line(" waiting for a '%s' to appear..." % food_id)
|
||||||
|
var ok := await wait_until(func(): return view.find_by_food_id(food_id) != null,
|
||||||
|
"a '%s' to be produced" % food_id, STATION_WORK_TIMEOUT)
|
||||||
|
if not ok:
|
||||||
|
return {"ok": false, "detail": "no '%s' was produced within %.0fs" % [food_id, STATION_WORK_TIMEOUT]}
|
||||||
|
# Let the spawn replicate before the checks that follow go looking for it.
|
||||||
|
await wait_frames(30)
|
||||||
|
var made := view.find_by_food_id(food_id)
|
||||||
|
return {"ok": true, "detail": "'%s' produced: %s at %s" % [food_id, made.name, made.global_position]}
|
||||||
|
|
||||||
|
|
||||||
|
func await_clean(plate_name: String) -> Dictionary:
|
||||||
|
var plate := view.find(plate_name)
|
||||||
|
if not plate:
|
||||||
|
return {"ok": false, "detail": "'%s' does not exist on this peer" % plate_name}
|
||||||
|
var pc := plate.get_node_or_null("PlateController")
|
||||||
|
report.log_line(" waiting for the sink to wash %s (dirty=%s)..." % [plate_name, pc.is_dirty if pc else "?"])
|
||||||
|
var ok := await wait_until(func(): return pc and not pc.is_dirty,
|
||||||
|
"the sink to wash the plate", STATION_WORK_TIMEOUT)
|
||||||
|
await wait_frames(30)
|
||||||
|
if not ok:
|
||||||
|
return {"ok": false, "detail": "%s was still dirty after %.0fs in the sink; %s"
|
||||||
|
% [plate_name, STATION_WORK_TIMEOUT, view.diag(plate, hand)]}
|
||||||
|
return {"ok": true, "detail": "%s was washed clean" % plate_name}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cnwwp8f22wwk5
|
||||||
+256
-907
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
|||||||
|
extends Node
|
||||||
|
class_name MpWorldView
|
||||||
|
|
||||||
|
## Finding things in the world under test, and describing what they are doing.
|
||||||
|
##
|
||||||
|
## Nothing here caches a node. Items are consumed and respawned as the run goes
|
||||||
|
## on — the raw burger becomes a cooked burger, which becomes a hamburger, which
|
||||||
|
## is absorbed into a plate — so a held reference is a freed object waiting to
|
||||||
|
## happen. Everything is looked up at the moment it is used.
|
||||||
|
|
||||||
|
var world: Node3D
|
||||||
|
var report: MpReport
|
||||||
|
|
||||||
|
var _despawn_timers_seen := {}
|
||||||
|
|
||||||
|
|
||||||
|
func setup(p_world: Node3D, p_report: MpReport) -> void:
|
||||||
|
world = p_world
|
||||||
|
report = p_report
|
||||||
|
|
||||||
|
|
||||||
|
## Objects live either baked in the scene root or, once spawned at runtime, under
|
||||||
|
## WorldContent. Look in both.
|
||||||
|
func find(name: String) -> Node3D:
|
||||||
|
var n := world.get_node_or_null(name)
|
||||||
|
if not n:
|
||||||
|
n = world.get_node_or_null("WorldContent/" + name)
|
||||||
|
return n as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
func zone_of(station_name: String) -> XRToolsSnapZone:
|
||||||
|
var s := find(station_name)
|
||||||
|
return s.get_node_or_null("XRToolsSnapZone") as XRToolsSnapZone if s else null
|
||||||
|
|
||||||
|
|
||||||
|
## Cooking and combining spawn their results with engine-assigned names, so the
|
||||||
|
## only stable way to find one is by the food id it carries.
|
||||||
|
func find_by_food_id(id: String) -> Node3D:
|
||||||
|
for child in roots_children():
|
||||||
|
var f := child.get_node_or_null("FoodItem") as FoodItem
|
||||||
|
if f and f.id == id and is_instance_valid(child):
|
||||||
|
return child as Node3D
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
func roots_children() -> Array[Node]:
|
||||||
|
var out: Array[Node] = []
|
||||||
|
for root in [world, world.get_node_or_null("WorldContent")]:
|
||||||
|
if not root:
|
||||||
|
continue
|
||||||
|
for child in root.get_children():
|
||||||
|
out.append(child)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
func pickables() -> Array[Node]:
|
||||||
|
var out: Array[Node] = []
|
||||||
|
for child in roots_children():
|
||||||
|
if child is XRToolsPickable and not child.is_queued_for_deletion():
|
||||||
|
out.append(child)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
## Who is currently driving this object's transform.
|
||||||
|
##
|
||||||
|
## Under the high-level model the object's own multiplayer authority never
|
||||||
|
## changes — only its NetXform's does, and that is what "who is holding this"
|
||||||
|
## means now.
|
||||||
|
func xform_authority(item: Node) -> int:
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return 0
|
||||||
|
var xform := item.get_node_or_null(NetReplication.XFORM_NAME)
|
||||||
|
return xform.get_multiplayer_authority() if xform else 0
|
||||||
|
|
||||||
|
|
||||||
|
## The test deliberately leaves objects sitting still for minutes at a time,
|
||||||
|
## which DespawningItem treats as litter and removes — it took the raw burger
|
||||||
|
## away before the client had even connected. Hold them indefinitely instead, so
|
||||||
|
## the run tests the kitchen rather than the despawn timer.
|
||||||
|
##
|
||||||
|
## Re-checked before every step rather than only at startup, because cooking and
|
||||||
|
## combining spawn new objects as the run goes on.
|
||||||
|
func disable_despawn_timers() -> void:
|
||||||
|
var stopped := 0
|
||||||
|
for node in world.find_children("*", "DespawningItem", true, false):
|
||||||
|
if _despawn_timers_seen.has(node.get_instance_id()):
|
||||||
|
continue
|
||||||
|
_despawn_timers_seen[node.get_instance_id()] = true
|
||||||
|
node.set_process(false)
|
||||||
|
stopped += 1
|
||||||
|
if stopped > 0:
|
||||||
|
report.log_line("disabled %d DespawningItem timer(s) so test objects don't vanish mid-run" % stopped)
|
||||||
|
|
||||||
|
|
||||||
|
func diag(item: Node3D, hand: XRToolsFunctionPickup = null) -> String:
|
||||||
|
if not is_instance_valid(item):
|
||||||
|
return "item[freed]"
|
||||||
|
var pc := item.get_node_or_null("PlateController")
|
||||||
|
var s := "%s[pos=%s xform_authority=%d" % [item.name, item.global_position, xform_authority(item)]
|
||||||
|
if item is XRToolsPickable:
|
||||||
|
s += " enabled=%s can_pick_up=%s layer=%d held_by=%s" % [
|
||||||
|
item.enabled, item.can_pick_up(hand) if hand else "?",
|
||||||
|
item.collision_layer, item.get_picked_up_by()]
|
||||||
|
if pc:
|
||||||
|
s += " dirty=%s contains=%s" % [pc.is_dirty, str(pc.contained_ids)]
|
||||||
|
return s + "]"
|
||||||
|
|
||||||
|
|
||||||
|
func dump_state(stations: Array, hand: XRToolsFunctionPickup) -> void:
|
||||||
|
report.log_line(" stations:")
|
||||||
|
for station in stations:
|
||||||
|
var z := zone_of(station)
|
||||||
|
if z:
|
||||||
|
report.log_line(" %-12s zone holds %s (enabled=%s)" % [station, z.picked_up_object, z.enabled])
|
||||||
|
report.log_line(" items:")
|
||||||
|
for child in pickables():
|
||||||
|
report.log_line(" %s" % diag(child as Node3D, hand))
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dxqia06f3e4u4
|
||||||
@@ -34,7 +34,7 @@ size = Vector3(15, 20, 0.1)
|
|||||||
|
|
||||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||||
script = ExtResource("1_6uucx")
|
script = ExtResource("1_6uucx")
|
||||||
populate_from_layout = false
|
populate_from_layout = true
|
||||||
|
|
||||||
[node name="XROrigin3D" parent="." unique_id=2055526621 groups=["local_xr_origin"] instance=ExtResource("2_j7vd1")]
|
[node name="XROrigin3D" parent="." unique_id=2055526621 groups=["local_xr_origin"] instance=ExtResource("2_j7vd1")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
|
||||||
@@ -63,7 +63,6 @@ environment = SubResource("Environment_bvwq1")
|
|||||||
spawn_path = NodePath("../WorldContent")
|
spawn_path = NodePath("../WorldContent")
|
||||||
|
|
||||||
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
||||||
_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
|
|
||||||
spawn_path = NodePath("../Players")
|
spawn_path = NodePath("../Players")
|
||||||
|
|
||||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
||||||
@@ -100,10 +99,10 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0.5, 0)
|
|||||||
script = ExtResource("9_mptst")
|
script = ExtResource("9_mptst")
|
||||||
|
|
||||||
[node name="raw_burger" parent="." unique_id=1675596942 instance=ExtResource("10_51k0c")]
|
[node name="raw_burger" parent="." unique_id=1675596942 instance=ExtResource("10_51k0c")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.28692234, 1.0149999, 0.3505687)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.28692234, 1.0149999, 1.1)
|
||||||
|
|
||||||
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("11_psgbv")]
|
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("11_psgbv")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.40037438, 1.0094403, 0.34120744)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.40037438, 1.0094403, 1.1)
|
||||||
|
|
||||||
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("12_j5uvh")]
|
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("12_j5uvh")]
|
||||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0.5, 0)
|
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1, 0.5, 0)
|
||||||
|
|||||||
@@ -29,27 +29,28 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$proj = Split-Path -Parent $PSScriptRoot
|
$proj = Split-Path -Parent $PSScriptRoot
|
||||||
$scene = "res://test/multiPlayerTest.tscn"
|
$serverScene = "res://test/multiPlayerTest.tscn"
|
||||||
|
$clientScene = "res://Scenes/multiPlayer.tscn"
|
||||||
|
|
||||||
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
||||||
|
|
||||||
function Start-Instance($extraArgs) {
|
function Start-Instance($extraArgs, $sceneFor) {
|
||||||
$a = "--xr-mode off --resolution 900x600 --path `"$proj`" $scene"
|
$a = "--xr-mode off --resolution 900x600 --path `"$proj`" $sceneFor -- --mptest-manual"
|
||||||
if ($extraArgs) { $a += " -- $($extraArgs -join ' ')" }
|
if ($extraArgs) { $a += " $($extraArgs -join ' ')" }
|
||||||
return Start-Process -FilePath $Godot -ArgumentList $a -PassThru
|
return Start-Process -FilePath $Godot -ArgumentList $a -PassThru
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($Solo) {
|
if ($Solo) {
|
||||||
Write-Host "Opening a single offline window (no networking)."
|
Write-Host "Opening a single offline window (no networking)."
|
||||||
$null = Move-GameWindow (Start-Instance $null) 300 100
|
$null = Move-GameWindow (Start-Instance $null $serverScene) 300 100
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Opening SERVER window (left)..."
|
Write-Host "Opening SERVER window (left)..."
|
||||||
$w = Move-GameWindow (Start-Instance @("--server")) 20 60
|
$w = Move-GameWindow (Start-Instance @("--server") $serverScene) 20 60
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
Write-Host "Opening CLIENT window (right)..."
|
Write-Host "Opening CLIENT window (right)..."
|
||||||
$null = Move-GameWindow (Start-Instance @("--join", "127.0.0.1")) (20 + $w + 12) 60
|
$null = Move-GameWindow (Start-Instance @("--join", "127.0.0.1") $clientScene) (20 + $w + 12) 60
|
||||||
|
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Both windows are up. Click one to focus it, then press:"
|
Write-Host "Both windows are up. Click one to focus it, then press:"
|
||||||
|
|||||||
+62
-24
@@ -1,41 +1,69 @@
|
|||||||
# Runs the headless two-instance multiplayer test (test/multiPlayerTest.tscn).
|
# Runs the two-instance multiplayer test and ALWAYS produces a GIF of the run.
|
||||||
#
|
#
|
||||||
# powershell -File test\run_mp_test.ps1
|
# powershell -File test\run_mp_test.ps1
|
||||||
#
|
#
|
||||||
# Starts a server instance and a client instance of the game with --xr-mode off
|
# Starts a server instance and a client instance on test/multiPlayerTest.tscn,
|
||||||
# (SteamVR's OpenXR runtime crashes a headless process), lets test/mp_test_driver.gd
|
# lets test/mp_test_driver.gd drive the scripted kitchen sequence, then writes:
|
||||||
# drive the scripted plate/dirt-station sequence, then prints both logs.
|
# logs\mptest_report.txt - every check, pass/fail, plus failure details
|
||||||
|
# logs\mptest_run.gif - both peers side by side, one frame per step
|
||||||
|
# logs\mptest_{server,client}.log - full step logs
|
||||||
# Exits non-zero if any check failed.
|
# Exits non-zero if any check failed.
|
||||||
|
#
|
||||||
|
# The windows are visible because frame capture needs a real framebuffer: a
|
||||||
|
# headless Godot renders nothing, so there would be no GIF. Use -Headless when
|
||||||
|
# you only want the pass/fail result (faster, but no GIF).
|
||||||
|
|
||||||
param(
|
param(
|
||||||
[string]$Godot = "C:\Users\Aqua Aurora\Desktop\Godot_v4.7-stable_win64.exe",
|
[string]$Godot = "C:\Users\Aqua Aurora\Desktop\Godot_v4.7-stable_win64.exe",
|
||||||
[int]$TimeoutSec = 120
|
[int]$TimeoutSec = 400,
|
||||||
|
# Seconds to pause between steps, and to hold the final state on screen.
|
||||||
|
[double]$Pause = 0.4,
|
||||||
|
[double]$Hold = 3,
|
||||||
|
# Passed through to make_gif.ps1.
|
||||||
|
[int]$HalfWidth = 900,
|
||||||
|
[double]$SecondsPerStep = 1.2,
|
||||||
|
# Skip the windows entirely. No frames are captured, so no GIF is produced.
|
||||||
|
[switch]$Headless
|
||||||
)
|
)
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$proj = Split-Path -Parent $PSScriptRoot
|
$proj = Split-Path -Parent $PSScriptRoot
|
||||||
$scene = "res://test/multiPlayerTest.tscn"
|
# The two peers deliberately load DIFFERENT scenes, mirroring the real game:
|
||||||
|
# the host opens whatever world it is running, the client opens the bare
|
||||||
|
# multiplayer scene and must receive the entire layout over the network. That is
|
||||||
|
# also exactly what a client joining mid-session goes through.
|
||||||
|
$serverScene = "res://test/multiPlayerTest.tscn"
|
||||||
|
$clientScene = "res://Scenes/multiPlayer.tscn"
|
||||||
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
||||||
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
||||||
|
$report = Join-Path $proj "logs/mptest_report.txt"
|
||||||
|
$gif = Join-Path $proj "logs/mptest_run.gif"
|
||||||
|
|
||||||
foreach ($f in @($serverLog, $clientLog)) { if (Test-Path $f) { Remove-Item $f -Force } }
|
foreach ($f in @($serverLog, $clientLog, $report, $gif)) { if (Test-Path $f) { Remove-Item $f -Force } }
|
||||||
|
|
||||||
function Start-Instance($extraArgs) {
|
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
||||||
# Single argument string with the project path quoted: Start-Process does
|
|
||||||
# not quote array elements, so the space in the path would split it.
|
function Start-Instance($extraArgs, $sceneFor) {
|
||||||
$a = "--headless --xr-mode off --path `"$proj`" $scene -- $($extraArgs -join ' ')"
|
# --xr-mode off keeps it on the desktop (SteamVR's OpenXR runtime otherwise
|
||||||
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru -NoNewWindow
|
# takes over, and two instances can't share a headset anyway).
|
||||||
# Touching .Handle caches it, which is what makes .ExitCode readable later;
|
$a = "--xr-mode off"
|
||||||
# without this it comes back empty even after the process has exited.
|
if ($Headless) { $a = "--headless " + $a }
|
||||||
|
$a += " --resolution 900x600 --path `"$proj`" $sceneFor -- $($extraArgs -join ' ') --mptest"
|
||||||
|
if (-not $Headless) { $a += " --mptest-frames" }
|
||||||
|
$a += " --mptest-pause $Pause --mptest-hold $Hold"
|
||||||
|
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru -NoNewWindow:$Headless
|
||||||
|
# Touching .Handle caches it, which is what makes .ExitCode readable later.
|
||||||
$null = $p.Handle
|
$null = $p.Handle
|
||||||
return $p
|
return $p
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Starting server..."
|
Write-Host "Starting SERVER..."
|
||||||
$server = Start-Instance @("--server", "--mptest")
|
$server = Start-Instance @("--server") $serverScene
|
||||||
Start-Sleep -Seconds 4
|
if (-not $Headless) { $w = Move-GameWindow $server 20 60 }
|
||||||
Write-Host "Starting client..."
|
Start-Sleep -Seconds 5
|
||||||
$client = Start-Instance @("--join", "127.0.0.1", "--mptest")
|
Write-Host "Starting CLIENT..."
|
||||||
|
$client = Start-Instance @("--join", "127.0.0.1") $clientScene
|
||||||
|
if (-not $Headless) { $null = Move-GameWindow $client (20 + $w + 12) 60 }
|
||||||
|
|
||||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||||
while ((Get-Date) -lt $deadline -and -not $server.HasExited) { Start-Sleep -Milliseconds 500 }
|
while ((Get-Date) -lt $deadline -and -not $server.HasExited) { Start-Sleep -Milliseconds 500 }
|
||||||
@@ -45,16 +73,26 @@ foreach ($p in @($server, $client)) {
|
|||||||
}
|
}
|
||||||
Start-Sleep -Milliseconds 500
|
Start-Sleep -Milliseconds 500
|
||||||
|
|
||||||
foreach ($pair in @(@("SERVER", $serverLog), @("CLIENT", $clientLog))) {
|
if (-not $Headless) {
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "======================== $($pair[0]) ========================"
|
Write-Host "======================== GIF ========================"
|
||||||
if (Test-Path $pair[1]) { Get-Content $pair[1] -Encoding UTF8 } else { Write-Host "(no log written)" }
|
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "make_gif.ps1") `
|
||||||
|
-HalfWidth $HalfWidth -SecondsPerStep $SecondsPerStep
|
||||||
|
} else {
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host "(headless run: no frames captured, so no GIF - drop -Headless to get one)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ExitCode is only populated on the process object after a WaitForExit() call,
|
Write-Host ""
|
||||||
# even when HasExited is already true - without this it reads back empty.
|
Write-Host "======================== REPORT ========================"
|
||||||
|
if (Test-Path $report) { Get-Content $report -Encoding UTF8 } else { Write-Host "(no report written - the run did not finish)" }
|
||||||
|
|
||||||
$server.WaitForExit(2000) | Out-Null
|
$server.WaitForExit(2000) | Out-Null
|
||||||
$code = if ($server.HasExited) { $server.ExitCode } else { 1 }
|
$code = if ($server.HasExited) { $server.ExitCode } else { 1 }
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
|
Write-Host "report: $report"
|
||||||
|
if (-not $Headless -and (Test-Path $gif)) { Write-Host "gif: $gif" }
|
||||||
|
Write-Host "logs: $serverLog"
|
||||||
|
Write-Host " $clientLog"
|
||||||
Write-Host "server exit code: $code"
|
Write-Host "server exit code: $code"
|
||||||
exit $code
|
exit $code
|
||||||
|
|||||||
@@ -24,7 +24,8 @@ param(
|
|||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$proj = Split-Path -Parent $PSScriptRoot
|
$proj = Split-Path -Parent $PSScriptRoot
|
||||||
$scene = "res://test/multiPlayerTest.tscn"
|
$serverScene = "res://test/multiPlayerTest.tscn"
|
||||||
|
$clientScene = "res://Scenes/multiPlayer.tscn"
|
||||||
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
||||||
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
||||||
|
|
||||||
@@ -32,10 +33,10 @@ foreach ($f in @($serverLog, $clientLog)) { if (Test-Path $f) { Remove-Item $f -
|
|||||||
|
|
||||||
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
||||||
|
|
||||||
function Start-Instance($extraArgs) {
|
function Start-Instance($extraArgs, $sceneFor) {
|
||||||
# --xr-mode off keeps it on the desktop (SteamVR's OpenXR runtime otherwise
|
# --xr-mode off keeps it on the desktop (SteamVR's OpenXR runtime otherwise
|
||||||
# takes over, and two instances can't share a headset anyway).
|
# takes over, and two instances can't share a headset anyway).
|
||||||
$a = "--xr-mode off --resolution 900x600 --path `"$proj`" $scene -- " +
|
$a = "--xr-mode off --resolution 900x600 --path `"$proj`" $sceneFor -- " +
|
||||||
"$($extraArgs -join ' ') --mptest --mptest-frames --mptest-pause $Pause --mptest-hold $Hold"
|
"$($extraArgs -join ' ') --mptest --mptest-frames --mptest-pause $Pause --mptest-hold $Hold"
|
||||||
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru
|
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru
|
||||||
# Touching .Handle caches it, which is what makes .ExitCode readable later.
|
# Touching .Handle caches it, which is what makes .ExitCode readable later.
|
||||||
@@ -44,11 +45,11 @@ function Start-Instance($extraArgs) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Starting SERVER window (left)..."
|
Write-Host "Starting SERVER window (left)..."
|
||||||
$server = Start-Instance @("--server")
|
$server = Start-Instance @("--server") $serverScene
|
||||||
$w = Move-GameWindow $server 20 60
|
$w = Move-GameWindow $server 20 60
|
||||||
Start-Sleep -Seconds 5
|
Start-Sleep -Seconds 5
|
||||||
Write-Host "Starting CLIENT window (right)..."
|
Write-Host "Starting CLIENT window (right)..."
|
||||||
$client = Start-Instance @("--join", "127.0.0.1")
|
$client = Start-Instance @("--join", "127.0.0.1") $clientScene
|
||||||
$null = Move-GameWindow $client (20 + $w + 12) 60
|
$null = Move-GameWindow $client (20 + $w + 12) 60
|
||||||
|
|
||||||
Write-Host "Watch the two windows (each has a fixed camera on the test area)."
|
Write-Host "Watch the two windows (each has a fixed camera on the test area)."
|
||||||
|
|||||||
@@ -0,0 +1,137 @@
|
|||||||
|
@tool
|
||||||
|
extends EditorScript
|
||||||
|
|
||||||
|
## Runs the whole multiplayer test suite from inside the Godot editor.
|
||||||
|
##
|
||||||
|
## HOW TO USE
|
||||||
|
## 1. Open this file in the editor's script panel.
|
||||||
|
## 2. File > Run (Ctrl+Shift+X).
|
||||||
|
##
|
||||||
|
## It launches two copies of the game (server + client) as separate processes,
|
||||||
|
## waits for them to finish, then prints the full pass/fail report into the
|
||||||
|
## Output panel and builds the side-by-side GIF.
|
||||||
|
##
|
||||||
|
## Everything it produces lands in logs/:
|
||||||
|
## mptest_report.txt every check, pass/fail, plus details for failures
|
||||||
|
## mptest_run.gif both peers side by side, one frame per step
|
||||||
|
## mptest_{server,client}.log full step logs
|
||||||
|
##
|
||||||
|
## NOTE: the editor is blocked while the run is in progress (a couple of
|
||||||
|
## minutes). Watch the two game windows to follow along.
|
||||||
|
|
||||||
|
## Give up if the server hasn't finished in this long.
|
||||||
|
const TIMEOUT_SEC := 400
|
||||||
|
## Seconds each step is held, so the run is watchable and the GIF is readable.
|
||||||
|
const STEP_PAUSE := 0.4
|
||||||
|
## Seconds the final state stays on screen before the instances close.
|
||||||
|
const END_HOLD := 3.0
|
||||||
|
|
||||||
|
## The peers load different scenes on purpose, mirroring the real game: the host
|
||||||
|
## opens whatever world it is running, the client opens the bare multiplayer
|
||||||
|
## scene and has to receive the whole layout over the network — the same path a
|
||||||
|
## client joining mid-session takes.
|
||||||
|
const SERVER_SCENE := "res://test/multiPlayerTest.tscn"
|
||||||
|
const CLIENT_SCENE := "res://Scenes/multiPlayer.tscn"
|
||||||
|
|
||||||
|
|
||||||
|
func _run() -> void:
|
||||||
|
var exe := OS.get_executable_path()
|
||||||
|
var project_dir := ProjectSettings.globalize_path("res://").rstrip("/")
|
||||||
|
var logs_dir := ProjectSettings.globalize_path("res://logs")
|
||||||
|
DirAccess.make_dir_recursive_absolute(logs_dir)
|
||||||
|
_clear_previous_results(logs_dir)
|
||||||
|
|
||||||
|
print_rich("[b]Running the multiplayer test suite...[/b]")
|
||||||
|
print(" the editor will be unresponsive until it finishes (~2 minutes)")
|
||||||
|
|
||||||
|
var server_pid := _launch(exe, project_dir, ["--server"], SERVER_SCENE)
|
||||||
|
if server_pid <= 0:
|
||||||
|
push_error("Could not start the server instance")
|
||||||
|
return
|
||||||
|
# Give the host time to come up and open its port before the client dials in.
|
||||||
|
OS.delay_msec(5000)
|
||||||
|
var client_pid := _launch(exe, project_dir, ["--join", "127.0.0.1"], CLIENT_SCENE)
|
||||||
|
if client_pid <= 0:
|
||||||
|
push_error("Could not start the client instance")
|
||||||
|
OS.kill(server_pid)
|
||||||
|
return
|
||||||
|
|
||||||
|
var waited := 0.0
|
||||||
|
while OS.is_process_running(server_pid) and waited < TIMEOUT_SEC:
|
||||||
|
OS.delay_msec(500)
|
||||||
|
waited += 0.5
|
||||||
|
if OS.is_process_running(server_pid):
|
||||||
|
print(" timed out after %ds, stopping the instances" % TIMEOUT_SEC)
|
||||||
|
OS.kill(server_pid)
|
||||||
|
if OS.is_process_running(client_pid):
|
||||||
|
OS.kill(client_pid)
|
||||||
|
OS.delay_msec(500)
|
||||||
|
|
||||||
|
_print_report(logs_dir)
|
||||||
|
_build_gif(project_dir, logs_dir)
|
||||||
|
|
||||||
|
|
||||||
|
func _launch(exe: String, project_dir: String, extra: Array, scene: String) -> int:
|
||||||
|
var args := PackedStringArray([
|
||||||
|
"--xr-mode", "off", "--resolution", "900x600",
|
||||||
|
"--path", project_dir, scene, "--",
|
||||||
|
])
|
||||||
|
for a in extra:
|
||||||
|
args.append(a)
|
||||||
|
# --mptest runs the scripted sequence; --mptest-frames captures the frame per
|
||||||
|
# step that the GIF is stitched from (needs a real window, hence no
|
||||||
|
# --headless here).
|
||||||
|
args.append_array(PackedStringArray([
|
||||||
|
"--mptest", "--mptest-frames",
|
||||||
|
"--mptest-pause", str(STEP_PAUSE), "--mptest-hold", str(END_HOLD),
|
||||||
|
]))
|
||||||
|
return OS.create_process(exe, args)
|
||||||
|
|
||||||
|
|
||||||
|
# Remove the previous run's output so a failed launch can't leave stale results
|
||||||
|
# looking like this run's.
|
||||||
|
func _clear_previous_results(logs_dir: String) -> void:
|
||||||
|
for name in ["mptest_report.txt", "mptest_run.gif", "mptest_server.log", "mptest_client.log"]:
|
||||||
|
DirAccess.remove_absolute(logs_dir.path_join(name))
|
||||||
|
for role in ["server", "client"]:
|
||||||
|
var frames := logs_dir.path_join("mptest_frames_%s" % role)
|
||||||
|
var dir := DirAccess.open(frames)
|
||||||
|
if dir:
|
||||||
|
for f in dir.get_files():
|
||||||
|
if f.ends_with(".png"):
|
||||||
|
dir.remove(f)
|
||||||
|
|
||||||
|
|
||||||
|
func _print_report(logs_dir: String) -> void:
|
||||||
|
var path := logs_dir.path_join("mptest_report.txt")
|
||||||
|
if not FileAccess.file_exists(path):
|
||||||
|
push_error("No report at %s — the run did not finish. Check logs/mptest_server.log" % path)
|
||||||
|
return
|
||||||
|
var text := FileAccess.get_file_as_string(path)
|
||||||
|
print("")
|
||||||
|
# Colour the summary so a failure is obvious in the Output panel.
|
||||||
|
for line in text.split("\n"):
|
||||||
|
if line.begins_with("FAIL") or line.contains("RESULT: FAILED"):
|
||||||
|
print_rich("[color=red]%s[/color]" % line)
|
||||||
|
elif line.contains("ALL CHECKS PASSED"):
|
||||||
|
print_rich("[color=green][b]%s[/b][/color]" % line)
|
||||||
|
elif line.begins_with("PASS"):
|
||||||
|
print_rich("[color=gray]%s[/color]" % line)
|
||||||
|
else:
|
||||||
|
print(line)
|
||||||
|
print("report: %s" % path)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_gif(project_dir: String, logs_dir: String) -> void:
|
||||||
|
var script := project_dir.path_join("test/make_gif.ps1")
|
||||||
|
var out := []
|
||||||
|
var code := OS.execute("powershell", [
|
||||||
|
"-ExecutionPolicy", "Bypass", "-File", script,
|
||||||
|
], out, true)
|
||||||
|
for line in out:
|
||||||
|
print(line)
|
||||||
|
var gif := logs_dir.path_join("mptest_run.gif")
|
||||||
|
if code == 0 and FileAccess.file_exists(gif):
|
||||||
|
print_rich("[b]gif:[/b] %s" % gif)
|
||||||
|
else:
|
||||||
|
push_warning("GIF was not produced (is ffmpeg on PATH?). See the output above.")
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://crcuhy3n8goep
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
extends Node
|
||||||
|
|
||||||
|
## Throwaway probe: prints how GDScript reports typed-array properties, so
|
||||||
|
## NetReplication's filter can exclude Array[Node3D] / Array[FoodItem] without
|
||||||
|
## guessing at the hint format.
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
for path in [
|
||||||
|
"res://Containers/container.gd",
|
||||||
|
"res://Containers/plate_controller.gd",
|
||||||
|
"res://Stations/sink.gd",
|
||||||
|
"res://Stations/item_dispenser.gd",
|
||||||
|
"res://Stations/hob.gd",
|
||||||
|
]:
|
||||||
|
var script: Script = load(path)
|
||||||
|
print("=== ", path)
|
||||||
|
for prop in script.get_script_property_list():
|
||||||
|
if not (prop["usage"] & PROPERTY_USAGE_SCRIPT_VARIABLE):
|
||||||
|
continue
|
||||||
|
print(" name=%s type=%d hint=%d hint_string=%s" % [
|
||||||
|
prop["name"], prop["type"], prop["hint"], prop["hint_string"],
|
||||||
|
])
|
||||||
|
print("TYPE_ARRAY=%d TYPE_OBJECT=%d" % [TYPE_ARRAY, TYPE_OBJECT])
|
||||||
|
get_tree().quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ctsonlf12536k
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bspike0probe0"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://test/spike/probe.gd" id="1_probe"]
|
||||||
|
|
||||||
|
[node name="Probe" type="Node"]
|
||||||
|
script = ExtResource("1_probe")
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
## Spike fixture only. A plain scene with NO authored MultiplayerSynchronizer,
|
||||||
|
## so the run proves the runtime-generated one is what carries the state.
|
||||||
|
|
||||||
|
var health: int = 0
|
||||||
|
var label: String = ""
|
||||||
|
var _private_should_not_replicate: int = 99
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cbgmii1peyxu8
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bspike0item00"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://test/spike/spike_item.gd" id="1_spike"]
|
||||||
|
|
||||||
|
[node name="SpikeItem" type="Node3D"]
|
||||||
|
script = ExtResource("1_spike")
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
extends Node3D
|
||||||
|
|
||||||
|
## Throwaway spike. Answers the questions about Godot's high-level replication
|
||||||
|
## that the rebuild depends on, before any of the real code is rewritten.
|
||||||
|
##
|
||||||
|
## Q1 Does default (auto) MultiplayerSpawner replication carry the spawn
|
||||||
|
## properties of synchronizers attached AT RUNTIME rather than baked into
|
||||||
|
## the .tscn? [CONFIRMED]
|
||||||
|
## Q2 Do ON_CHANGE properties reach a peer that joins AFTER the value was
|
||||||
|
## set, and do later changes propagate? [CONFIRMED]
|
||||||
|
## Q3 Can set_visibility_for(peer, false) suppress a peer's updates without
|
||||||
|
## despawning the node on it? [REFUTED — the
|
||||||
|
## node is despawned and respawned on every visibility flip, so this
|
||||||
|
## cannot be used to stop the server fighting a client's held object.]
|
||||||
|
## Q4 Does handing ONE of a node's two synchronizers (NetXform) to a client
|
||||||
|
## let that client drive the transform locally — ignoring the server's
|
||||||
|
## inbound updates — while the other (NetSync) keeps pushing server-owned
|
||||||
|
## state to it? This is the grab-prediction mechanism.
|
||||||
|
##
|
||||||
|
## Both peers run on one ABSOLUTE clock (see _at) rather than chains of relative
|
||||||
|
## timers: the first version of this spike drifted enough that the client's
|
||||||
|
## reports straddled the server's actions and read as inconclusive.
|
||||||
|
|
||||||
|
const PORT := 24599
|
||||||
|
const ITEM := "res://test/spike/spike_item.tscn"
|
||||||
|
|
||||||
|
var _log: FileAccess
|
||||||
|
var _is_server := false
|
||||||
|
var _start_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
var args := OS.get_cmdline_user_args()
|
||||||
|
_is_server = args.has("--server")
|
||||||
|
_start_ms = Time.get_ticks_msec()
|
||||||
|
_open_log()
|
||||||
|
|
||||||
|
# Registered identically on both peers BEFORE connecting: auto-spawn sends an
|
||||||
|
# index into this array, so a mismatch builds the wrong scene on the client.
|
||||||
|
$Spawner.add_spawnable_scene(ITEM)
|
||||||
|
|
||||||
|
# The hook under test: every peer attaches the generated synchronizers as the
|
||||||
|
# node enters the tree. On the server that is our own add_child; on the
|
||||||
|
# client it is MultiplayerSpawner's.
|
||||||
|
$Content.child_entered_tree.connect(_on_content_child)
|
||||||
|
|
||||||
|
if _is_server:
|
||||||
|
_run_server()
|
||||||
|
else:
|
||||||
|
_run_client()
|
||||||
|
|
||||||
|
|
||||||
|
func _on_content_child(node: Node) -> void:
|
||||||
|
if node is MultiplayerSynchronizer:
|
||||||
|
return
|
||||||
|
NetReplication.attach(node)
|
||||||
|
_say("attached synchronizers to %s" % node.name)
|
||||||
|
|
||||||
|
|
||||||
|
## Waits until `t` seconds after this scene started. Both peers start within a
|
||||||
|
## few hundred ms of each other, which is close enough to interleave their
|
||||||
|
## actions and observations deterministically.
|
||||||
|
func _at(t: float) -> void:
|
||||||
|
var target := _start_ms + int(t * 1000.0)
|
||||||
|
while Time.get_ticks_msec() < target:
|
||||||
|
await get_tree().process_frame
|
||||||
|
|
||||||
|
|
||||||
|
## set_multiplayer_authority is a local call, so it has to run on every peer for
|
||||||
|
## them to agree on who is driving the transform.
|
||||||
|
@rpc("authority", "call_local", "reliable")
|
||||||
|
func _set_xform_authority(path: NodePath, peer: int) -> void:
|
||||||
|
var node := get_node_or_null(path)
|
||||||
|
if not node:
|
||||||
|
return
|
||||||
|
node.get_node(NetReplication.XFORM_NAME).set_multiplayer_authority(peer)
|
||||||
|
_say("NetXform authority of %s -> peer %d" % [node.name, peer])
|
||||||
|
|
||||||
|
|
||||||
|
func _item() -> Node3D:
|
||||||
|
return $Content.get_node_or_null("SpikeItem") as Node3D
|
||||||
|
|
||||||
|
|
||||||
|
# --- server ----------------------------------------------------------------
|
||||||
|
|
||||||
|
func _run_server() -> void:
|
||||||
|
var peer := ENetMultiplayerPeer.new()
|
||||||
|
var err := peer.create_server(PORT, 4)
|
||||||
|
if err != OK:
|
||||||
|
_say("FATAL: create_server failed: %s" % error_string(err))
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
_say("server up on %d" % PORT)
|
||||||
|
|
||||||
|
# t=1: spawn BEFORE anyone joins, so the client receives it purely as a
|
||||||
|
# late-joiner replay — the same path as joining a running game.
|
||||||
|
await _at(1.0)
|
||||||
|
var item: Node3D = load(ITEM).instantiate()
|
||||||
|
item.name = "SpikeItem"
|
||||||
|
item.position = Vector3(5, 6, 7)
|
||||||
|
item.health = 42
|
||||||
|
item.label = "hello"
|
||||||
|
$Content.add_child(item)
|
||||||
|
_say("spawned SpikeItem at %s health=%d label=%s" % [item.position, item.health, item.label])
|
||||||
|
|
||||||
|
await _at(7.0)
|
||||||
|
if multiplayer.get_peers().is_empty():
|
||||||
|
_say("FATAL: no client connected by t=7")
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
var client: int = multiplayer.get_peers()[0]
|
||||||
|
_say("client connected: %d" % client)
|
||||||
|
|
||||||
|
# t=8: change an ON_CHANGE value well after the client joined (Q2b).
|
||||||
|
await _at(8.0)
|
||||||
|
item.health = 7
|
||||||
|
item.label = "changed"
|
||||||
|
_say("changed health=7 label=changed")
|
||||||
|
|
||||||
|
# t=12: hand the transform to the client, as if it had just grabbed the item.
|
||||||
|
await _at(12.0)
|
||||||
|
_set_xform_authority.rpc(item.get_path(), client)
|
||||||
|
|
||||||
|
# t=13: the server now tries to move it and also changes server-owned state.
|
||||||
|
# The move must NOT reach the client (it owns the transform); the state must.
|
||||||
|
await _at(13.0)
|
||||||
|
item.position = Vector3(-9, -9, -9)
|
||||||
|
item.health = 123
|
||||||
|
_say("server set pos=%s health=123 while the CLIENT owns NetXform" % item.position)
|
||||||
|
|
||||||
|
await _at(15.5)
|
||||||
|
_say("Q4 server's view of item pos (expect the CLIENT's 1,1,1) -> %s" % item.position)
|
||||||
|
|
||||||
|
# t=17: take it back, as if the client had released it, and move it.
|
||||||
|
await _at(17.0)
|
||||||
|
_set_xform_authority.rpc(item.get_path(), 1)
|
||||||
|
await _at(17.5)
|
||||||
|
item.position = Vector3(-3, -3, -3)
|
||||||
|
_say("server reclaimed NetXform and set pos=%s" % item.position)
|
||||||
|
|
||||||
|
# Stay alive past the client's last report, so a shutdown-induced despawn
|
||||||
|
# can't be mistaken for anything else.
|
||||||
|
await _at(24.0)
|
||||||
|
_say("server done")
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
# --- client ----------------------------------------------------------------
|
||||||
|
|
||||||
|
func _run_client() -> void:
|
||||||
|
# Deliberately late, so the spawn is replayed rather than observed live.
|
||||||
|
await _at(5.0)
|
||||||
|
var peer := ENetMultiplayerPeer.new()
|
||||||
|
var err := peer.create_client("127.0.0.1", PORT)
|
||||||
|
if err != OK:
|
||||||
|
_say("FATAL: create_client failed: %s" % error_string(err))
|
||||||
|
_finish()
|
||||||
|
return
|
||||||
|
multiplayer.multiplayer_peer = peer
|
||||||
|
_say("client connecting...")
|
||||||
|
|
||||||
|
await _at(7.5)
|
||||||
|
_report("Q1/Q2a late-join spawn (expect pos=5,6,7 health=42 label=hello)")
|
||||||
|
|
||||||
|
await _at(10.0)
|
||||||
|
_report("Q2b after change (expect health=7 label=changed)")
|
||||||
|
|
||||||
|
# t=13.5: we now own NetXform. Drive the item ourselves, the way a held
|
||||||
|
# object's grab driver would, and see whether the server's competing write
|
||||||
|
# from t=13 gets ignored here and whether ours reaches the server.
|
||||||
|
await _at(13.5)
|
||||||
|
var item := _item()
|
||||||
|
if item:
|
||||||
|
item.position = Vector3(1, 1, 1)
|
||||||
|
_say("client set pos=(1,1,1) while owning NetXform")
|
||||||
|
|
||||||
|
await _at(15.0)
|
||||||
|
_report("Q4 client owns NetXform (expect pos=1,1,1 — NOT -9; health=123 — server state still arrives)")
|
||||||
|
|
||||||
|
await _at(20.0)
|
||||||
|
_report("Q4b server reclaimed (expect pos=-3,-3,-3)")
|
||||||
|
|
||||||
|
_say("client done")
|
||||||
|
_finish()
|
||||||
|
|
||||||
|
|
||||||
|
func _report(what: String) -> void:
|
||||||
|
var item := _item()
|
||||||
|
if not item:
|
||||||
|
_say("%s -> NO NODE (item absent on client)" % what)
|
||||||
|
return
|
||||||
|
_say("%s -> pos=%s health=%d label=%s private=%d" % [
|
||||||
|
what, item.position, item.health, item.label,
|
||||||
|
item._private_should_not_replicate,
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
|
# --- logging ---------------------------------------------------------------
|
||||||
|
|
||||||
|
func _open_log() -> void:
|
||||||
|
var who := "server" if _is_server else "client"
|
||||||
|
var dir := ProjectSettings.globalize_path("res://logs")
|
||||||
|
DirAccess.make_dir_recursive_absolute(dir)
|
||||||
|
_log = FileAccess.open(dir.path_join("spike_%s.log" % who), FileAccess.WRITE)
|
||||||
|
|
||||||
|
|
||||||
|
func _say(s: String) -> void:
|
||||||
|
var line := "[%s t=%5.1f] %s" % [
|
||||||
|
"SERVER" if _is_server else "CLIENT",
|
||||||
|
(Time.get_ticks_msec() - _start_ms) / 1000.0, s,
|
||||||
|
]
|
||||||
|
print(line)
|
||||||
|
if _log:
|
||||||
|
_log.store_line(line)
|
||||||
|
_log.flush()
|
||||||
|
|
||||||
|
|
||||||
|
func _finish() -> void:
|
||||||
|
if _log:
|
||||||
|
_log.flush()
|
||||||
|
get_tree().quit()
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bh6fdnthnelfa
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[gd_scene load_steps=2 format=3 uid="uid://bspike0world0"]
|
||||||
|
|
||||||
|
[ext_resource type="Script" path="res://test/spike/spike_world.gd" id="1_spikew"]
|
||||||
|
|
||||||
|
[node name="SpikeWorld" type="Node3D"]
|
||||||
|
script = ExtResource("1_spikew")
|
||||||
|
|
||||||
|
[node name="Content" type="Node3D" parent="."]
|
||||||
|
|
||||||
|
[node name="Spawner" type="MultiplayerSpawner" parent="."]
|
||||||
|
spawn_path = NodePath("../Content")
|
||||||
Reference in New Issue
Block a user