Compare commits
23 Commits
265dd12b37
...
multi2
| Author | SHA1 | Date | |
|---|---|---|---|
| 34a649f507 | |||
| e04c5519d0 | |||
| 7ac87984bc | |||
| 776aaa3020 | |||
| 5ca64b8dff | |||
| ae3e7ec674 | |||
| 23ec41c1d6 | |||
| 33ef81306d | |||
| 50efeeb353 | |||
| 71d01e5be1 | |||
| 87641dd55c | |||
| d65b2ca863 | |||
| 0af0c1bfc1 | |||
| 428f00a812 | |||
| 773cf647d8 | |||
| 61ea80b933 | |||
| f6ac103233 | |||
| 17d20440da | |||
| a9ca11fd17 | |||
| 13567e5dd3 | |||
| 4ca0a05d1b | |||
| 596d777fae | |||
| f7024db98d |
@@ -1,3 +1,7 @@
|
||||
# Godot 4+ specific ignores
|
||||
.godot/
|
||||
.build/
|
||||
/android/
|
||||
/logs
|
||||
|
||||
*.log
|
||||
|
||||
+128
-36
@@ -24,7 +24,11 @@ func _ready() -> void:
|
||||
push_error("XRPickable node not found in container.gd")
|
||||
|
||||
|
||||
# Absorbing items is a server decision (the container's holder is server-
|
||||
# snapped into a station, matching table.gd/hob.gd's convention).
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
print("Container enabled: ", enabled)
|
||||
if not enabled:
|
||||
print("Container disabled in _on_body_entered body")
|
||||
@@ -38,63 +42,151 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
# If compatible and enough space, add item
|
||||
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])
|
||||
if food_item.type == FoodItem.Type.MEAL and _meal_container.get_child_count() < meal_positions.size():
|
||||
var meal_count := contained_items.filter(func(f): return f.type == FoodItem.Type.MEAL).size()
|
||||
var side_count := contained_items.filter(func(f): return f.type == FoodItem.Type.SIDE).size()
|
||||
if food_item.type == FoodItem.Type.MEAL and meal_count < meal_positions.size():
|
||||
print("Container adding meal")
|
||||
_add_item(body, meal_positions, _meal_container)
|
||||
if food_item.type == FoodItem.Type.SIDE and _side_container.get_child_count() < side_positions.size():
|
||||
_add_item(body)
|
||||
if food_item.type == FoodItem.Type.SIDE and side_count < side_positions.size():
|
||||
print("Container adding side")
|
||||
_add_item(body, side_positions, _side_container)
|
||||
_add_item(body)
|
||||
|
||||
|
||||
func _add_item(item: Node3D, positions: Array[Node3D], container_root: Node3D) -> void:
|
||||
# Contents are synced as data (ids on the plate's PlateController), not
|
||||
# reparented nodes: reparenting a MultiplayerSpawner-tracked item out of
|
||||
# WorldContent would despawn it on every client the instant it happened.
|
||||
func _add_item(item: Node3D) -> void:
|
||||
var pickable = item as XRToolsPickable
|
||||
var rigidbody = item as RigidBody3D
|
||||
|
||||
# Drop item
|
||||
if pickable:
|
||||
if pickable.is_picked_up():
|
||||
if pickable and pickable.is_picked_up():
|
||||
pickable.drop()
|
||||
pickable.enabled = false
|
||||
|
||||
# Freeze the rigid body and disable its collisions so it doesn't fight the container
|
||||
if rigidbody:
|
||||
rigidbody.freeze = true
|
||||
rigidbody.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||
rigidbody.process_mode = PROCESS_MODE_DISABLED
|
||||
# Optional: Disable collision layer/mask so it doesn't bump into other food
|
||||
rigidbody.collision_layer = 0
|
||||
rigidbody.collision_mask = 0
|
||||
var food_node := item.get_node_or_null("FoodItem") as FoodItem
|
||||
if not food_node:
|
||||
return
|
||||
|
||||
item.reparent(container_root, false) # false = discard global transform
|
||||
# Copy the data out before despawning the real item.
|
||||
var data := FoodItem.new()
|
||||
data.id = food_node.id
|
||||
data.type = food_node.type
|
||||
data.sell_value = food_node.sell_value
|
||||
contained_items.append(data)
|
||||
|
||||
# Get target position index
|
||||
var target_slot_index = container_root.get_child_count() - 1
|
||||
if target_slot_index < positions.size():
|
||||
item.position = positions[target_slot_index].position
|
||||
item.rotation = positions[target_slot_index].rotation
|
||||
var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController
|
||||
if plate_controller:
|
||||
# Reassign rather than append in place. contained_ids has a setter that
|
||||
# rebuilds the plate's visuals, and mutating the array never triggers it —
|
||||
# so the peer that actually added the food was the one peer that never
|
||||
# redrew the plate. Remote peers looked right (the synchronizer assigns
|
||||
# the value there, which does fire the setter), and the stale peer only
|
||||
# caught up if someone else took the plate and sent the value back.
|
||||
# duplicate() keeps the Array[String] typing that the property requires.
|
||||
var updated := plate_controller.contained_ids.duplicate()
|
||||
updated.append(food_node.id)
|
||||
plate_controller.contained_ids = updated
|
||||
|
||||
# Add to list
|
||||
var food_node = item.get_node_or_null("FoodItem")
|
||||
if food_node:
|
||||
contained_items.append(food_node as FoodItem)
|
||||
NetworkManager.despawn_item(item)
|
||||
|
||||
|
||||
func erase_item(item: FoodItem) -> void:
|
||||
for container_root in [_meal_container, _side_container]:
|
||||
for child in container_root.get_children():
|
||||
var food_item = child.get_node_or_null("FoodItem") as FoodItem
|
||||
if food_item.id == item.id:
|
||||
contained_items.erase(item)
|
||||
child.queue_free()
|
||||
var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController
|
||||
if plate_controller:
|
||||
# Same reason as _add_item: assign so the visuals actually refresh.
|
||||
var remaining := plate_controller.contained_ids.duplicate()
|
||||
remaining.erase(item.id)
|
||||
plate_controller.contained_ids = remaining
|
||||
|
||||
|
||||
func clear() -> void:
|
||||
# delete all nodes in containers and contained_items
|
||||
contained_items.clear()
|
||||
var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController
|
||||
if plate_controller:
|
||||
plate_controller.contained_ids = []
|
||||
|
||||
|
||||
## Rebuilds the purely-cosmetic visual representation of the plate's contents
|
||||
## from a synced id list. Runs on every peer (called from PlateController
|
||||
## whenever contained_ids changes, whether set locally or by the network).
|
||||
func refresh_visuals(ids: Array[String]) -> void:
|
||||
for child in _meal_container.get_children():
|
||||
child.queue_free()
|
||||
for child in _side_container.get_children():
|
||||
child.queue_free()
|
||||
contained_items.clear()
|
||||
|
||||
var meal_idx := 0
|
||||
var side_idx := 0
|
||||
for id in ids:
|
||||
var scene := RecipeManager.get_item_scene(id)
|
||||
if not scene:
|
||||
continue
|
||||
var visual := scene.instantiate()
|
||||
var food_node := visual.get_node_or_null("FoodItem") as FoodItem
|
||||
|
||||
var container_root: Node3D
|
||||
var positions: Array[Node3D]
|
||||
var idx: int
|
||||
if food_node and food_node.type == FoodItem.Type.MEAL and meal_idx < meal_positions.size():
|
||||
container_root = _meal_container
|
||||
positions = meal_positions
|
||||
idx = meal_idx
|
||||
meal_idx += 1
|
||||
elif food_node and food_node.type == FoodItem.Type.SIDE and side_idx < side_positions.size():
|
||||
container_root = _side_container
|
||||
positions = side_positions
|
||||
idx = side_idx
|
||||
side_idx += 1
|
||||
else:
|
||||
visual.queue_free()
|
||||
continue
|
||||
|
||||
_make_cosmetic(visual)
|
||||
container_root.add_child(visual)
|
||||
# Must happen after add_child: entering the world is what puts the body
|
||||
# into the physics space, so it can only be taken out again afterwards.
|
||||
_remove_from_physics(visual)
|
||||
visual.position = positions[idx].position
|
||||
visual.rotation = positions[idx].rotation
|
||||
|
||||
|
||||
# Strip interactivity/networking from a display-only copy: it's not spawned
|
||||
# through NetworkManager, so it must never try to sync (its NetPickable child,
|
||||
# if any, would have no corresponding replicated identity on other peers) or
|
||||
# be grabbable/collidable.
|
||||
func _make_cosmetic(visual: Node3D) -> void:
|
||||
var net_pickable := visual.get_node_or_null("NetPickable")
|
||||
if net_pickable:
|
||||
# Detach and free it outright rather than queue_free(): this runs before
|
||||
# `visual` is added to the tree, and a merely-queued node still enters the
|
||||
# tree with its parent and runs _ready() (which starts syncing and logging)
|
||||
# before the queued deletion lands at the end of the frame.
|
||||
visual.remove_child(net_pickable)
|
||||
net_pickable.free()
|
||||
if visual is RigidBody3D:
|
||||
visual.freeze = true
|
||||
# STATIC, not KINEMATIC: a kinematic body is still driven by the physics
|
||||
# engine (see _remove_from_physics), and "static decoration" is what this
|
||||
# actually is.
|
||||
visual.freeze_mode = RigidBody3D.FREEZE_MODE_STATIC
|
||||
visual.collision_layer = 0
|
||||
visual.collision_mask = 0
|
||||
if visual is XRToolsPickable:
|
||||
visual.enabled = false
|
||||
visual.set_process(false)
|
||||
visual.set_physics_process(false)
|
||||
|
||||
|
||||
# Take a display-only copy out of the physics simulation completely.
|
||||
#
|
||||
# Freezing is not enough. Under Jolt (this project's physics engine) a frozen
|
||||
# KINEMATIC RigidBody3D is still simulated: it is driven toward its target
|
||||
# transform by velocity rather than being teleported. Parented to a plate that
|
||||
# gets picked up and carried, it therefore lags behind, keeps its velocity, and
|
||||
# overshoots — so the food visibly slid off the plate and ended up metres away,
|
||||
# differently on each peer since each simulates its own copy. A body with no
|
||||
# space is never touched by the engine, so it simply follows its parent.
|
||||
func _remove_from_physics(visual: Node3D) -> void:
|
||||
if visual is RigidBody3D:
|
||||
PhysicsServer3D.body_set_space((visual as RigidBody3D).get_rid(), RID())
|
||||
|
||||
|
||||
#plate (Pickalbe)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[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="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"]
|
||||
height = 0.0635376
|
||||
@@ -26,6 +27,23 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_vlqg6"]
|
||||
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]
|
||||
collision_layer = 4
|
||||
collision_mask = 196615
|
||||
@@ -134,3 +152,8 @@ transform = Transform3D(0.21189868, 0, 0, 0, -9.2623855e-09, 0.21189868, 0, -0.2
|
||||
polygon = PackedVector2Array(-0.053057775, 0.2586278, 0.09469998, 0.40791017, 0.31746316, 0.26044407, 0.4514293, -0.05809097, 0.105977096, -0.11683442, -4.3913722e-05, 0.02302903)
|
||||
depth = 0.02
|
||||
material = SubResource("StandardMaterial3D_vlqg6")
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_plate")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
@@ -6,6 +6,13 @@ extends Node
|
||||
|
||||
@export var is_dirty: bool = false
|
||||
|
||||
## Synced plate contents (item ids), replacing reparented child nodes so
|
||||
## contents survive replication (a MultiplayerSpawner-tracked item would
|
||||
## despawn on every client the instant it was reparented out of WorldContent).
|
||||
## Server writes it via ItemContainer; every peer (server included) renders
|
||||
## the cosmetic result via the setter below.
|
||||
@export var contained_ids: Array[String] = []: set = _set_contained_ids
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if not dirty_node:
|
||||
@@ -20,3 +27,22 @@ func _process(_delta: float) -> void:
|
||||
else:
|
||||
container.enabled = true
|
||||
dirty_node.visible = false
|
||||
|
||||
|
||||
func _set_contained_ids(value: Array[String]) -> void:
|
||||
# Only rebuild when the contents actually changed. This property is
|
||||
# replicated in ALWAYS mode, so the synchronizer assigns it every network
|
||||
# tick on every peer that doesn't own the plate — and refresh_visuals()
|
||||
# frees and re-instantiates a scene per item each time. That was thousands
|
||||
# of throwaway nodes per run (and a log line from each one's NetPickable).
|
||||
if contained_ids == value:
|
||||
return
|
||||
contained_ids = value
|
||||
# Deferred: this can be written by the replicated spawn payload before
|
||||
# this node's own @onready vars (container) have resolved.
|
||||
_refresh_visuals.call_deferred()
|
||||
|
||||
|
||||
func _refresh_visuals() -> void:
|
||||
if container:
|
||||
container.refresh_visuals(contained_ids)
|
||||
|
||||
@@ -22,3 +22,4 @@ static func get_random_side() -> String:
|
||||
var rand_index = randi() % sides_in_play.size()
|
||||
print("GameManager: get_random_side() returning ", sides_in_play[rand_index])
|
||||
return sides_in_play[rand_index]
|
||||
|
||||
|
||||
@@ -8,6 +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="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="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_vlqg6"]
|
||||
height = 0.10708985
|
||||
@@ -26,6 +27,17 @@ metadata/_custom_type_script = "uid://dvobm6vcfnqe8"
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_hoqox"]
|
||||
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="CollisionShape3D" parent="." index="0"]
|
||||
@@ -72,3 +84,8 @@ id = "burger_buns"
|
||||
type = 2
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_wb51u")]
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_buns")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
+17
-1
@@ -8,9 +8,9 @@
|
||||
[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://3lr2dhy62rhk" path="res://Prefabs/combinable_item.tscn" id="8_wmrff"]
|
||||
[ext_resource type="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_gkni8"]
|
||||
custom_solver_bias = 0.1
|
||||
height = 0.1
|
||||
radius = 0.1
|
||||
|
||||
@@ -33,6 +33,17 @@ script = ExtResource("4_mgacb")
|
||||
closed_pose = ExtResource("6_cp3eg")
|
||||
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="CollisionShape3D" parent="." index="0"]
|
||||
@@ -54,3 +65,8 @@ id = "charcoal"
|
||||
type = 2
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("8_wmrff")]
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_charcoal")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[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://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"]
|
||||
size = Vector3(0.1, 0.1, 0.1)
|
||||
@@ -29,6 +30,17 @@ script = ExtResource("4_hc3f7")
|
||||
closed_pose = ExtResource("6_bdp75")
|
||||
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="CollisionShape3D" parent="." index="0"]
|
||||
@@ -50,3 +62,8 @@ id = "cube"
|
||||
type = 1
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("7_bdp75")]
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_cube")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[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="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"]
|
||||
height = 0.0338974
|
||||
@@ -26,6 +27,17 @@ script = ExtResource("5_w8sii")
|
||||
closed_pose = ExtResource("7_wqxjj")
|
||||
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="CollisionShape3D" parent="." index="0"]
|
||||
@@ -49,3 +61,8 @@ hand_pose = SubResource("Resource_qyiot")
|
||||
[node name="FoodItem" parent="." index="4" unique_id=63948206 instance=ExtResource("10_yxtxs")]
|
||||
id = "raw_burger"
|
||||
type = 2
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_burger")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[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://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="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||
height = 0.04777527
|
||||
@@ -26,6 +27,17 @@ script = ExtResource("5_ychvb")
|
||||
closed_pose = ExtResource("7_rl64h")
|
||||
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="CollisionShape3D" parent="." index="0"]
|
||||
@@ -51,3 +63,8 @@ id = "cooked_burger"
|
||||
type = 2
|
||||
|
||||
[node name="CombinableItem" parent="." index="5" unique_id=996936271 instance=ExtResource("10_ut7mg")]
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_cookedburger")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
@@ -7,6 +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="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="Script" path="res://Net/net_pickable.gd" id="20_netpk"]
|
||||
|
||||
[sub_resource type="CylinderShape3D" id="CylinderShape3D_fco8w"]
|
||||
height = 0.10392761
|
||||
@@ -28,6 +29,17 @@ albedo_color = Color(0.6784191, 0.54546416, 0.016760282, 1)
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_6l01i"]
|
||||
albedo_color = Color(0.29, 0.101500005, 0, 1)
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_hamburger"]
|
||||
properties/0/path = NodePath(".:position")
|
||||
properties/0/spawn = false
|
||||
properties/0/replication_mode = 1
|
||||
properties/1/path = NodePath(".:quaternion")
|
||||
properties/1/spawn = false
|
||||
properties/1/replication_mode = 1
|
||||
properties/2/path = NodePath("NetPickable:net_held_by")
|
||||
properties/2/spawn = true
|
||||
properties/2/replication_mode = 1
|
||||
|
||||
[node name="Hamburger" unique_id=1675596942 groups=["platalbe_item"] instance=ExtResource("1_3gf3l")]
|
||||
gravity_scale = 0.04
|
||||
|
||||
@@ -81,3 +93,8 @@ material = SubResource("StandardMaterial3D_6l01i")
|
||||
id = "hamburger"
|
||||
type = 0
|
||||
sell_value = 4
|
||||
|
||||
[node name="NetPickable" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_hamburger")
|
||||
script = ExtResource("20_netpk")
|
||||
|
||||
@@ -0,0 +1,209 @@
|
||||
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()]
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwd0pe2udb5xo
|
||||
@@ -0,0 +1,548 @@
|
||||
extends Node
|
||||
|
||||
## Client-server session manager for VRyHungry (listen-server model).
|
||||
##
|
||||
## Registered as the "NetworkManager" autoload. Owns transport (ENet), tracks
|
||||
## the session, and is the single place that reassigns multiplayer authority
|
||||
## (only the server does so). The world scene (main.gd) registers its spawners
|
||||
## here via [method register_world]; higher layers (players, items, stations)
|
||||
## build on top of this in later phases.
|
||||
|
||||
const DEFAULT_PORT := 24565
|
||||
const MAX_CLIENTS := 7
|
||||
|
||||
## 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
|
||||
## uses it to spawn that peer's player.
|
||||
signal player_joined(peer_id: int)
|
||||
signal player_left(peer_id: int)
|
||||
signal session_started(is_server: bool)
|
||||
signal session_ended()
|
||||
signal connection_failed()
|
||||
|
||||
# World hooks, registered by main.gd once the scene tree exists.
|
||||
var _world: Node = null
|
||||
var _players_spawner: MultiplayerSpawner = null
|
||||
var _items_spawner: MultiplayerSpawner = null
|
||||
var _content_root: Node = null
|
||||
|
||||
var _log_file: FileAccess
|
||||
|
||||
## Reason the session ended, shown by the menu on the next _ready() (see
|
||||
## take_status). Avoids depending on a live signal connection to a panel that
|
||||
## doesn't exist yet at the moment the session actually ends.
|
||||
var last_status := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_open_log()
|
||||
multiplayer.peer_connected.connect(_on_peer_connected)
|
||||
multiplayer.peer_disconnected.connect(_on_peer_disconnected)
|
||||
multiplayer.connected_to_server.connect(_on_connected_to_server)
|
||||
multiplayer.connection_failed.connect(_on_connection_failed)
|
||||
multiplayer.server_disconnected.connect(_on_server_disconnected)
|
||||
|
||||
|
||||
# --- Public API ------------------------------------------------------------
|
||||
|
||||
## Start hosting. The host is peer 1 and also plays (listen server).
|
||||
func host(port: int = DEFAULT_PORT) -> Error:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_server(port, MAX_CLIENTS)
|
||||
if err != OK:
|
||||
log_line("HOST failed to create_server on port %d: %s" % [port, error_string(err)])
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
log_line("HOST started on port %d (peer id %d)" % [port, multiplayer.get_unique_id()])
|
||||
session_started.emit(true)
|
||||
# The host's own local player joins immediately.
|
||||
_on_player_present(multiplayer.get_unique_id())
|
||||
return OK
|
||||
|
||||
|
||||
## Join an existing host.
|
||||
func join(address: String = "127.0.0.1", port: int = DEFAULT_PORT) -> Error:
|
||||
var peer := ENetMultiplayerPeer.new()
|
||||
var err := peer.create_client(address, port)
|
||||
if err != OK:
|
||||
log_line("JOIN failed to create_client %s:%d: %s" % [address, port, error_string(err)])
|
||||
return err
|
||||
multiplayer.multiplayer_peer = peer
|
||||
log_line("JOIN connecting to %s:%d ..." % [address, port])
|
||||
return OK
|
||||
|
||||
|
||||
## Leave the session and tear down transport.
|
||||
func leave() -> void:
|
||||
_go_offline()
|
||||
log_line("Session ended")
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
# Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer
|
||||
# null), so is_multiplayer_authority()/get_unique_id() keep working while we are
|
||||
# back in single-player / menu state.
|
||||
func _go_offline() -> void:
|
||||
if multiplayer.multiplayer_peer:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
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:
|
||||
return is_online() and multiplayer.is_server()
|
||||
|
||||
|
||||
## True only when a real ENet session is active. Godot installs a default
|
||||
## OfflineMultiplayerPeer, so a non-null peer alone does not mean "online".
|
||||
func is_online() -> bool:
|
||||
var p := multiplayer.multiplayer_peer
|
||||
return p != null and not (p is OfflineMultiplayerPeer)
|
||||
|
||||
|
||||
## True on the machine that owns authoritative world logic: the server when
|
||||
## online, or the single player when offline. Station logic and spawning should
|
||||
## only run where this is true, so state has one source of truth.
|
||||
func owns_world() -> bool:
|
||||
return not is_online() or is_server()
|
||||
|
||||
|
||||
# --- Station work-progress seam -------------------------------------------
|
||||
|
||||
## Reusable entry point for a client to contribute work to a station (e.g. a
|
||||
## future chopping/gesture station). The client detects the gesture locally and
|
||||
## calls this; the server validates and accumulates. Timer-driven stations like
|
||||
## the Hob don't need it, but it is the drop-in seam for input-driven ones.
|
||||
@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
|
||||
_players_spawner = players_spawner
|
||||
_items_spawner = items_spawner
|
||||
_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
|
||||
## the autoload doesn't hold stale/freed references across a scene reload.
|
||||
func unregister_world() -> void:
|
||||
_world = null
|
||||
_content_root = null
|
||||
_players_spawner = null
|
||||
_items_spawner = null
|
||||
|
||||
|
||||
## 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 doesn't exist yet when the session ends.
|
||||
func take_status() -> String:
|
||||
var s := last_status
|
||||
last_status = ""
|
||||
return s
|
||||
|
||||
|
||||
## Called by main.gd after it has registered the world and connected its
|
||||
## player_joined/left listeners. Kicks off any menu- or command-line-driven
|
||||
## session so that session signals never fire before the world is listening.
|
||||
func world_ready() -> void:
|
||||
consume_pending_session()
|
||||
|
||||
|
||||
# --- Menu-driven session request -------------------------------------------
|
||||
|
||||
# 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
|
||||
# signals (avoids a race between change_scene_to_file and connection callbacks).
|
||||
var pending_action := ""
|
||||
var pending_ip := ""
|
||||
|
||||
func request_host() -> void:
|
||||
pending_action = "host"
|
||||
|
||||
func request_join(ip: String) -> void:
|
||||
pending_action = "join"
|
||||
pending_ip = ip
|
||||
|
||||
func consume_pending_session() -> void:
|
||||
if pending_action == "host":
|
||||
pending_action = ""
|
||||
host()
|
||||
elif pending_action == "join":
|
||||
pending_action = ""
|
||||
join(pending_ip)
|
||||
else:
|
||||
_handle_cmdline()
|
||||
|
||||
|
||||
# --- Session signal handlers ----------------------------------------------
|
||||
|
||||
func _on_peer_connected(peer_id: int) -> void:
|
||||
log_line("peer_connected: %d" % peer_id)
|
||||
# Only the server reacts by materialising that peer's player.
|
||||
if is_server():
|
||||
_on_player_present(peer_id)
|
||||
|
||||
func _on_peer_disconnected(peer_id: int) -> void:
|
||||
log_line("peer_disconnected: %d" % peer_id)
|
||||
if is_server():
|
||||
_on_player_absent(peer_id)
|
||||
|
||||
func _on_connected_to_server() -> void:
|
||||
log_line("connected_to_server (my id=%d)" % multiplayer.get_unique_id())
|
||||
session_started.emit(false)
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
log_line("connection_failed")
|
||||
_go_offline()
|
||||
last_status = "Could not connect"
|
||||
connection_failed.emit()
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
log_line("server_disconnected")
|
||||
_go_offline()
|
||||
last_status = "Host disconnected"
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
# Player materialise/dematerialise. Phase 2 wires these to the PlayersSpawner;
|
||||
# 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:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.has("--server"):
|
||||
log_line("cmdline: --server")
|
||||
host()
|
||||
elif args.has("--join"):
|
||||
var idx := args.find("--join")
|
||||
var addr := "127.0.0.1"
|
||||
if idx + 1 < args.size():
|
||||
addr = args[idx + 1]
|
||||
log_line("cmdline: --join %s" % addr)
|
||||
join(addr)
|
||||
|
||||
|
||||
# --- Logging ---------------------------------------------------------------
|
||||
|
||||
func _open_log() -> void:
|
||||
var dir := OS.get_environment("TEMP")
|
||||
if dir.is_empty():
|
||||
dir = OS.get_environment("TMPDIR")
|
||||
if dir.is_empty():
|
||||
dir = "user://"
|
||||
var path := dir.path_join("vryhungry_net_%d.log" % OS.get_process_id())
|
||||
_log_file = FileAccess.open(path, FileAccess.WRITE)
|
||||
log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id())
|
||||
|
||||
func log_line(s: String) -> void:
|
||||
var id := 0
|
||||
var p := multiplayer.multiplayer_peer
|
||||
if p != null and p.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED:
|
||||
id = multiplayer.get_unique_id()
|
||||
var line := "[NET %d] %s" % [id, s]
|
||||
print(line)
|
||||
if _log_file:
|
||||
_log_file.store_line(line)
|
||||
_log_file.flush()
|
||||
@@ -0,0 +1 @@
|
||||
uid://deefoory0vqmm
|
||||
@@ -0,0 +1,81 @@
|
||||
class_name WorldLayout
|
||||
|
||||
## Data-driven description of what's in the multiplayer world. The server (or
|
||||
## the single machine, when offline) spawns every entry through
|
||||
## 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
|
||||
## Scenes/multiPlayer.tscn so the starting world is unchanged.
|
||||
|
||||
static func get_stations() -> Array[Dictionary]:
|
||||
return [
|
||||
{"scene": "res://Stations/Hob.tscn", "name": "Hob",
|
||||
"xform": Transform3D(Basis(), Vector3(0.29336345, 0.8981018, -1.484)), "props": {}},
|
||||
{"scene": "res://Stations/BurgerBunsDispenser.tscn", "name": "BurgerBunsDispenser",
|
||||
"xform": Transform3D(Basis(), Vector3(-1.832131, 0.40028095, -1.4813508)), "props": {}},
|
||||
{"scene": "res://Stations/sink.tscn", "name": "Sink",
|
||||
"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]:
|
||||
var items: Array[Dictionary] = []
|
||||
|
||||
items.append(_item("res://Items/BurgerBuns.tscn", "BurgerBuns", Vector3(-1.8162017, 1.6081157, -1.4714175)))
|
||||
items.append(_item("res://Items/BurgerBuns.tscn", "BurgerBuns2", Vector3(-1.3596323, 1.4110342, -0.22482127)))
|
||||
|
||||
items.append(_item("res://Containers/plate.tscn", "Plate", Vector3(-1.5717233, 1.5454081, 1.5373346)))
|
||||
items.append(_item("res://Containers/plate.tscn", "Plate2", Vector3(-1.5730225, 1.499024, 0.59790254)))
|
||||
items.append(_item("res://Containers/plate.tscn", "Plate3", Vector3(-1.5818124, 1.499024, -0.4634577)))
|
||||
|
||||
items.append(_item("res://Items/burger.tscn", "burger", Vector3(0.6888188, 1.4195822, -1.7110313)))
|
||||
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:
|
||||
return {"scene": scene, "name": name, "xform": Transform3D(Basis(), pos), "props": {}}
|
||||
@@ -0,0 +1 @@
|
||||
uid://donvkica3drtx
|
||||
@@ -0,0 +1,71 @@
|
||||
extends Node3D
|
||||
|
||||
## Networked representation of one connected player. The owning peer's local
|
||||
## XR rig (found via the "local_xr_origin" group) drives Head/LeftHand/
|
||||
## RightHand each frame; the MultiplayerSynchronizer replicates those
|
||||
## transforms so every other peer sees a matching avatar.
|
||||
|
||||
@onready var _head: Node3D = $Head
|
||||
@onready var _left_hand: Node3D = $LeftHand
|
||||
@onready var _right_hand: Node3D = $RightHand
|
||||
|
||||
var _local_camera: Node3D
|
||||
var _local_left_hand: Node3D
|
||||
var _local_right_hand: Node3D
|
||||
|
||||
|
||||
func _enter_tree() -> void:
|
||||
# MultiplayerSynchronizer resolves authority when this node enters the
|
||||
# tree, so authority must be set here rather than in _ready(), or the
|
||||
# first synced frames go the wrong direction.
|
||||
set_multiplayer_authority(str(name).to_int())
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
# LeftHand/RightHand are the full godot-xr-tools glove scenes, whose root
|
||||
# node carries hand.gd (XRToolsHand). That script sets top_level = true
|
||||
# and every physics frame does global_transform = get_parent().
|
||||
# global_transform * offset — i.e. it actively repositions itself to
|
||||
# track a live XRController3D parent. Here the parent is just this
|
||||
# NetPlayer node, not a controller, so left running it fights (and mostly
|
||||
# wins, since it runs every physics tick regardless of our own _process)
|
||||
# against both the local authority's transform copy below and the
|
||||
# replicated values on other peers — the exact "hands stuck near origin,
|
||||
# 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)
|
||||
_right_hand.set_physics_process(false)
|
||||
|
||||
if 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)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _local_camera:
|
||||
_head.global_transform = _local_camera.global_transform
|
||||
if _local_left_hand:
|
||||
_left_hand.global_transform = _local_left_hand.global_transform
|
||||
if _local_right_hand:
|
||||
_right_hand.global_transform = _local_right_hand.global_transform
|
||||
@@ -0,0 +1 @@
|
||||
uid://bncuxh7j7ix5q
|
||||
@@ -0,0 +1,44 @@
|
||||
[gd_scene load_steps=5 format=3]
|
||||
|
||||
[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://xqimcf20s2jp" path="res://addons/godot-xr-tools/hands/scenes/lowpoly/right_fullglove_low.tscn" id="3_np003"]
|
||||
|
||||
[sub_resource type="CapsuleMesh" id="CapsuleMesh_np004"]
|
||||
radius = 0.09
|
||||
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"]
|
||||
script = ExtResource("1_np001")
|
||||
|
||||
[node name="Head" type="MeshInstance3D" parent="."]
|
||||
mesh = SubResource("CapsuleMesh_np004")
|
||||
|
||||
[node name="LeftHand" parent="." instance=ExtResource("2_np002")]
|
||||
|
||||
[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
|
||||
@@ -49,7 +49,10 @@ func _on_item_dropped(_item: Node3D) -> void:
|
||||
set_deferred("monitoring", false)
|
||||
|
||||
# If the other body is a FoodItem, check for a recipe and combine if one exists.
|
||||
# Combining is a server decision (the base item is server-snapped into a zone).
|
||||
func _on_body_entered(body: Node3D) -> void:
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
if _combining or body == _pickable:
|
||||
print("CombinableItem _on_body_entered: other is our own pickable")
|
||||
return
|
||||
@@ -69,28 +72,22 @@ func _on_body_entered(body: Node3D) -> void:
|
||||
# Instantiate the result of combination and free the two ingredient items
|
||||
func _combine(other_body: Node3D, result: PackedScene) -> void:
|
||||
print("CombinableItem _combine: combining %s + %s into %s" % [_food_item.id, _find_food_item(other_body).id, result.resource_path])
|
||||
_combining = true
|
||||
|
||||
# Get Snapzone
|
||||
var snap_zone := _pickable.get_picked_up_by()
|
||||
if not snap_zone or not snap_zone.has_method("pick_up_object"):
|
||||
push_warning("CombineZone: base item is not held by a snap zone; cannot combine.")
|
||||
_combining = false
|
||||
return
|
||||
|
||||
# Spawn the result at the base item's location, in world space.
|
||||
# Spawn the result through the server (so it replicates to every peer,
|
||||
# including late joiners) at the base item's location, in world space.
|
||||
var base_transform := _pickable.global_transform
|
||||
var result_instance: Node3D = result.instantiate()
|
||||
_pickable.get_tree().current_scene.add_child(result_instance)
|
||||
result_instance.global_transform = base_transform
|
||||
var result_instance: Node3D = NetworkManager.spawn_item(result.resource_path, base_transform)
|
||||
|
||||
# Free the pickable / root of this item and consume the incoming item.
|
||||
snap_zone.drop_object()
|
||||
_pickable.queue_free()
|
||||
if other_body.has_method("drop_and_free"): # XRToolsPickable has this method
|
||||
other_body.drop_and_free()
|
||||
else:
|
||||
other_body.queue_free()
|
||||
NetworkManager.despawn_item(_pickable)
|
||||
other_body.drop()
|
||||
NetworkManager.despawn_item(other_body)
|
||||
|
||||
# Snap the result into the now-empty zone.
|
||||
snap_zone.pick_up_object(result_instance)
|
||||
|
||||
@@ -33,6 +33,9 @@ ssao_enabled = true
|
||||
ssil_enabled = true
|
||||
sdfgi_enabled = true
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_24d3s"]
|
||||
size = Vector3(5.1, 10, 0.1)
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||
script = ExtResource("1_57ppd")
|
||||
|
||||
@@ -163,3 +166,21 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7201865, 1.5329367, 0.1477
|
||||
|
||||
[node name="Plate3" parent="." unique_id=2004250522 instance=ExtResource("10_ay2w6")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5818124, 1.499024, -0.4634577)
|
||||
|
||||
[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.688614, 2.5070028)
|
||||
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.688614, -2.5446289)
|
||||
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, -2.525816, 4.688614, -0.018813243)
|
||||
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, 2.525816, 4.688614, -0.018813023)
|
||||
shape = SubResource("BoxShape3D_24d3s")
|
||||
|
||||
@@ -0,0 +1,166 @@
|
||||
[gd_scene format=3 uid="uid://c1nv4w33fedj6"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://b4ldkd3ngs0vp" path="res://main.gd" id="1_72gy5"]
|
||||
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_8wkh3"]
|
||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_m56cs"]
|
||||
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_5kvh0"]
|
||||
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_l1qm6"]
|
||||
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="6_bktvt"]
|
||||
[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="7_1nkd0"]
|
||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="8_l1owj"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="9_220hi"]
|
||||
[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="10_qacki"]
|
||||
[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="11_npf8s"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_8apyq"]
|
||||
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_jnwcx"]
|
||||
[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="14_1lg2m"]
|
||||
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_yy81s"]
|
||||
[ext_resource type="PackedScene" uid="uid://clujaf3u776a3" path="res://addons/godot-xr-tools/objects/viewport_2d_in_3d.tscn" id="16_vp"]
|
||||
[ext_resource type="PackedScene" path="res://UI/main_menu_panel.tscn" id="17_panel"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||
size = Vector3(115, 0.1, 15)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||
material = ExtResource("3_m56cs")
|
||||
size = Vector3(15, 0.1, 15)
|
||||
|
||||
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||
background_mode = 2
|
||||
sky = ExtResource("5_l1qm6")
|
||||
reflected_light_source = 2
|
||||
ssr_enabled = true
|
||||
ssao_enabled = true
|
||||
ssil_enabled = true
|
||||
sdfgi_enabled = true
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||
script = ExtResource("1_72gy5")
|
||||
|
||||
[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_8wkh3")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
|
||||
|
||||
[node name="MainMenuPanel3D" parent="." unique_id=1448860532 instance=ExtResource("16_vp")]
|
||||
transform = Transform3D(5, 0, 0, 0, 5, 0, 0, 0, 5, 0.36171648, 2.0238447, -1.9758987)
|
||||
screen_size = Vector2(0.6, 0.4)
|
||||
scene = ExtResource("17_panel")
|
||||
viewport_size = Vector2(600, 400)
|
||||
transparent = 1
|
||||
scene_properties_keys = PackedStringArray("main_menu_panel.gd")
|
||||
|
||||
[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]
|
||||
mesh = SubResource("BoxMesh_24d3s")
|
||||
|
||||
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_5kvh0")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.5, -1.484)
|
||||
|
||||
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||
environment = SubResource("Environment_bvwq1")
|
||||
|
||||
[node name="BurgerBuns" parent="." unique_id=1088240294 instance=ExtResource("6_bktvt")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5530176, 1.0505146, -1.3074328)
|
||||
|
||||
[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_1nkd0")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.568947, -0.018512607, -1.317366)
|
||||
|
||||
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("8_l1owj")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5717233, 1.1548845, 1.5373346)
|
||||
|
||||
[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("9_220hi")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6888188, 1.0325116, -1.7110313)
|
||||
|
||||
[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("9_220hi")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6931299, 1.1429771, -1.71225)
|
||||
|
||||
[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("9_220hi")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69027674, 1.1099085, -1.7210286)
|
||||
|
||||
[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("9_220hi")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69134104, 1.0672915, -1.7210286)
|
||||
|
||||
[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("10_qacki")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9352558, 1.2334514, 1.2447833)
|
||||
|
||||
[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("11_npf8s")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.6225724, 1.0654817, -1.0473135)
|
||||
|
||||
[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("11_npf8s")]
|
||||
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.0501236, -1.1673055)
|
||||
|
||||
[node name="PickableObject3" parent="." unique_id=879935619 instance=ExtResource("11_npf8s")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.5142721, 1.0654817, -1.0473135)
|
||||
|
||||
[node name="PickableObject4" parent="." unique_id=198541902 instance=ExtResource("11_npf8s")]
|
||||
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.0501236, -1.1673055)
|
||||
|
||||
[node name="Hamburger2" parent="." unique_id=1349934579 instance=ExtResource("10_qacki")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9857153, 1.1424131, 0.9472374)
|
||||
|
||||
[node name="PickableObject5" parent="." unique_id=1021333509 instance=ExtResource("11_npf8s")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.73287535, 1.0922265, -1.0473135)
|
||||
|
||||
[node name="PickableObject6" parent="." unique_id=1268843669 instance=ExtResource("11_npf8s")]
|
||||
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.0768684, -1.1673055)
|
||||
|
||||
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_8apyq")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3147688, 0.5, -1.4940417)
|
||||
|
||||
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_jnwcx")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.0864775, 0.5, -1.244947)
|
||||
|
||||
[node name="Plate2" parent="." unique_id=356790445 instance=ExtResource("8_l1owj")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5730225, 1.1085004, 0.59790254)
|
||||
|
||||
[node name="CookedBurger" parent="." unique_id=1127807542 instance=ExtResource("14_1lg2m")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30782473, 1.1446649, -1.0928738)
|
||||
|
||||
[node name="CookedBurger2" parent="." unique_id=160088590 instance=ExtResource("14_1lg2m")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.0522771, -1.0928738)
|
||||
|
||||
[node name="CookedBurger3" parent="." unique_id=992183367 instance=ExtResource("14_1lg2m")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344773, 1.0492828, -0.7096845)
|
||||
|
||||
[node name="CookedBurger4" parent="." unique_id=1837607086 instance=ExtResource("14_1lg2m")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.098119, -1.0928738)
|
||||
|
||||
[node name="BurgerBuns2" parent="." unique_id=1210358958 instance=ExtResource("6_bktvt")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3596323, 1.0205106, -0.22482127)
|
||||
|
||||
[node name="PickableObject7" parent="." unique_id=641653545 instance=ExtResource("11_npf8s")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.3556751, 1.0887735, 0.28881657)
|
||||
|
||||
[node name="PickableObject8" parent="." unique_id=1733819363 instance=ExtResource("11_npf8s")]
|
||||
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.0734154, 0.16882455)
|
||||
|
||||
[node name="PickableObject9" parent="." unique_id=12419746 instance=ExtResource("11_npf8s")]
|
||||
transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.4639754, 1.0887735, 0.28881657)
|
||||
|
||||
[node name="PickableObject10" parent="." unique_id=313160217 instance=ExtResource("11_npf8s")]
|
||||
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.0734154, 0.16882455)
|
||||
|
||||
[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("15_yy81s")]
|
||||
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_yy81s")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.5, -0.4458799)
|
||||
|
||||
[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_yy81s")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.5, 0.55367994)
|
||||
|
||||
[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_yy81s")]
|
||||
transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.5, 1.5539298)
|
||||
|
||||
[node name="Hamburger3" parent="." unique_id=369573848 instance=ExtResource("10_qacki")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7201865, 1.142413, 0.14773655)
|
||||
|
||||
[node name="Plate3" parent="." unique_id=2004250522 instance=ExtResource("8_l1owj")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5818124, 1.1085004, -0.4634577)
|
||||
@@ -0,0 +1,76 @@
|
||||
[gd_scene format=3 uid="uid://c30i6h32w8p47"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d1cefhe3yyyds" path="res://Scenes/multiplayer_world.gd" id="1_kdan8"]
|
||||
[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="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||
size = Vector3(15, 0.1, 15)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||
material = ExtResource("3_75ecy")
|
||||
size = Vector3(15, 0.1, 15)
|
||||
|
||||
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||
background_mode = 2
|
||||
sky = ExtResource("5_c3xgf")
|
||||
reflected_light_source = 2
|
||||
ssr_enabled = true
|
||||
ssao_enabled = true
|
||||
ssil_enabled = true
|
||||
sdfgi_enabled = true
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_arao0"]
|
||||
size = Vector3(15, 20, 0.1)
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||
script = ExtResource("1_kdan8")
|
||||
|
||||
[node name="XROrigin3D" parent="." unique_id=2055526621 groups=["local_xr_origin"] instance=ExtResource("2_g30gi")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.36171648, 0, 0.81835127)
|
||||
|
||||
[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="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||
environment = SubResource("Environment_bvwq1")
|
||||
|
||||
[node name="WorldContent" type="Node3D" parent="." unique_id=291550153]
|
||||
|
||||
[node name="Players" type="Node3D" parent="." unique_id=1595463693]
|
||||
|
||||
[node name="ItemsSpawner" type="MultiplayerSpawner" parent="." unique_id=627119248]
|
||||
spawn_path = NodePath("../WorldContent")
|
||||
|
||||
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
||||
_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
|
||||
spawn_path = NodePath("../Players")
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=998476672]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.197384, 7.589958)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="StaticBody3D" unique_id=340303902]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.197384, -7.509142)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[node name="CollisionShape3D3" type="CollisionShape3D" parent="StaticBody3D" unique_id=1193703037]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -7.438612, 9.197384, -0.018813243)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[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)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
@@ -0,0 +1,117 @@
|
||||
extends Node3D
|
||||
|
||||
## World script for the multiplayer scene. Consumes the host/join request set
|
||||
## by the main menu, spawns the world's stations/items on whichever machine
|
||||
## owns the world (server, or the local player when offline), spawns/despawns
|
||||
## 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 —
|
||||
## it's spawned at runtime from Net/world_layout.gd via NetworkManager, so a
|
||||
## joining client receives it from the server (MultiplayerSpawner replays
|
||||
## existing spawns to late joiners) instead of relying on its own local copy
|
||||
## matching.
|
||||
|
||||
const PLAYER_SCENE := preload("res://Player/net_player.tscn")
|
||||
|
||||
## Whether to spawn the full WorldLayout on the machine that owns the world.
|
||||
## The real game scene wants this; focused debug scenes (test/) bake their own
|
||||
## handful of stations and items instead and turn it off, so the thing under
|
||||
## test isn't sharing the world with a second copy of the whole kitchen.
|
||||
@export var populate_from_layout: bool = true
|
||||
|
||||
var xr_interface: XRInterface
|
||||
var _populated := false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
xr_interface = XRServer.find_interface("OpenXR")
|
||||
if xr_interface and xr_interface.is_initialized():
|
||||
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
||||
get_viewport().use_xr = true
|
||||
|
||||
NetworkManager.register_world(self, $PlayersSpawner, $ItemsSpawner)
|
||||
NetworkManager.player_joined.connect(_on_player_joined)
|
||||
NetworkManager.player_left.connect(_on_player_left)
|
||||
NetworkManager.session_started.connect(_on_session_started)
|
||||
NetworkManager.session_ended.connect(_on_session_ended)
|
||||
NetworkManager.connection_failed.connect(_on_connection_failed)
|
||||
|
||||
# world_ready() is what actually calls host()/join() (or the cmdline
|
||||
# equivalent). Populating before this point is wrong for EVERY case, not
|
||||
# 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
|
||||
# build its own local copy instead of receiving the server's via the
|
||||
# spawner. host() emits session_started synchronously, which populates
|
||||
# via _on_session_started below; the explicit call after world_ready()
|
||||
# only matters for the case where neither host() nor join() ran (no
|
||||
# pending session, no cmdline args) — running this scene directly offline.
|
||||
NetworkManager.world_ready()
|
||||
_populate_world_if_owner()
|
||||
|
||||
get_tree().create_timer(3.0).timeout.connect(_log_world_state)
|
||||
|
||||
|
||||
# Temporary-ish sanity check: confirms WorldContent actually ended up
|
||||
# populated on this peer (whether by spawning it or by receiving it via
|
||||
# replication), so a silent replication failure shows up in the net log
|
||||
# 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:
|
||||
NetworkManager.unregister_world()
|
||||
|
||||
|
||||
func _on_session_started(_is_server: bool) -> void:
|
||||
NetworkManager.gate_existing_stations()
|
||||
_populate_world_if_owner()
|
||||
|
||||
|
||||
## Spawns the world's stations/items exactly once, on the machine that owns
|
||||
## world logic (server or offline). Safe to call multiple times/entry points.
|
||||
func _populate_world_if_owner() -> void:
|
||||
if not NetworkManager.owns_world() or _populated or not populate_from_layout:
|
||||
return
|
||||
_populated = true
|
||||
GameManager.meals_in_play = ["hamburger"]
|
||||
var stations := WorldLayout.get_stations()
|
||||
var items := WorldLayout.get_items()
|
||||
NetworkManager.log_line("Populating world: %d stations, %d items" % [stations.size(), items.size()])
|
||||
for d in stations:
|
||||
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
||||
for d in items:
|
||||
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
|
||||
NetworkManager.log_line("World populated")
|
||||
|
||||
|
||||
## Only the server (or the single offline machine) materialises player
|
||||
## avatars; MultiplayerSpawner replicates the result to everyone else,
|
||||
## including late joiners.
|
||||
func _on_player_joined(peer_id: int) -> void:
|
||||
if not NetworkManager.owns_world() or $Players.has_node(str(peer_id)):
|
||||
return
|
||||
var p := PLAYER_SCENE.instantiate()
|
||||
p.name = str(peer_id)
|
||||
$Players.add_child(p, true)
|
||||
NetworkManager.log_line("Spawned avatar for peer %d" % peer_id)
|
||||
|
||||
|
||||
func _on_player_left(peer_id: int) -> void:
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
var p := $Players.get_node_or_null(str(peer_id))
|
||||
if p:
|
||||
p.queue_free()
|
||||
NetworkManager.log_line("Despawned avatar for peer %d" % peer_id)
|
||||
|
||||
|
||||
func _on_session_ended() -> void:
|
||||
NetworkManager.log_line("Session ended, returning to main menu")
|
||||
get_tree().change_scene_to_file("res://Scenes/mainMenu.tscn")
|
||||
|
||||
|
||||
func _on_connection_failed() -> void:
|
||||
NetworkManager.log_line("Connection failed, returning to main menu")
|
||||
get_tree().change_scene_to_file("res://Scenes/mainMenu.tscn")
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1cefhe3yyyds
|
||||
@@ -22,7 +22,6 @@ collision_layer = 65536
|
||||
collision_mask = 65536
|
||||
script = ExtResource("1_p30r1")
|
||||
snap_mode = 1
|
||||
initial_object = NodePath("../../BurgerBuns")
|
||||
metadata/_custom_type_script = "uid://cquqe4f1m1sw6"
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=1104626493]
|
||||
|
||||
@@ -8,6 +8,14 @@
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_vlqg6"]
|
||||
radius = 0.3
|
||||
|
||||
[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
|
||||
|
||||
[node name="Hob" type="StaticBody3D" unique_id=1687971542 groups=["station"]]
|
||||
script = ExtResource("1_7jc4g")
|
||||
|
||||
@@ -103,3 +111,7 @@ size = Vector3(0.9842529, 0.095687866, 0.14019775)
|
||||
transform = Transform3D(0.94465846, 0, 0, 0, 0.94465846, 0, 0, 0, 0.94465846, -0.003479004, -0.08253479, 0.5616675)
|
||||
operation = 2
|
||||
size = Vector3(0.9842529, 0.8349304, 0.072387695)
|
||||
|
||||
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_hob")
|
||||
|
||||
@@ -7,9 +7,13 @@ func _ready() -> void:
|
||||
snap_zone.has_picked_up.connect(_makeDirty)
|
||||
|
||||
func _makeDirty(item) -> void:
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
var plate: PlateController = item.get_node_or_null("PlateController") as PlateController
|
||||
if not plate:
|
||||
print("Dirt Station: held object is not a Plate")
|
||||
return
|
||||
if not plate.is_dirty:
|
||||
plate.is_dirty = true
|
||||
print("Dirt Station could not find Dirty in: ", item)
|
||||
|
||||
|
||||
|
||||
+6
-5
@@ -38,6 +38,8 @@ func _on_object_dropped() -> void:
|
||||
|
||||
|
||||
func convert_held_to_item(_item: PackedScene) -> void:
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
print("Hob converting ", _item)
|
||||
|
||||
var old_pickable = snap_zone.picked_up_object
|
||||
@@ -45,16 +47,15 @@ func convert_held_to_item(_item: PackedScene) -> void:
|
||||
push_warning("Hob finished cooking _item, but snap zone is missing its reference")
|
||||
return
|
||||
|
||||
# Create new item as child of root in the same position
|
||||
# Spawn the new item through the server so it replicates to every peer
|
||||
# (including late joiners), in the old item's position.
|
||||
var original_transform = old_pickable.global_transform
|
||||
var new_scene_instance = _item.instantiate()
|
||||
get_tree().get_root().add_child(new_scene_instance)
|
||||
new_scene_instance.global_transform = original_transform
|
||||
var new_scene_instance = NetworkManager.spawn_item(_item.resource_path, original_transform)
|
||||
|
||||
# Drop and free the old item and pick up the new one
|
||||
print("Hob freeing old_pickable ", old_pickable)
|
||||
snap_zone.drop_object()
|
||||
old_pickable.queue_free()
|
||||
NetworkManager.despawn_item(old_pickable)
|
||||
snap_zone.pick_up_object(new_scene_instance)
|
||||
|
||||
|
||||
|
||||
@@ -13,10 +13,8 @@ func _ready() -> void:
|
||||
|
||||
# Called every frame. 'delta' is the elapsed time since the previous frame.
|
||||
func _process(delta: float) -> void:
|
||||
if not NetworkManager.owns_world():
|
||||
return
|
||||
if not snap_zone.picked_up_object:
|
||||
var new_item = item_scene.instantiate()
|
||||
|
||||
# Add the new item to the hob's parent (so it lives in the main world space)
|
||||
get_parent().add_child(new_item)
|
||||
new_item.global_transform = snap_zone.transform
|
||||
var new_item = NetworkManager.spawn_item(item_scene.resource_path, snap_zone.global_transform)
|
||||
snap_zone.pick_up_object(new_item)
|
||||
|
||||
@@ -9,6 +9,11 @@
|
||||
[sub_resource type="SphereShape3D" id="SphereShape3D_dlkho"]
|
||||
radius = 0.3
|
||||
|
||||
[sub_resource type="SceneReplicationConfig" id="SceneReplicationConfig_np_sink"]
|
||||
properties/0/path = NodePath(".:progress")
|
||||
properties/0/spawn = true
|
||||
properties/0/replication_mode = 1
|
||||
|
||||
[node name="Sink" type="StaticBody3D" unique_id=2055277359 groups=["station"]]
|
||||
script = ExtResource("1_7hh4b")
|
||||
plate_scene = ExtResource("2_ai6d4")
|
||||
@@ -75,3 +80,7 @@ size = Vector3(1.4033203, 1.2053223, 0.352417)
|
||||
transform = Transform3D(1.3113414e-07, 0.12955962, 0.99157166, 8.8817837e-16, 0.9915716, -0.1295596, -0.9999999, 1.6989695e-08, 1.3002891e-07, 0.6035124, 0.019589934, -0.20879287)
|
||||
operation = 2
|
||||
size = Vector3(1.4033203, 1.2053223, 0.352417)
|
||||
|
||||
[node name="Sync" type="MultiplayerSynchronizer" parent="."]
|
||||
root_path = NodePath("..")
|
||||
replication_config = SubResource("SceneReplicationConfig_np_sink")
|
||||
|
||||
+96
-57
@@ -1,3 +1,4 @@
|
||||
class_name Table
|
||||
extends StaticBody3D
|
||||
|
||||
const GAME_MANAGER = preload("res://GameManager.gd")
|
||||
@@ -25,9 +26,16 @@ enum TableState {
|
||||
WAITING_FRIEND, # Waiting for friends food to arrive
|
||||
EATING
|
||||
}
|
||||
var _state = TableState.EMPTY # use _setState(), never set directly
|
||||
var _state_time: float = 0.0
|
||||
var _unsatisfied_orders: Array[String]
|
||||
|
||||
## State is server-authoritative and synced (see table.tscn's Sync node);
|
||||
## state transitions only ever run where NetworkManager.owns_world() is true
|
||||
## (the whole station's _process is gated off elsewhere for non-owners, see
|
||||
## NetworkManager._gate_station). The setters below just refresh the display,
|
||||
## so both the server (via _setState) and clients (via incoming sync) show
|
||||
## the same text. Use _setState(), never assign _state directly.
|
||||
var _state: TableState = TableState.EMPTY: set = _set_state
|
||||
var _state_time: float = 0.0: set = _set_state_time
|
||||
var _unsatisfied_orders: Array[String] = []: set = _set_unsatisfied_orders
|
||||
|
||||
func _ready() -> void:
|
||||
if not label_3d:
|
||||
@@ -81,7 +89,6 @@ func _absorb_item_if_correct(_item: Node) -> void:
|
||||
_unsatisfied_orders.erase(food_item.id)
|
||||
GAME_MANAGER.money += food_item.sell_value
|
||||
_setState(TableState.WAITING_FRIEND)
|
||||
_update_order_text()
|
||||
|
||||
# Table has everything it wants. Start eating
|
||||
if _unsatisfied_orders.is_empty():
|
||||
@@ -94,60 +101,15 @@ func place_order() -> void:
|
||||
_unsatisfied_orders.append(GAME_MANAGER.get_random_meal())
|
||||
|
||||
|
||||
func _update_order_text() -> void:
|
||||
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)]
|
||||
func satisfyAllOrders() -> void:
|
||||
print("Table: satisfyAllOrders()")
|
||||
_unsatisfied_orders.clear()
|
||||
clearAllPlates()
|
||||
_setState(TableState.EMPTY)
|
||||
|
||||
|
||||
func _setState(newState: TableState) -> void:
|
||||
print("Table set _state: ", TableState.keys()[newState])
|
||||
|
||||
if newState == TableState.EMPTY:
|
||||
label_3d.text = "empty"
|
||||
|
||||
if newState == TableState.THINKING:
|
||||
_state_time = initial_eating_time
|
||||
label_3d.text = lbl_thinking
|
||||
|
||||
if newState == TableState.WAITING_PRIMARY:
|
||||
_state_time = initial_primary_time
|
||||
_update_order_text()
|
||||
|
||||
if newState == TableState.WAITING_FRIEND:
|
||||
_state_time = initial_friend_time
|
||||
_update_order_text()
|
||||
|
||||
if newState == TableState.EATING:
|
||||
_state_time = initial_eating_time
|
||||
label_3d.text = lbl_eating
|
||||
|
||||
_state = newState
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Wait for state to finish
|
||||
if _state_time > 0:
|
||||
_state_time -= delta
|
||||
label_3d_time.text = "%.1f" % _state_time
|
||||
return
|
||||
|
||||
# When state timer is finished, do this stuff before moving to next state
|
||||
match _state:
|
||||
TableState.EMPTY:
|
||||
_setState(TableState.THINKING)
|
||||
|
||||
TableState.THINKING:
|
||||
_setState(TableState.WAITING_PRIMARY)
|
||||
place_order()
|
||||
_update_order_text()
|
||||
|
||||
|
||||
TableState.WAITING_PRIMARY:
|
||||
label_3d.text = lbl_gameover
|
||||
|
||||
TableState.WAITING_FRIEND:
|
||||
label_3d.text = lbl_gameover
|
||||
|
||||
TableState.EATING:
|
||||
func clearAllPlates() -> void:
|
||||
print("Table: clearAllPlates()")
|
||||
for snap_zone_node in snap_zones:
|
||||
var held_object = snap_zone_node.picked_up_object
|
||||
if not held_object:
|
||||
@@ -157,5 +119,82 @@ func _process(delta: float) -> void:
|
||||
if plate:
|
||||
plate.container.clear()
|
||||
plate.is_dirty = true
|
||||
_setState(TableState.EMPTY)
|
||||
|
||||
## Server-only state transition: sets the new state's timer and assigns
|
||||
## _state (whose setter refreshes the display on every peer).
|
||||
func _setState(newState: TableState) -> void:
|
||||
print("Table set _state: ", TableState.keys()[newState])
|
||||
|
||||
match newState:
|
||||
TableState.EMPTY:
|
||||
_state_time = 0.0
|
||||
TableState.THINKING:
|
||||
_state_time = initial_eating_time
|
||||
TableState.WAITING_PRIMARY:
|
||||
_state_time = initial_primary_time
|
||||
TableState.WAITING_FRIEND:
|
||||
_state_time = initial_friend_time
|
||||
TableState.EATING:
|
||||
_state_time = initial_eating_time
|
||||
|
||||
_state = newState
|
||||
|
||||
|
||||
## Pure presentation, driven off the current (locally authoritative or
|
||||
## synced-from-server) state. Runs on every peer.
|
||||
func _refresh_display() -> void:
|
||||
if not label_3d:
|
||||
return
|
||||
match _state:
|
||||
TableState.EMPTY:
|
||||
label_3d.text = "empty"
|
||||
TableState.THINKING:
|
||||
label_3d.text = lbl_thinking
|
||||
TableState.WAITING_PRIMARY, TableState.WAITING_FRIEND:
|
||||
label_3d.text = "%s\n%s" % [TableState.keys()[_state], "\n".join(_unsatisfied_orders)]
|
||||
TableState.EATING:
|
||||
label_3d.text = lbl_eating
|
||||
if label_3d_time:
|
||||
label_3d_time.text = "%.1f" % _state_time
|
||||
|
||||
|
||||
func _set_state(value: TableState) -> void:
|
||||
_state = value
|
||||
_refresh_display()
|
||||
|
||||
|
||||
func _set_state_time(value: float) -> void:
|
||||
_state_time = value
|
||||
_refresh_display()
|
||||
|
||||
|
||||
func _set_unsatisfied_orders(value: Array[String]) -> void:
|
||||
_unsatisfied_orders = value
|
||||
_refresh_display()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
# Wait for state to finish
|
||||
if _state_time > 0:
|
||||
_state_time -= delta
|
||||
return
|
||||
|
||||
# When state timer is finished, do this stuff before moving to next state
|
||||
match _state:
|
||||
TableState.EMPTY:
|
||||
_setState(TableState.THINKING)
|
||||
|
||||
TableState.THINKING:
|
||||
place_order()
|
||||
_setState(TableState.WAITING_PRIMARY)
|
||||
|
||||
|
||||
TableState.WAITING_PRIMARY:
|
||||
label_3d.text = lbl_gameover
|
||||
|
||||
TableState.WAITING_FRIEND:
|
||||
label_3d.text = lbl_gameover
|
||||
|
||||
TableState.EATING:
|
||||
clearAllPlates()
|
||||
_setState(TableState.EMPTY)
|
||||
|
||||
@@ -11,6 +11,17 @@ radius = 0.3
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_24d3s"]
|
||||
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"]]
|
||||
script = ExtResource("1_2vcpj")
|
||||
|
||||
@@ -155,3 +166,6 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.3306234, 0)
|
||||
pixel_size = 0.003
|
||||
billboard = 2
|
||||
text = "20.1s"
|
||||
|
||||
[node name="Sync" type="MultiplayerSynchronizer" parent="." unique_id=2000411505]
|
||||
replication_config = SubResource("SceneReplicationConfig_np_table")
|
||||
|
||||
@@ -4,12 +4,13 @@
|
||||
|
||||
[sub_resource type="StandardMaterial3D" id="StandardMaterial3D_0gbf8"]
|
||||
transparency = 1
|
||||
albedo_color = Color(0.62908286, 0.62908286, 0.62908286, 1)
|
||||
albedo_color = Color(0.11502249, 0.11502249, 0.11502249, 1)
|
||||
albedo_texture = ExtResource("1_3jxsn")
|
||||
uv1_triplanar = true
|
||||
|
||||
[resource]
|
||||
next_pass = SubResource("StandardMaterial3D_0gbf8")
|
||||
albedo_color = Color(0, 0, 0, 1)
|
||||
metallic = 1.0
|
||||
metallic_specular = 0.0
|
||||
roughness = 0.0
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
extends Control
|
||||
|
||||
## The 2D UI rendered inside the main menu's world-space viewport
|
||||
## (XRToolsViewport2DIn3D). Lets the player pick John's scene, host a
|
||||
## multiplayer game, or join one by IP, driving scene switches + NetworkManager
|
||||
## directly.
|
||||
|
||||
const JON_SCENE := "res://Scenes/JonScene.tscn"
|
||||
const MULTIPLAYER_SCENE := "res://Scenes/multiPlayer.tscn"
|
||||
const SETTINGS_PATH := "user://vryhungry_settings.cfg"
|
||||
|
||||
var _ip := "127.0.0.1"
|
||||
|
||||
@onready var _root_view: Control = %RootView
|
||||
@onready var _join_view: Control = %JoinView
|
||||
@onready var _ip_label: Label = %IPLabel
|
||||
@onready var _status: Label = %Status
|
||||
@onready var _keypad: GridContainer = %Keypad
|
||||
@onready var _john_btn: Button = %JohnButton
|
||||
@onready var _host_btn: Button = %HostButton
|
||||
@onready var _join_menu_btn: Button = %JoinMenuButton
|
||||
@onready var _join_btn: Button = %JoinButton
|
||||
@onready var _back_btn: Button = %BackButton
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
if _bypass_menu_for_cmdline():
|
||||
return
|
||||
for child in _keypad.get_children():
|
||||
if child is Button:
|
||||
child.pressed.connect(_on_key.bind(child.text))
|
||||
_john_btn.pressed.connect(_on_john_pressed)
|
||||
_host_btn.pressed.connect(_on_host_pressed)
|
||||
_join_menu_btn.pressed.connect(_show_join_view)
|
||||
_join_btn.pressed.connect(_on_join_pressed)
|
||||
_back_btn.pressed.connect(_show_root_view)
|
||||
_load_ip()
|
||||
_show_root_view()
|
||||
_refresh_ip()
|
||||
# Show why we're back here, if the last session ended unexpectedly
|
||||
# (dropped connection, failed join). Pulled rather than pushed, since
|
||||
# NetworkManager may have set this before this panel even existed.
|
||||
var status := NetworkManager.take_status()
|
||||
if not status.is_empty():
|
||||
set_status(status)
|
||||
|
||||
|
||||
## --server / --join <ip> on the command line skip straight to the multiplayer
|
||||
## scene, same as the previously headless-tested main.tscn flow.
|
||||
func _bypass_menu_for_cmdline() -> bool:
|
||||
var args := OS.get_cmdline_user_args()
|
||||
if args.has("--server"):
|
||||
NetworkManager.request_host()
|
||||
get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE)
|
||||
return true
|
||||
if args.has("--join"):
|
||||
var idx := args.find("--join")
|
||||
var addr := "127.0.0.1"
|
||||
if idx + 1 < args.size():
|
||||
addr = args[idx + 1]
|
||||
NetworkManager.request_join(addr)
|
||||
get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE)
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _show_root_view() -> void:
|
||||
_root_view.visible = true
|
||||
_join_view.visible = false
|
||||
|
||||
|
||||
func _show_join_view() -> void:
|
||||
_root_view.visible = false
|
||||
_join_view.visible = true
|
||||
set_status("Enter host IP, then Join")
|
||||
|
||||
|
||||
func _on_key(key: String) -> void:
|
||||
match key:
|
||||
"DEL":
|
||||
_ip = _ip.substr(0, max(0, _ip.length() - 1))
|
||||
_:
|
||||
if _ip.length() < 21:
|
||||
_ip += key
|
||||
_refresh_ip()
|
||||
|
||||
|
||||
func _refresh_ip() -> void:
|
||||
_ip_label.text = _ip if not _ip.is_empty() else "_"
|
||||
|
||||
|
||||
func _on_john_pressed() -> void:
|
||||
NetworkManager.leave()
|
||||
get_tree().change_scene_to_file.call_deferred(JON_SCENE)
|
||||
|
||||
|
||||
func _on_host_pressed() -> void:
|
||||
NetworkManager.request_host()
|
||||
get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE)
|
||||
|
||||
|
||||
func _on_join_pressed() -> void:
|
||||
if _ip.is_empty():
|
||||
set_status("Enter an IP first")
|
||||
return
|
||||
_save_ip()
|
||||
NetworkManager.request_join(_ip)
|
||||
get_tree().change_scene_to_file.call_deferred(MULTIPLAYER_SCENE)
|
||||
|
||||
|
||||
## Remembers the last IP the player tried to join, so the keypad starts
|
||||
## pre-filled next time instead of always defaulting to 127.0.0.1.
|
||||
func _load_ip() -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
if cfg.load(SETTINGS_PATH) == OK:
|
||||
_ip = cfg.get_value("network", "last_ip", _ip)
|
||||
|
||||
|
||||
func _save_ip() -> void:
|
||||
var cfg := ConfigFile.new()
|
||||
cfg.load(SETTINGS_PATH)
|
||||
cfg.set_value("network", "last_ip", _ip)
|
||||
cfg.save(SETTINGS_PATH)
|
||||
|
||||
|
||||
## Called by network_manager (e.g. on connection_failed after a bounce back to
|
||||
## this menu) to show feedback.
|
||||
func set_status(text: String) -> void:
|
||||
if _status:
|
||||
_status.text = text
|
||||
@@ -0,0 +1 @@
|
||||
uid://crs47shds8bm4
|
||||
@@ -0,0 +1,204 @@
|
||||
[gd_scene load_steps=2 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://UI/main_menu_panel.gd" id="1_panel"]
|
||||
|
||||
[node name="MainMenuPanel" type="Control"]
|
||||
layout_mode = 3
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
script = ExtResource("1_panel")
|
||||
|
||||
[node name="Background" type="ColorRect" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
color = Color(0.09, 0.1, 0.13, 1)
|
||||
|
||||
[node name="Margin" type="MarginContainer" parent="."]
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
theme_override_constants/margin_left = 24
|
||||
theme_override_constants/margin_top = 16
|
||||
theme_override_constants/margin_right = 24
|
||||
theme_override_constants/margin_bottom = 16
|
||||
|
||||
[node name="Title" type="Label" parent="Margin"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "VRyHungry"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Status" type="Label" parent="Margin"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 0
|
||||
theme_override_colors/font_color = Color(0.8, 0.8, 0.5, 1)
|
||||
theme_override_font_sizes/font_size = 16
|
||||
text = ""
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="RootView" type="VBoxContainer" parent="Margin"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 14
|
||||
|
||||
[node name="Spacer" type="Control" parent="Margin/RootView"]
|
||||
custom_minimum_size = Vector2(0, 36)
|
||||
layout_mode = 2
|
||||
|
||||
[node name="JohnButton" type="Button" parent="Margin/RootView"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 64)
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "Play John's Scene"
|
||||
|
||||
[node name="HostButton" type="Button" parent="Margin/RootView"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 64)
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "Host Multiplayer"
|
||||
|
||||
[node name="JoinMenuButton" type="Button" parent="Margin/RootView"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
custom_minimum_size = Vector2(0, 64)
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "Join Multiplayer"
|
||||
|
||||
[node name="JoinView" type="VBoxContainer" parent="Margin"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Title" type="Label" parent="Margin/JoinView"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "Join Multiplayer"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="IPLabel" type="Label" parent="Margin/JoinView"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 34
|
||||
text = "127.0.0.1"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Keypad" type="GridContainer" parent="Margin/JoinView"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
theme_override_constants/h_separation = 8
|
||||
theme_override_constants/v_separation = 8
|
||||
columns = 3
|
||||
|
||||
[node name="B1" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "1"
|
||||
|
||||
[node name="B2" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "2"
|
||||
|
||||
[node name="B3" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "3"
|
||||
|
||||
[node name="B4" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "4"
|
||||
|
||||
[node name="B5" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "5"
|
||||
|
||||
[node name="B6" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "6"
|
||||
|
||||
[node name="B7" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "7"
|
||||
|
||||
[node name="B8" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "8"
|
||||
|
||||
[node name="B9" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "9"
|
||||
|
||||
[node name="BDot" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "."
|
||||
|
||||
[node name="B0" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 26
|
||||
text = "0"
|
||||
|
||||
[node name="BDel" type="Button" parent="Margin/JoinView/Keypad"]
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
size_flags_vertical = 3
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "DEL"
|
||||
|
||||
[node name="Buttons" type="HBoxContainer" parent="Margin/JoinView"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="JoinButton" type="Button" parent="Margin/JoinView/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "JOIN"
|
||||
|
||||
[node name="BackButton" type="Button" parent="Margin/JoinView/Buttons"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
custom_minimum_size = Vector2(0, 56)
|
||||
theme_override_font_sizes/font_size = 24
|
||||
text = "BACK"
|
||||
@@ -7,6 +7,7 @@
|
||||
[ext_resource type="PackedScene" uid="uid://diyu06cw06syv" path="res://addons/godot-xr-tools/player/player_body.tscn" id="4_6xr3x"]
|
||||
[ext_resource type="PackedScene" uid="uid://b6bk2pj8vbj28" path="res://addons/godot-xr-tools/functions/movement_turn.tscn" id="4_rd8py"]
|
||||
[ext_resource type="Script" uid="uid://ck4yn3hxuobj7" path="res://addons/godot-xr-tools/player/player_body.gd" id="5_rd8py"]
|
||||
[ext_resource type="PackedScene" uid="uid://cqhw276realc" path="res://addons/godot-xr-tools/functions/function_pointer.tscn" id="6_ptr"]
|
||||
|
||||
[node name="XROrigin3D" type="XROrigin3D" unique_id=2055526621]
|
||||
|
||||
@@ -25,6 +26,10 @@ strafe = true
|
||||
[node name="FunctionPickup" parent="XRControllerLeftHand" unique_id=2133415351 instance=ExtResource("3_rd8py")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009340696, -0.0074635968, 0.024033919)
|
||||
|
||||
[node name="FunctionPointer" parent="XRControllerLeftHand" instance=ExtResource("6_ptr")]
|
||||
show_laser = 2
|
||||
show_target = true
|
||||
|
||||
[node name="XRControllerRightHand" type="XRController3D" parent="." unique_id=202756852]
|
||||
tracker = &"right_hand"
|
||||
|
||||
@@ -39,6 +44,10 @@ smooth_turn_speed = 3.5
|
||||
[node name="FunctionPickup" parent="XRControllerRightHand" unique_id=1876210450 instance=ExtResource("3_rd8py")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0074635968, 0.024033919)
|
||||
|
||||
[node name="FunctionPointer" parent="XRControllerRightHand" instance=ExtResource("6_ptr")]
|
||||
show_laser = 2
|
||||
show_target = true
|
||||
|
||||
[node name="PlayerBody" type="CharacterBody3D" parent="." unique_id=1444632058 groups=["player_body"] instance=ExtResource("4_6xr3x")]
|
||||
process_priority = -100
|
||||
process_physics_priority = -100
|
||||
|
||||
@@ -186,6 +186,12 @@ func _is_correct_hand(grabber : Node3D) -> bool:
|
||||
# Get the positional tracker
|
||||
var tracker := XRServer.get_tracker(controller.tracker) as XRPositionalTracker
|
||||
|
||||
# Without an XR runtime (desktop/headless testing) there is no tracker, so
|
||||
# we can't tell which hand this is. Treat it as "not the correct hand" —
|
||||
# the same result the null deref below used to produce after erroring.
|
||||
if not tracker:
|
||||
return false
|
||||
|
||||
# If left hand then verify left controller
|
||||
if hand == Hand.LEFT and tracker.hand != XRPositionalTracker.TRACKER_HAND_LEFT:
|
||||
return false
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,74 @@
|
||||
[runnable_presets]
|
||||
|
||||
"Windows Desktop"="Windows Desktop"
|
||||
|
||||
[preset.0]
|
||||
|
||||
name="Windows Desktop"
|
||||
platform="Windows Desktop"
|
||||
dedicated_server=false
|
||||
custom_features=""
|
||||
export_filter="all_resources"
|
||||
include_filter=""
|
||||
exclude_filter=""
|
||||
export_path=".build/game_debug.exe"
|
||||
patches=PackedStringArray()
|
||||
patch_delta_encoding=false
|
||||
patch_delta_compression_level_zstd=19
|
||||
patch_delta_min_reduction=0.1
|
||||
patch_delta_include_filters="*"
|
||||
patch_delta_exclude_filters=""
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
seed=0
|
||||
encrypt_pck=false
|
||||
encrypt_directory=false
|
||||
script_export_mode=2
|
||||
|
||||
[preset.0.options]
|
||||
|
||||
custom_template/debug=""
|
||||
custom_template/release=""
|
||||
debug/export_console_wrapper=1
|
||||
binary_format/embed_pck=true
|
||||
texture_format/s3tc_bptc=true
|
||||
texture_format/etc2_astc=false
|
||||
shader_baker/enabled=false
|
||||
binary_format/architecture="x86_64"
|
||||
codesign/enable=false
|
||||
codesign/timestamp=true
|
||||
codesign/timestamp_server_url=""
|
||||
codesign/digest_algorithm=1
|
||||
codesign/description=""
|
||||
codesign/custom_options=PackedStringArray()
|
||||
application/modify_resources=true
|
||||
application/icon=""
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=4
|
||||
application/file_version=""
|
||||
application/product_version=""
|
||||
application/company_name=""
|
||||
application/product_name=""
|
||||
application/file_description=""
|
||||
application/copyright=""
|
||||
application/trademarks=""
|
||||
application/export_angle=0
|
||||
application/export_d3d12=0
|
||||
application/d3d12_agility_sdk_multiarch=true
|
||||
ssh_remote_deploy/enabled=false
|
||||
ssh_remote_deploy/host="user@host_ip"
|
||||
ssh_remote_deploy/port="22"
|
||||
ssh_remote_deploy/extra_args_ssh=""
|
||||
ssh_remote_deploy/extra_args_scp=""
|
||||
ssh_remote_deploy/run_script="Expand-Archive -LiteralPath '{temp_dir}\\{archive_name}' -DestinationPath '{temp_dir}'
|
||||
$action = New-ScheduledTaskAction -Execute '{temp_dir}\\{exe_name}' -Argument '{cmd_args}'
|
||||
$trigger = New-ScheduledTaskTrigger -Once -At 00:00
|
||||
$settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries
|
||||
$task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings
|
||||
Register-ScheduledTask godot_remote_debug -InputObject $task -Force:$true
|
||||
Start-ScheduledTask -TaskName godot_remote_debug
|
||||
while (Get-ScheduledTask -TaskName godot_remote_debug | ? State -eq running) { Start-Sleep -Milliseconds 100 }
|
||||
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue"
|
||||
ssh_remote_deploy/cleanup_script="Stop-ScheduledTask -TaskName godot_remote_debug -ErrorAction:SilentlyContinue
|
||||
Unregister-ScheduledTask -TaskName godot_remote_debug -Confirm:$false -ErrorAction:SilentlyContinue
|
||||
Remove-Item -Recurse -Force '{temp_dir}'"
|
||||
@@ -0,0 +1,20 @@
|
||||
extends Node
|
||||
|
||||
func _unhandled_input(event: InputEvent) -> void:
|
||||
if event.is_action_pressed("quit_game"):
|
||||
get_tree().quit()
|
||||
if event.is_action_pressed("satisfy_table_orders"):
|
||||
satisfy_table_orders()
|
||||
|
||||
|
||||
# Finds any node of class/type Table and calls satisfyAllOrders()
|
||||
func satisfy_table_orders() -> void:
|
||||
# Searches all nodes in the current running scene matching the class "Table"
|
||||
var table_nodes = get_tree().root.find_children("*", "Table", true, false)
|
||||
|
||||
for node in table_nodes:
|
||||
if node.has_method("satisfyAllOrders"):
|
||||
node.satisfyAllOrders()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
uid://c60unagog5oi1
|
||||
@@ -9,30 +9,41 @@
|
||||
"stages": [
|
||||
{
|
||||
"uuid": "94333de9-ebf5-4f38-b95b-9477f4bb9dde",
|
||||
"title": "Todo",
|
||||
"title": "Backlog",
|
||||
"tasks": [
|
||||
"854c7e1e-7521-4bcd-83ff-bb3fecec3142",
|
||||
"4e2ca8d1-84e1-4913-ad8f-834007d52160",
|
||||
"dd822d14-88e5-4790-808e-7d2cf7f79133",
|
||||
"784d5cd3-333a-40a9-b35f-3d0c73f00761"
|
||||
"784d5cd3-333a-40a9-b35f-3d0c73f00761",
|
||||
"21bc19f1-ee3c-4bbb-ab9a-07ec6123a823",
|
||||
"d93a31cd-d475-4c5a-87fc-983174b2594f"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uuid": "211e4050-31cd-4425-a2ba-8ca56b4764cd",
|
||||
"title": "Doing",
|
||||
"title": "Todo",
|
||||
"tasks": [
|
||||
"d9cebd68-792e-4d01-a916-7df5f128b4a8",
|
||||
"1c3b9543-cb67-431d-bf71-c8753e816776"
|
||||
"55ac73b8-4378-4b58-bf3c-33f64590c804"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uuid": "fab8a81b-7ca9-4026-96bb-ed66ea58ef2e",
|
||||
"title": "Doing",
|
||||
"tasks": [
|
||||
"f8073eb2-8786-47b7-b63f-fa70c3f7115a",
|
||||
"6942c45a-53a4-484f-9885-4f249cb4572b"
|
||||
]
|
||||
},
|
||||
{
|
||||
"uuid": "0d743057-955a-473a-8123-a3f805505d5d",
|
||||
"title": "Done",
|
||||
"tasks": [
|
||||
"4ab36c89-a377-4f25-86a1-ac276aadf4a2",
|
||||
"cde44fb1-3fdf-4ea0-8267-d28d894e4e7d",
|
||||
"a4dd35ae-dc8e-42a3-9044-bf1cb1af281a",
|
||||
"1c3b9543-cb67-431d-bf71-c8753e816776",
|
||||
"d9cebd68-792e-4d01-a916-7df5f128b4a8",
|
||||
"eee1990e-f957-4499-84b6-e68003fcb78e",
|
||||
"a4dd35ae-dc8e-42a3-9044-bf1cb1af281a"
|
||||
"4ab36c89-a377-4f25-86a1-ac276aadf4a2",
|
||||
"cde44fb1-3fdf-4ea0-8267-d28d894e4e7d"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -196,6 +207,41 @@
|
||||
"done": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"uuid": "21bc19f1-ee3c-4bbb-ab9a-07ec6123a823",
|
||||
"title": "Visual indicator Hob",
|
||||
"description": "",
|
||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||
"steps": []
|
||||
},
|
||||
{
|
||||
"uuid": "d93a31cd-d475-4c5a-87fc-983174b2594f",
|
||||
"title": "Visual indicator Sink",
|
||||
"description": "",
|
||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||
"steps": []
|
||||
},
|
||||
{
|
||||
"uuid": "6942c45a-53a4-484f-9885-4f249cb4572b",
|
||||
"title": "Stations progress bar",
|
||||
"description": "",
|
||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||
"steps": []
|
||||
},
|
||||
{
|
||||
"uuid": "f8073eb2-8786-47b7-b63f-fa70c3f7115a",
|
||||
"title": "Game over screen / effect with restart button",
|
||||
"description": "",
|
||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||
"steps": []
|
||||
},
|
||||
{
|
||||
"uuid": "55ac73b8-4378-4b58-bf3c-33f64590c804",
|
||||
"title": "Restart / game reset button",
|
||||
"description": "",
|
||||
"category": "bf4ec62e-526e-4027-876f-0b22bd17f79a",
|
||||
"steps": []
|
||||
}
|
||||
],
|
||||
"layout": {
|
||||
@@ -208,6 +254,9 @@
|
||||
],
|
||||
[
|
||||
"fab8a81b-7ca9-4026-96bb-ed66ea58ef2e"
|
||||
],
|
||||
[
|
||||
"0d743057-955a-473a-8123-a3f805505d5d"
|
||||
]
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ var xr_interface :XRInterface
|
||||
func _ready():
|
||||
RECIPE_MANAGER.print_all_recipes()
|
||||
|
||||
GAME_MANAGER.meals_in_play.append("hamburger")
|
||||
GAME_MANAGER.meals_in_play = ["hamburger"]
|
||||
|
||||
xr_interface = XRServer.find_interface("OpenXR")
|
||||
if xr_interface and xr_interface.is_initialized():
|
||||
|
||||
@@ -27,6 +27,9 @@ ssao_enabled = true
|
||||
ssil_enabled = true
|
||||
sdfgi_enabled = true
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_kek77"]
|
||||
size = Vector3(5.1, 10, 0.1)
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||
script = ExtResource("1_0xm2m")
|
||||
|
||||
@@ -73,3 +76,21 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 1.482068, -1.2252241)
|
||||
|
||||
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("9_kek77")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.22971058, 1.699144, -1.8499806)
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=82945083]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=1909896160]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.688614, 2.5070028)
|
||||
shape = SubResource("BoxShape3D_kek77")
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="StaticBody3D" unique_id=39538720]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 4.688614, -2.5446289)
|
||||
shape = SubResource("BoxShape3D_kek77")
|
||||
|
||||
[node name="CollisionShape3D3" type="CollisionShape3D" parent="StaticBody3D" unique_id=1984048740]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -2.525816, 4.688614, -0.018813243)
|
||||
shape = SubResource("BoxShape3D_kek77")
|
||||
|
||||
[node name="CollisionShape3D4" type="CollisionShape3D" parent="StaticBody3D" unique_id=1484088721]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, 2.525816, 4.688614, -0.018813023)
|
||||
shape = SubResource("BoxShape3D_kek77")
|
||||
|
||||
+1
-301
@@ -248,306 +248,6 @@ binding_path = "/user/hand/right/output/haptic"
|
||||
interaction_profile_path = "/interaction_profiles/khr/generic_controller"
|
||||
bindings = [SubResource("OpenXRIPBinding_r3qn1"), SubResource("OpenXRIPBinding_n01b8"), SubResource("OpenXRIPBinding_pjtev"), SubResource("OpenXRIPBinding_nqyri"), SubResource("OpenXRIPBinding_86uui"), SubResource("OpenXRIPBinding_nrtxc"), SubResource("OpenXRIPBinding_qovyo"), SubResource("OpenXRIPBinding_d6uso"), SubResource("OpenXRIPBinding_hvi7v"), SubResource("OpenXRIPBinding_7dxun"), SubResource("OpenXRIPBinding_rp8ih"), SubResource("OpenXRIPBinding_0uca0"), SubResource("OpenXRIPBinding_rjtq8"), SubResource("OpenXRIPBinding_lce2q"), SubResource("OpenXRIPBinding_ckeh6"), SubResource("OpenXRIPBinding_538mi"), SubResource("OpenXRIPBinding_548p5"), SubResource("OpenXRIPBinding_6o0wr"), SubResource("OpenXRIPBinding_fsghu"), SubResource("OpenXRIPBinding_88umk"), SubResource("OpenXRIPBinding_4uneg"), SubResource("OpenXRIPBinding_67o31"), SubResource("OpenXRIPBinding_lf1a1"), SubResource("OpenXRIPBinding_x1adc"), SubResource("OpenXRIPBinding_j1vtv"), SubResource("OpenXRIPBinding_tud50")]
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_xri1r"]
|
||||
action = SubResource("OpenXRAction_oi0ij")
|
||||
binding_path = "/user/hand/left/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_etqcv"]
|
||||
action = SubResource("OpenXRAction_oi0ij")
|
||||
binding_path = "/user/hand/right/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_og5pg"]
|
||||
action = SubResource("OpenXRAction_m08eo")
|
||||
binding_path = "/user/hand/left/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_nwe40"]
|
||||
action = SubResource("OpenXRAction_m08eo")
|
||||
binding_path = "/user/hand/right/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ts2ff"]
|
||||
action = SubResource("OpenXRAction_c4j1d")
|
||||
binding_path = "/user/hand/left/input/grip/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_yhsv0"]
|
||||
action = SubResource("OpenXRAction_c4j1d")
|
||||
binding_path = "/user/hand/right/input/grip/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_sf2dt"]
|
||||
action = SubResource("OpenXRAction_sopde")
|
||||
binding_path = "/user/hand/left/input/grip_surface/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_67dwi"]
|
||||
action = SubResource("OpenXRAction_sopde")
|
||||
binding_path = "/user/hand/right/input/grip_surface/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_hswdx"]
|
||||
action = SubResource("OpenXRAction_iphn4")
|
||||
binding_path = "/user/hand/left/input/menu/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_7gr0f"]
|
||||
action = SubResource("OpenXRAction_iphn4")
|
||||
binding_path = "/user/hand/right/input/system/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_uvspk"]
|
||||
action = SubResource("OpenXRAction_wdehm")
|
||||
binding_path = "/user/hand/left/input/x/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ica2g"]
|
||||
action = SubResource("OpenXRAction_wdehm")
|
||||
binding_path = "/user/hand/right/input/a/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_5fecu"]
|
||||
action = SubResource("OpenXRAction_clfly")
|
||||
binding_path = "/user/hand/left/input/x/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_0nfxl"]
|
||||
action = SubResource("OpenXRAction_clfly")
|
||||
binding_path = "/user/hand/right/input/a/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_sbe0d"]
|
||||
action = SubResource("OpenXRAction_e1frq")
|
||||
binding_path = "/user/hand/left/input/y/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_rf1ko"]
|
||||
action = SubResource("OpenXRAction_e1frq")
|
||||
binding_path = "/user/hand/right/input/b/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_jx7ge"]
|
||||
action = SubResource("OpenXRAction_l7aq8")
|
||||
binding_path = "/user/hand/left/input/y/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_d2w1t"]
|
||||
action = SubResource("OpenXRAction_l7aq8")
|
||||
binding_path = "/user/hand/right/input/b/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_v2kct"]
|
||||
action = SubResource("OpenXRAction_6ivru")
|
||||
binding_path = "/user/hand/left/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_37uq4"]
|
||||
action = SubResource("OpenXRAction_6ivru")
|
||||
binding_path = "/user/hand/right/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_kooyb"]
|
||||
action = SubResource("OpenXRAction_vfhwq")
|
||||
binding_path = "/user/hand/left/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_51qre"]
|
||||
action = SubResource("OpenXRAction_vfhwq")
|
||||
binding_path = "/user/hand/right/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_fncxp"]
|
||||
action = SubResource("OpenXRAction_5w03k")
|
||||
binding_path = "/user/hand/left/input/trigger/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_qi50k"]
|
||||
action = SubResource("OpenXRAction_5w03k")
|
||||
binding_path = "/user/hand/right/input/trigger/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_h5icu"]
|
||||
action = SubResource("OpenXRAction_typ1r")
|
||||
binding_path = "/user/hand/left/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_b1sv6"]
|
||||
action = SubResource("OpenXRAction_typ1r")
|
||||
binding_path = "/user/hand/right/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_yu2t6"]
|
||||
action = SubResource("OpenXRAction_clvbf")
|
||||
binding_path = "/user/hand/left/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_labib"]
|
||||
action = SubResource("OpenXRAction_clvbf")
|
||||
binding_path = "/user/hand/right/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_altuc"]
|
||||
action = SubResource("OpenXRAction_3k6la")
|
||||
binding_path = "/user/hand/left/input/thumbstick"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_7p0fp"]
|
||||
action = SubResource("OpenXRAction_3k6la")
|
||||
binding_path = "/user/hand/right/input/thumbstick"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_yjnix"]
|
||||
action = SubResource("OpenXRAction_i8esw")
|
||||
binding_path = "/user/hand/left/input/thumbstick/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_pgh0x"]
|
||||
action = SubResource("OpenXRAction_i8esw")
|
||||
binding_path = "/user/hand/right/input/thumbstick/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_lplyu"]
|
||||
action = SubResource("OpenXRAction_um1hv")
|
||||
binding_path = "/user/hand/left/input/thumbstick/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ifnya"]
|
||||
action = SubResource("OpenXRAction_um1hv")
|
||||
binding_path = "/user/hand/right/input/thumbstick/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_jl4vo"]
|
||||
action = SubResource("OpenXRAction_sow2k")
|
||||
binding_path = "/user/hand/left/output/haptic"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_1n6j6"]
|
||||
action = SubResource("OpenXRAction_sow2k")
|
||||
binding_path = "/user/hand/right/output/haptic"
|
||||
|
||||
[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_o1nfs"]
|
||||
interaction_profile_path = "/interaction_profiles/oculus/touch_controller"
|
||||
bindings = [SubResource("OpenXRIPBinding_xri1r"), SubResource("OpenXRIPBinding_etqcv"), SubResource("OpenXRIPBinding_og5pg"), SubResource("OpenXRIPBinding_nwe40"), SubResource("OpenXRIPBinding_ts2ff"), SubResource("OpenXRIPBinding_yhsv0"), SubResource("OpenXRIPBinding_sf2dt"), SubResource("OpenXRIPBinding_67dwi"), SubResource("OpenXRIPBinding_hswdx"), SubResource("OpenXRIPBinding_7gr0f"), SubResource("OpenXRIPBinding_uvspk"), SubResource("OpenXRIPBinding_ica2g"), SubResource("OpenXRIPBinding_5fecu"), SubResource("OpenXRIPBinding_0nfxl"), SubResource("OpenXRIPBinding_sbe0d"), SubResource("OpenXRIPBinding_rf1ko"), SubResource("OpenXRIPBinding_jx7ge"), SubResource("OpenXRIPBinding_d2w1t"), SubResource("OpenXRIPBinding_v2kct"), SubResource("OpenXRIPBinding_37uq4"), SubResource("OpenXRIPBinding_kooyb"), SubResource("OpenXRIPBinding_51qre"), SubResource("OpenXRIPBinding_fncxp"), SubResource("OpenXRIPBinding_qi50k"), SubResource("OpenXRIPBinding_h5icu"), SubResource("OpenXRIPBinding_b1sv6"), SubResource("OpenXRIPBinding_yu2t6"), SubResource("OpenXRIPBinding_labib"), SubResource("OpenXRIPBinding_altuc"), SubResource("OpenXRIPBinding_7p0fp"), SubResource("OpenXRIPBinding_yjnix"), SubResource("OpenXRIPBinding_pgh0x"), SubResource("OpenXRIPBinding_lplyu"), SubResource("OpenXRIPBinding_ifnya"), SubResource("OpenXRIPBinding_jl4vo"), SubResource("OpenXRIPBinding_1n6j6")]
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_unnrh"]
|
||||
action = SubResource("OpenXRAction_oi0ij")
|
||||
binding_path = "/user/hand/left/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_3wafl"]
|
||||
action = SubResource("OpenXRAction_oi0ij")
|
||||
binding_path = "/user/hand/right/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_tjb53"]
|
||||
action = SubResource("OpenXRAction_m08eo")
|
||||
binding_path = "/user/hand/left/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_lcg2b"]
|
||||
action = SubResource("OpenXRAction_m08eo")
|
||||
binding_path = "/user/hand/right/input/aim/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_sp6l2"]
|
||||
action = SubResource("OpenXRAction_c4j1d")
|
||||
binding_path = "/user/hand/left/input/grip/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_xj6ir"]
|
||||
action = SubResource("OpenXRAction_c4j1d")
|
||||
binding_path = "/user/hand/right/input/grip/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_igmf3"]
|
||||
action = SubResource("OpenXRAction_sopde")
|
||||
binding_path = "/user/hand/left/input/grip_surface/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_inw5v"]
|
||||
action = SubResource("OpenXRAction_sopde")
|
||||
binding_path = "/user/hand/right/input/grip_surface/pose"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_iy2wq"]
|
||||
action = SubResource("OpenXRAction_3p2as")
|
||||
binding_path = "/user/hand/left/input/system/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_plu03"]
|
||||
action = SubResource("OpenXRAction_3p2as")
|
||||
binding_path = "/user/hand/right/input/system/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_dad45"]
|
||||
action = SubResource("OpenXRAction_iphn4")
|
||||
binding_path = "/user/hand/left/input/menu/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_m5e8q"]
|
||||
action = SubResource("OpenXRAction_wdehm")
|
||||
binding_path = "/user/hand/left/input/x/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_5t7jh"]
|
||||
action = SubResource("OpenXRAction_wdehm")
|
||||
binding_path = "/user/hand/right/input/a/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_chplt"]
|
||||
action = SubResource("OpenXRAction_clfly")
|
||||
binding_path = "/user/hand/left/input/x/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_obxrh"]
|
||||
action = SubResource("OpenXRAction_clfly")
|
||||
binding_path = "/user/hand/right/input/a/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_on7oi"]
|
||||
action = SubResource("OpenXRAction_e1frq")
|
||||
binding_path = "/user/hand/left/input/y/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ege4h"]
|
||||
action = SubResource("OpenXRAction_e1frq")
|
||||
binding_path = "/user/hand/right/input/b/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_h7ix0"]
|
||||
action = SubResource("OpenXRAction_l7aq8")
|
||||
binding_path = "/user/hand/left/input/y/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_8qanm"]
|
||||
action = SubResource("OpenXRAction_l7aq8")
|
||||
binding_path = "/user/hand/right/input/b/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_3senm"]
|
||||
action = SubResource("OpenXRAction_6ivru")
|
||||
binding_path = "/user/hand/left/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_7ca55"]
|
||||
action = SubResource("OpenXRAction_6ivru")
|
||||
binding_path = "/user/hand/right/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ih1l2"]
|
||||
action = SubResource("OpenXRAction_vfhwq")
|
||||
binding_path = "/user/hand/left/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ipewn"]
|
||||
action = SubResource("OpenXRAction_vfhwq")
|
||||
binding_path = "/user/hand/right/input/trigger/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_5ngl7"]
|
||||
action = SubResource("OpenXRAction_5w03k")
|
||||
binding_path = "/user/hand/left/input/trigger/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_klygg"]
|
||||
action = SubResource("OpenXRAction_5w03k")
|
||||
binding_path = "/user/hand/right/input/trigger/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_4p63k"]
|
||||
action = SubResource("OpenXRAction_typ1r")
|
||||
binding_path = "/user/hand/left/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_6vi2m"]
|
||||
action = SubResource("OpenXRAction_typ1r")
|
||||
binding_path = "/user/hand/right/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_888d1"]
|
||||
action = SubResource("OpenXRAction_clvbf")
|
||||
binding_path = "/user/hand/left/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_246v5"]
|
||||
action = SubResource("OpenXRAction_clvbf")
|
||||
binding_path = "/user/hand/right/input/squeeze/value"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_xj73r"]
|
||||
action = SubResource("OpenXRAction_3k6la")
|
||||
binding_path = "/user/hand/left/input/thumbstick"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_sugej"]
|
||||
action = SubResource("OpenXRAction_3k6la")
|
||||
binding_path = "/user/hand/right/input/thumbstick"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_fp7u7"]
|
||||
action = SubResource("OpenXRAction_i8esw")
|
||||
binding_path = "/user/hand/left/input/thumbstick/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_gvgeq"]
|
||||
action = SubResource("OpenXRAction_i8esw")
|
||||
binding_path = "/user/hand/right/input/thumbstick/click"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_i0s8c"]
|
||||
action = SubResource("OpenXRAction_um1hv")
|
||||
binding_path = "/user/hand/left/input/thumbstick/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_ynetq"]
|
||||
action = SubResource("OpenXRAction_um1hv")
|
||||
binding_path = "/user/hand/right/input/thumbstick/touch"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_p8bcx"]
|
||||
action = SubResource("OpenXRAction_sow2k")
|
||||
binding_path = "/user/hand/left/output/haptic"
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_jkemj"]
|
||||
action = SubResource("OpenXRAction_sow2k")
|
||||
binding_path = "/user/hand/right/output/haptic"
|
||||
|
||||
[sub_resource type="OpenXRInteractionProfile" id="OpenXRInteractionProfile_d3nfp"]
|
||||
interaction_profile_path = "/interaction_profiles/bytedance/pico4_controller"
|
||||
bindings = [SubResource("OpenXRIPBinding_unnrh"), SubResource("OpenXRIPBinding_3wafl"), SubResource("OpenXRIPBinding_tjb53"), SubResource("OpenXRIPBinding_lcg2b"), SubResource("OpenXRIPBinding_sp6l2"), SubResource("OpenXRIPBinding_xj6ir"), SubResource("OpenXRIPBinding_igmf3"), SubResource("OpenXRIPBinding_inw5v"), SubResource("OpenXRIPBinding_iy2wq"), SubResource("OpenXRIPBinding_plu03"), SubResource("OpenXRIPBinding_dad45"), SubResource("OpenXRIPBinding_m5e8q"), SubResource("OpenXRIPBinding_5t7jh"), SubResource("OpenXRIPBinding_chplt"), SubResource("OpenXRIPBinding_obxrh"), SubResource("OpenXRIPBinding_on7oi"), SubResource("OpenXRIPBinding_ege4h"), SubResource("OpenXRIPBinding_h7ix0"), SubResource("OpenXRIPBinding_8qanm"), SubResource("OpenXRIPBinding_3senm"), SubResource("OpenXRIPBinding_7ca55"), SubResource("OpenXRIPBinding_ih1l2"), SubResource("OpenXRIPBinding_ipewn"), SubResource("OpenXRIPBinding_5ngl7"), SubResource("OpenXRIPBinding_klygg"), SubResource("OpenXRIPBinding_4p63k"), SubResource("OpenXRIPBinding_6vi2m"), SubResource("OpenXRIPBinding_888d1"), SubResource("OpenXRIPBinding_246v5"), SubResource("OpenXRIPBinding_xj73r"), SubResource("OpenXRIPBinding_sugej"), SubResource("OpenXRIPBinding_fp7u7"), SubResource("OpenXRIPBinding_gvgeq"), SubResource("OpenXRIPBinding_i0s8c"), SubResource("OpenXRIPBinding_ynetq"), SubResource("OpenXRIPBinding_p8bcx"), SubResource("OpenXRIPBinding_jkemj")]
|
||||
|
||||
[sub_resource type="OpenXRIPBinding" id="OpenXRIPBinding_jyu76"]
|
||||
action = SubResource("OpenXRAction_oi0ij")
|
||||
binding_path = "/user/hand/left/input/aim/pose"
|
||||
@@ -618,4 +318,4 @@ bindings = [SubResource("OpenXRIPBinding_jyu76"), SubResource("OpenXRIPBinding_a
|
||||
|
||||
[resource]
|
||||
action_sets = [SubResource("OpenXRActionSet_ngwcy")]
|
||||
interaction_profiles = [SubResource("OpenXRInteractionProfile_akdt0"), SubResource("OpenXRInteractionProfile_o1nfs"), SubResource("OpenXRInteractionProfile_d3nfp"), SubResource("OpenXRInteractionProfile_m1cgb")]
|
||||
interaction_profiles = [SubResource("OpenXRInteractionProfile_akdt0"), SubResource("OpenXRInteractionProfile_m1cgb")]
|
||||
|
||||
+22
-1
@@ -11,7 +11,7 @@ config_version=5
|
||||
[application]
|
||||
|
||||
config/name="GodotVR2"
|
||||
run/main_scene="uid://clw8ai6kngqxb"
|
||||
run/main_scene="uid://c1nv4w33fedj6"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://icon.svg"
|
||||
|
||||
@@ -19,9 +19,17 @@ config/icon="res://icon.svg"
|
||||
|
||||
XRToolsUserSettings="*uid://bqgb8i74tm0t"
|
||||
XRToolsRumbleManager="*uid://by853dk86g1qw"
|
||||
NetworkManager="*res://Net/network_manager.gd"
|
||||
GlobalKeyEvents="*uid://c60unagog5oi1"
|
||||
|
||||
[debug]
|
||||
|
||||
file_logging/enable_file_logging=true
|
||||
|
||||
[display]
|
||||
|
||||
window/size/viewport_width=1800
|
||||
window/size/viewport_height=650
|
||||
window/stretch/mode="canvas_items"
|
||||
window/stretch/aspect="expand"
|
||||
|
||||
@@ -38,6 +46,19 @@ import/blender/enabled=false
|
||||
platalbe_item="Meals, sides, augments for meals"
|
||||
station="Hobs, Counters ..."
|
||||
|
||||
[input]
|
||||
|
||||
quit_game={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194305,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
satisfy_table_orders={
|
||||
"deadzone": 0.2,
|
||||
"events": [Object(InputEventKey,"resource_local_to_scene":false,"resource_name":"","device":-1,"window_id":0,"alt_pressed":false,"shift_pressed":false,"ctrl_pressed":false,"meta_pressed":false,"pressed":false,"keycode":0,"physical_keycode":4194309,"key_label":0,"unicode":0,"location":0,"echo":false,"script":null)
|
||||
]
|
||||
}
|
||||
|
||||
[layer_names]
|
||||
|
||||
3d_physics/layer_1="Static World"
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
uid://dqqu56yetl8ok
|
||||
@@ -0,0 +1,89 @@
|
||||
# Stitches the per-step screenshots from a test run into one side-by-side GIF,
|
||||
# server on the left, client on the right.
|
||||
#
|
||||
# powershell -File test\make_gif.ps1
|
||||
#
|
||||
# Reads logs\mptest_frames_server\ and logs\mptest_frames_client\ (written when
|
||||
# the driver runs with --mptest-frames) and writes logs\mptest_run.gif.
|
||||
# run_mp_test_windowed.ps1 calls this automatically; run it by hand to rebuild
|
||||
# the GIF after a manual session, or to re-render at a different speed.
|
||||
#
|
||||
# Each frame already carries its own caption: the on-screen overlay in the
|
||||
# capture starts with [SERVER] or [CLIENT] and shows that step's log lines.
|
||||
|
||||
param(
|
||||
[string]$FFmpeg = "ffmpeg",
|
||||
# Seconds each step is held on screen.
|
||||
[double]$SecondsPerStep = 1.2,
|
||||
# Width of each peer's half of the frame, in pixels.
|
||||
[int]$HalfWidth = 900,
|
||||
[string]$Out = ""
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$proj = Split-Path -Parent $PSScriptRoot
|
||||
$serverDir = Join-Path $proj "logs/mptest_frames_server"
|
||||
$clientDir = Join-Path $proj "logs/mptest_frames_client"
|
||||
if (-not $Out) { $Out = Join-Path $proj "logs/mptest_run.gif" }
|
||||
|
||||
if (-not (Get-Command $FFmpeg -EA SilentlyContinue)) {
|
||||
Write-Host "ffmpeg not found. Install it, or pass -FFmpeg <path to ffmpeg.exe>."
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Frame-Count($dir) {
|
||||
if (-not (Test-Path $dir)) { return 0 }
|
||||
return (Get-ChildItem (Join-Path $dir "frame_*.png") -EA SilentlyContinue).Count
|
||||
}
|
||||
|
||||
$ns = Frame-Count $serverDir
|
||||
$nc = Frame-Count $clientDir
|
||||
Write-Host "server frames: $ns"
|
||||
Write-Host "client frames: $nc"
|
||||
|
||||
if ($ns -eq 0 -and $nc -eq 0) {
|
||||
Write-Host ""
|
||||
Write-Host "No frames found. Run the test with frame capture first:"
|
||||
Write-Host " powershell -File test\run_mp_test_windowed.ps1"
|
||||
Write-Host "(frame capture needs a real window - a headless run renders nothing to grab)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
$fps = [Math]::Round(1.0 / $SecondsPerStep, 4)
|
||||
|
||||
if ($ns -gt 0 -and $nc -gt 0) {
|
||||
if ($ns -ne $nc) {
|
||||
# hstack stops at the shorter input, so the tail of the longer one is lost.
|
||||
Write-Host "note: frame counts differ, the GIF will stop after $([Math]::Min($ns,$nc)) steps"
|
||||
}
|
||||
# Two passes in one command: build a palette from the stacked frames, then
|
||||
# apply it. A single global palette keeps the GIF small and stops colours
|
||||
# shifting from frame to frame.
|
||||
$filter = "[0:v]scale=${HalfWidth}:-1:flags=lanczos[l];" +
|
||||
"[1:v]scale=${HalfWidth}:-1:flags=lanczos[r];" +
|
||||
"[l][r]hstack=inputs=2[v];" +
|
||||
"[v]split[a][b];[a]palettegen=max_colors=192[p];[b][p]paletteuse=dither=bayer:bayer_scale=3"
|
||||
$args = @(
|
||||
"-y", "-hide_banner", "-loglevel", "error",
|
||||
"-framerate", $fps, "-i", (Join-Path $serverDir "frame_%04d.png"),
|
||||
"-framerate", $fps, "-i", (Join-Path $clientDir "frame_%04d.png"),
|
||||
"-filter_complex", $filter, "-loop", "0", $Out
|
||||
)
|
||||
} else {
|
||||
# Only one peer produced frames (e.g. a solo manual session).
|
||||
$dir = if ($ns -gt 0) { $serverDir } else { $clientDir }
|
||||
$filter = "[0:v]scale=${HalfWidth}:-1:flags=lanczos[v];" +
|
||||
"[v]split[a][b];[a]palettegen=max_colors=192[p];[b][p]paletteuse=dither=bayer:bayer_scale=3"
|
||||
$args = @(
|
||||
"-y", "-hide_banner", "-loglevel", "error",
|
||||
"-framerate", $fps, "-i", (Join-Path $dir "frame_%04d.png"),
|
||||
"-filter_complex", $filter, "-loop", "0", $Out
|
||||
)
|
||||
}
|
||||
|
||||
& $FFmpeg @args
|
||||
if ($LASTEXITCODE -ne 0) { Write-Host "ffmpeg failed ($LASTEXITCODE)"; exit $LASTEXITCODE }
|
||||
|
||||
$size = [Math]::Round((Get-Item $Out).Length / 1MB, 2)
|
||||
Write-Host ""
|
||||
Write-Host "wrote $Out (${size} MB)"
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
uid://biu4qr3nr1eny
|
||||
@@ -0,0 +1,47 @@
|
||||
# Shared helper for placing the two game windows side by side.
|
||||
#
|
||||
# Godot's --position flag is ignored on this setup (the window lands at x=-7
|
||||
# whatever you pass), so the windows are moved with the Win32 API after they
|
||||
# come up. Dot-source this file to get Move-GameWindow.
|
||||
|
||||
if (-not ('MpWin' -as [type])) {
|
||||
Add-Type @"
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
public class MpWin {
|
||||
[DllImport("user32.dll")] public static extern bool SetWindowPos(IntPtr h, IntPtr after, int x, int y, int cx, int cy, uint flags);
|
||||
[DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr h, out RECT r);
|
||||
[DllImport("user32.dll")] public static extern bool SetProcessDPIAware();
|
||||
public struct RECT { public int Left, Top, Right, Bottom; }
|
||||
}
|
||||
"@
|
||||
}
|
||||
|
||||
# Godot's windows are DPI aware; PowerShell's process is not by default, so
|
||||
# GetWindowRect/SetWindowPos would otherwise be talking in virtualised
|
||||
# coordinates and the windows land nowhere near where we asked.
|
||||
[void][MpWin]::SetProcessDPIAware()
|
||||
|
||||
# Moves a just-launched game window to (x, y) and returns its width, so the
|
||||
# caller can place the next window immediately to its right. Never resizes:
|
||||
# changing the window size independently of Godot's --resolution distorts the
|
||||
# rendered aspect ratio.
|
||||
function Move-GameWindow($proc, [int]$x, [int]$y) {
|
||||
for ($i = 0; $i -lt 60 -and $proc.MainWindowHandle -eq 0; $i++) {
|
||||
Start-Sleep -Milliseconds 250
|
||||
$proc.Refresh()
|
||||
}
|
||||
if ($proc.MainWindowHandle -eq 0) {
|
||||
Write-Host " (window never appeared; leaving it where it is)"
|
||||
return 620
|
||||
}
|
||||
# The handle shows up before Godot has finished sizing/positioning the
|
||||
# window - moving it too early gets overwritten by Godot's own setup.
|
||||
Start-Sleep -Milliseconds 2500
|
||||
$h = $proc.MainWindowHandle
|
||||
# SWP_NOSIZE (0x1) | SWP_NOZORDER (0x4)
|
||||
[void][MpWin]::SetWindowPos($h, [IntPtr]::Zero, $x, $y, 0, 0, 0x0005)
|
||||
$r = New-Object MpWin+RECT
|
||||
[void][MpWin]::GetWindowRect($h, [ref]$r)
|
||||
return [int]($r.Right - $r.Left)
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
[gd_scene format=3 uid="uid://bodj8op527o2c"]
|
||||
|
||||
[ext_resource type="Script" uid="uid://d1cefhe3yyyds" path="res://Scenes/multiplayer_world.gd" id="1_6uucx"]
|
||||
[ext_resource type="PackedScene" uid="uid://j5s5wus7tuhb" path="res://XROrigin.tscn" id="2_j7vd1"]
|
||||
[ext_resource type="Material" uid="uid://cmia50cfqxxo4" path="res://Textures/dev_material_3d.tres" id="3_ssbaf"]
|
||||
[ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="4_081u3"]
|
||||
[ext_resource type="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="5_t1fa7"]
|
||||
[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="6_6jhmh"]
|
||||
[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="7_bowes"]
|
||||
[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="8_pvl84"]
|
||||
[ext_resource type="Script" uid="uid://biu4qr3nr1eny" path="res://test/mp_test_driver.gd" id="9_mptst"]
|
||||
[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="10_51k0c"]
|
||||
[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="11_psgbv"]
|
||||
[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="12_j5uvh"]
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"]
|
||||
size = Vector3(15, 0.1, 15)
|
||||
|
||||
[sub_resource type="BoxMesh" id="BoxMesh_24d3s"]
|
||||
material = ExtResource("3_ssbaf")
|
||||
size = Vector3(15, 0.1, 15)
|
||||
|
||||
[sub_resource type="Environment" id="Environment_bvwq1"]
|
||||
background_mode = 2
|
||||
sky = ExtResource("4_081u3")
|
||||
reflected_light_source = 2
|
||||
ssr_enabled = true
|
||||
ssao_enabled = true
|
||||
ssil_enabled = true
|
||||
sdfgi_enabled = true
|
||||
|
||||
[sub_resource type="BoxShape3D" id="BoxShape3D_arao0"]
|
||||
size = Vector3(15, 20, 0.1)
|
||||
|
||||
[node name="Main" type="Node3D" unique_id=1312265607]
|
||||
script = ExtResource("1_6uucx")
|
||||
populate_from_layout = false
|
||||
|
||||
[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)
|
||||
|
||||
[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="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
|
||||
environment = SubResource("Environment_bvwq1")
|
||||
|
||||
[node name="WorldContent" type="Node3D" parent="." unique_id=291550153]
|
||||
|
||||
[node name="Players" type="Node3D" parent="." unique_id=1595463693]
|
||||
|
||||
[node name="ItemsSpawner" type="MultiplayerSpawner" parent="." unique_id=627119248]
|
||||
spawn_path = NodePath("../WorldContent")
|
||||
|
||||
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
|
||||
_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
|
||||
spawn_path = NodePath("../Players")
|
||||
|
||||
[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=4404969]
|
||||
|
||||
[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=998476672]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.197384, 7.589958)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[node name="CollisionShape3D2" type="CollisionShape3D" parent="StaticBody3D" unique_id=340303902]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 9.197384, -7.509142)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[node name="CollisionShape3D3" type="CollisionShape3D" parent="StaticBody3D" unique_id=1193703037]
|
||||
transform = Transform3D(-4.371139e-08, 0, -1, 0, 1, 0, 1, 0, -4.371139e-08, -7.438612, 9.197384, -0.018813243)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[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)
|
||||
shape = SubResource("BoxShape3D_arao0")
|
||||
|
||||
[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("5_t1fa7")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5, 0)
|
||||
|
||||
[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("6_6jhmh")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0.5, 0)
|
||||
|
||||
[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("7_bowes")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.4050963, 1.1702834, 0.049627244)
|
||||
|
||||
[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("8_pvl84")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2, 0.5, 0)
|
||||
|
||||
[node name="TestDriver" type="Node" parent="." unique_id=1002011928]
|
||||
script = ExtResource("9_mptst")
|
||||
|
||||
[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)
|
||||
|
||||
[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)
|
||||
|
||||
[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)
|
||||
|
||||
[node name="Counter2" parent="." unique_id=860178119 instance=ExtResource("12_j5uvh")]
|
||||
transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2, 0.5, 0)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Opens two windows on the multiplayer test scene in MANUAL mode, so you can
|
||||
# drive the plate / dirt-station flow yourself and watch both peers.
|
||||
#
|
||||
# powershell -File test\play_mp_test.ps1
|
||||
#
|
||||
# No scripted sequence runs. Click a window to focus it, then:
|
||||
#
|
||||
# 1 grab the plate 2 drop it
|
||||
# 3 carry it to the dirt zone 4 drop it at the dirt zone
|
||||
# 5 check it snapped + went dirty
|
||||
# 6 run the whole automatic sequence (server window only)
|
||||
# 0 dump the current state of everything
|
||||
# C toggle between the fixed debug camera and the XR rig camera
|
||||
#
|
||||
# Keys act on whichever window has focus, so you can grab on the CLIENT and
|
||||
# watch the SERVER window follow. Each window opens on a fixed camera looking
|
||||
# at the test area, overlays its own step log, and writes it to
|
||||
# logs\mptest_server.log / logs\mptest_client.log.
|
||||
#
|
||||
# The typical repro: on the CLIENT press 1, 2 (grab and drop), then on the
|
||||
# SERVER press 1, 2. Then on the CLIENT press 1, 3, 4 and press 5 on both.
|
||||
|
||||
param(
|
||||
[string]$Godot = "C:\Users\Aqua Aurora\Desktop\Godot_v4.7-stable_win64.exe",
|
||||
# Pass -Solo to open a single window with no networking, to compare the
|
||||
# same steps against single-player behaviour.
|
||||
[switch]$Solo
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$proj = Split-Path -Parent $PSScriptRoot
|
||||
$scene = "res://test/multiPlayerTest.tscn"
|
||||
|
||||
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
||||
|
||||
function Start-Instance($extraArgs) {
|
||||
$a = "--xr-mode off --resolution 900x600 --path `"$proj`" $scene"
|
||||
if ($extraArgs) { $a += " -- $($extraArgs -join ' ')" }
|
||||
return Start-Process -FilePath $Godot -ArgumentList $a -PassThru
|
||||
}
|
||||
|
||||
if ($Solo) {
|
||||
Write-Host "Opening a single offline window (no networking)."
|
||||
$null = Move-GameWindow (Start-Instance $null) 300 100
|
||||
return
|
||||
}
|
||||
|
||||
Write-Host "Opening SERVER window (left)..."
|
||||
$w = Move-GameWindow (Start-Instance @("--server")) 20 60
|
||||
Start-Sleep -Seconds 5
|
||||
Write-Host "Opening CLIENT window (right)..."
|
||||
$null = Move-GameWindow (Start-Instance @("--join", "127.0.0.1")) (20 + $w + 12) 60
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Both windows are up. Click one to focus it, then press:"
|
||||
Write-Host " 1 grab 2 drop 3 carry-to-dirt 4 drop-at-dirt 5 verify 0 dump"
|
||||
Write-Host " 6 runs the whole automatic sequence (server window only) C toggles the camera"
|
||||
Write-Host ""
|
||||
Write-Host "Step logs: $(Join-Path $proj 'logs\mptest_server.log')"
|
||||
Write-Host " $(Join-Path $proj 'logs\mptest_client.log')"
|
||||
Write-Host "Close the windows when you're done (Esc quits)."
|
||||
@@ -0,0 +1,60 @@
|
||||
# Runs the headless two-instance multiplayer test (test/multiPlayerTest.tscn).
|
||||
#
|
||||
# powershell -File test\run_mp_test.ps1
|
||||
#
|
||||
# Starts a server instance and a client instance of the game with --xr-mode off
|
||||
# (SteamVR's OpenXR runtime crashes a headless process), lets test/mp_test_driver.gd
|
||||
# drive the scripted plate/dirt-station sequence, then prints both logs.
|
||||
# Exits non-zero if any check failed.
|
||||
|
||||
param(
|
||||
[string]$Godot = "C:\Users\Aqua Aurora\Desktop\Godot_v4.7-stable_win64.exe",
|
||||
[int]$TimeoutSec = 120
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$proj = Split-Path -Parent $PSScriptRoot
|
||||
$scene = "res://test/multiPlayerTest.tscn"
|
||||
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
||||
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
||||
|
||||
foreach ($f in @($serverLog, $clientLog)) { if (Test-Path $f) { Remove-Item $f -Force } }
|
||||
|
||||
function Start-Instance($extraArgs) {
|
||||
# Single argument string with the project path quoted: Start-Process does
|
||||
# not quote array elements, so the space in the path would split it.
|
||||
$a = "--headless --xr-mode off --path `"$proj`" $scene -- $($extraArgs -join ' ')"
|
||||
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru -NoNewWindow
|
||||
# Touching .Handle caches it, which is what makes .ExitCode readable later;
|
||||
# without this it comes back empty even after the process has exited.
|
||||
$null = $p.Handle
|
||||
return $p
|
||||
}
|
||||
|
||||
Write-Host "Starting server..."
|
||||
$server = Start-Instance @("--server", "--mptest")
|
||||
Start-Sleep -Seconds 4
|
||||
Write-Host "Starting client..."
|
||||
$client = Start-Instance @("--join", "127.0.0.1", "--mptest")
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
while ((Get-Date) -lt $deadline -and -not $server.HasExited) { Start-Sleep -Milliseconds 500 }
|
||||
|
||||
foreach ($p in @($server, $client)) {
|
||||
if (-not $p.HasExited) { Write-Host "Killing pid $($p.Id) (still running)"; $p.Kill() }
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
foreach ($pair in @(@("SERVER", $serverLog), @("CLIENT", $clientLog))) {
|
||||
Write-Host ""
|
||||
Write-Host "======================== $($pair[0]) ========================"
|
||||
if (Test-Path $pair[1]) { Get-Content $pair[1] -Encoding UTF8 } else { Write-Host "(no log written)" }
|
||||
}
|
||||
|
||||
# ExitCode is only populated on the process object after a WaitForExit() call,
|
||||
# even when HasExited is already true - without this it reads back empty.
|
||||
$server.WaitForExit(2000) | Out-Null
|
||||
$code = if ($server.HasExited) { $server.ExitCode } else { 1 }
|
||||
Write-Host ""
|
||||
Write-Host "server exit code: $code"
|
||||
exit $code
|
||||
@@ -0,0 +1,82 @@
|
||||
# Runs the two-instance multiplayer test in two VISIBLE windows, side by side,
|
||||
# pausing between steps so you can watch what happens on each peer.
|
||||
#
|
||||
# powershell -File test\run_mp_test_windowed.ps1
|
||||
#
|
||||
# Same test as run_mp_test.ps1 (headless); this one is for watching it. Each
|
||||
# window shows an on-screen overlay of the step log for that peer.
|
||||
#
|
||||
# To drive it yourself instead, see test\play_mp_test.ps1 (manual mode).
|
||||
|
||||
param(
|
||||
[string]$Godot = "C:\Users\Aqua Aurora\Desktop\Godot_v4.7-stable_win64.exe",
|
||||
# Seconds to pause between steps, and to hold the final state on screen.
|
||||
[double]$Pause = 1.5,
|
||||
[double]$Hold = 20,
|
||||
[int]$TimeoutSec = 300,
|
||||
# Passed through to make_gif.ps1. HalfWidth 900 keeps the on-screen
|
||||
# step log readable in the GIF; lower it to shrink the file.
|
||||
[int]$HalfWidth = 900,
|
||||
[double]$SecondsPerStep = 1.2,
|
||||
# Skip building the GIF (frames are still captured).
|
||||
[switch]$NoGif
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$proj = Split-Path -Parent $PSScriptRoot
|
||||
$scene = "res://test/multiPlayerTest.tscn"
|
||||
$serverLog = Join-Path $proj "logs/mptest_server.log"
|
||||
$clientLog = Join-Path $proj "logs/mptest_client.log"
|
||||
|
||||
foreach ($f in @($serverLog, $clientLog)) { if (Test-Path $f) { Remove-Item $f -Force } }
|
||||
|
||||
. (Join-Path $PSScriptRoot "mp_window_layout.ps1")
|
||||
|
||||
function Start-Instance($extraArgs) {
|
||||
# --xr-mode off keeps it on the desktop (SteamVR's OpenXR runtime otherwise
|
||||
# takes over, and two instances can't share a headset anyway).
|
||||
$a = "--xr-mode off --resolution 900x600 --path `"$proj`" $scene -- " +
|
||||
"$($extraArgs -join ' ') --mptest --mptest-frames --mptest-pause $Pause --mptest-hold $Hold"
|
||||
$p = Start-Process -FilePath $Godot -ArgumentList $a -PassThru
|
||||
# Touching .Handle caches it, which is what makes .ExitCode readable later.
|
||||
$null = $p.Handle
|
||||
return $p
|
||||
}
|
||||
|
||||
Write-Host "Starting SERVER window (left)..."
|
||||
$server = Start-Instance @("--server")
|
||||
$w = Move-GameWindow $server 20 60
|
||||
Start-Sleep -Seconds 5
|
||||
Write-Host "Starting CLIENT window (right)..."
|
||||
$client = Start-Instance @("--join", "127.0.0.1")
|
||||
$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 "Step logs: $serverLog"
|
||||
Write-Host " $clientLog"
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSec)
|
||||
while ((Get-Date) -lt $deadline -and -not $server.HasExited) { Start-Sleep -Milliseconds 500 }
|
||||
|
||||
foreach ($p in @($server, $client)) {
|
||||
if (-not $p.HasExited) { Write-Host "Killing pid $($p.Id) (still running)"; $p.Kill() }
|
||||
}
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
foreach ($pair in @(@("SERVER", $serverLog), @("CLIENT", $clientLog))) {
|
||||
Write-Host ""
|
||||
Write-Host "======================== $($pair[0]) ========================"
|
||||
if (Test-Path $pair[1]) { Get-Content $pair[1] -Encoding UTF8 } else { Write-Host "(no log written)" }
|
||||
}
|
||||
|
||||
if (-not $NoGif) {
|
||||
Write-Host ""
|
||||
Write-Host "======================== GIF ========================"
|
||||
& powershell -ExecutionPolicy Bypass -File (Join-Path $PSScriptRoot "make_gif.ps1") `
|
||||
-HalfWidth $HalfWidth -SecondsPerStep $SecondsPerStep
|
||||
}
|
||||
|
||||
$server.WaitForExit(2000) | Out-Null
|
||||
$code = if ($server.HasExited) { $server.ExitCode } else { 1 }
|
||||
Write-Host ""
|
||||
Write-Host "server exit code: $code"
|
||||
exit $code
|
||||
Reference in New Issue
Block a user