From 596d777fae61c560f629ce202ded2dafaa25c2e3 Mon Sep 17 00:00:00 2001 From: algodoogle Date: Sat, 25 Jul 2026 19:26:43 +0100 Subject: [PATCH] asd --- Containers/container.gd | 141 +++++++++++++++++++++--------- Containers/plate.tscn | 23 +++++ Containers/plate_controller.gd | 21 ++++- Items/BurgerBuns.tscn | 17 ++++ Items/Charcoal.tscn | 17 ++++ Items/PickupCube.tscn | 17 ++++ Items/burger.tscn | 17 ++++ Items/cooked_burger.tscn | 17 ++++ Items/hamburger.tscn | 17 ++++ Net/net_pickable.gd | 74 ++++++++++++++++ Net/net_pickable.gd.uid | 1 + Net/network_manager.gd | 112 +++++++++++++++++++++--- Net/world_layout.gd | 81 +++++++++++++++++ Net/world_layout.gd.uid | 1 + Player/net_player.gd | 50 +++++++++++ Player/net_player.gd.uid | 1 + Player/net_player.tscn | 44 ++++++++++ Prefabs/combinable_item.gd | 18 ++-- Scenes/mainMenu.tscn | 2 +- Scenes/multiPlayer.tscn | 128 ++------------------------- Scenes/multiplayer_world.gd | 65 +++++++++++++- Stations/BurgerBunsDispenser.tscn | 1 - Stations/Hob.tscn | 12 +++ Stations/dirt_station.gd | 6 +- Stations/hob.gd | 17 ++-- Stations/item_dispenser.gd | 8 +- Stations/sink.tscn | 9 ++ Stations/table.gd | 113 +++++++++++++++--------- Stations/table.tscn | 15 ++++ UI/main_menu_panel.gd | 25 ++++++ UI/main_menu_panel.tscn | 17 ++-- main.gd | 2 +- 32 files changed, 832 insertions(+), 257 deletions(-) create mode 100644 Net/net_pickable.gd create mode 100644 Net/net_pickable.gd.uid create mode 100644 Net/world_layout.gd create mode 100644 Net/world_layout.gd.uid create mode 100644 Player/net_player.gd create mode 100644 Player/net_player.gd.uid create mode 100644 Player/net_player.tscn diff --git a/Containers/container.gd b/Containers/container.gd index 24b19f7..c5308eb 100644 --- a/Containers/container.gd +++ b/Containers/container.gd @@ -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") @@ -34,67 +38,118 @@ func _on_body_entered(body: Node3D) -> void: print("Container _on_body_entered body: ", body) var picked_by = xr_pickable.get_picked_up_by() if picked_by and picked_by.is_in_group("station"): - + # If compatible and enough space, add item 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(): - 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 - - item.reparent(container_root, false) # false = discard global transform - - # 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 - - # Add to list - var food_node = item.get_node_or_null("FoodItem") - if food_node: - contained_items.append(food_node as FoodItem) + if pickable and pickable.is_picked_up(): + pickable.drop() + + var food_node := item.get_node_or_null("FoodItem") as FoodItem + if not food_node: + return + + # Copy the data out before despawning the real item. + var data := FoodItem.new() + data.id = food_node.id + data.type = food_node.type + data.sell_value = food_node.sell_value + contained_items.append(data) + + var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController + if plate_controller: + plate_controller.contained_ids.append(food_node.id) + + 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() + contained_items.erase(item) + var plate_controller := xr_pickable.get_node_or_null("PlateController") as PlateController + if plate_controller: + plate_controller.contained_ids.erase(item.id) 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) + 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: + net_pickable.queue_free() + if visual is RigidBody3D: + visual.freeze = true + visual.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC + visual.collision_layer = 0 + visual.collision_mask = 0 + if visual is XRToolsPickable: + visual.enabled = false + visual.set_process(false) + visual.set_physics_process(false) #plate (Pickalbe) diff --git a/Containers/plate.tscn b/Containers/plate.tscn index 28c647f..15013f6 100644 --- a/Containers/plate.tscn +++ b/Containers/plate.tscn @@ -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") diff --git a/Containers/plate_controller.gd b/Containers/plate_controller.gd index d225a89..3f639da 100644 --- a/Containers/plate_controller.gd +++ b/Containers/plate_controller.gd @@ -5,7 +5,14 @@ extends Node @onready var container: ItemContainer = $"../Container" @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,15 @@ func _process(_delta: float) -> void: else: container.enabled = true dirty_node.visible = false + + +func _set_contained_ids(value: Array[String]) -> void: + contained_ids = value + # Deferred: this can be written by the replicated spawn payload before + # this node's own @onready vars (container) have resolved. + _refresh_visuals.call_deferred() + + +func _refresh_visuals() -> void: + if container: + container.refresh_visuals(contained_ids) diff --git a/Items/BurgerBuns.tscn b/Items/BurgerBuns.tscn index 3aef5fd..e479e36 100644 --- a/Items/BurgerBuns.tscn +++ b/Items/BurgerBuns.tscn @@ -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") diff --git a/Items/Charcoal.tscn b/Items/Charcoal.tscn index 4e4aa3b..e1a5523 100644 --- a/Items/Charcoal.tscn +++ b/Items/Charcoal.tscn @@ -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_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 @@ -33,6 +34,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 +66,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") diff --git a/Items/PickupCube.tscn b/Items/PickupCube.tscn index 91287d6..b00c9e2 100644 --- a/Items/PickupCube.tscn +++ b/Items/PickupCube.tscn @@ -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") diff --git a/Items/burger.tscn b/Items/burger.tscn index cdaff38..0d49d37 100644 --- a/Items/burger.tscn +++ b/Items/burger.tscn @@ -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") diff --git a/Items/cooked_burger.tscn b/Items/cooked_burger.tscn index 59941aa..d5ebd5c 100644 --- a/Items/cooked_burger.tscn +++ b/Items/cooked_burger.tscn @@ -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") diff --git a/Items/hamburger.tscn b/Items/hamburger.tscn index 0191c1e..3bddd7c 100644 --- a/Items/hamburger.tscn +++ b/Items/hamburger.tscn @@ -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") diff --git a/Net/net_pickable.gd b/Net/net_pickable.gd new file mode 100644 index 0000000..c0e5ba1 --- /dev/null +++ b/Net/net_pickable.gd @@ -0,0 +1,74 @@ +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 + + +func _ready() -> void: + _pickable = get_parent() as XRToolsPickable + if not _pickable: + push_error("NetPickable must be a child of an XRToolsPickable") + return + _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: + net_held_by = value + 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. +func apply_held_state() -> void: + if not _pickable: + return + if not NetworkManager.is_online() or is_multiplayer_authority(): + # We own this item's simulation (offline, loose+server, or currently + # holding it): leave physics alone, XRToolsPickable manages the rest. + return + # Someone else owns it: stop simulating locally, just follow the sync. + if _pickable.is_picked_up(): + _pickable.drop() + _pickable.freeze = true + _pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC + _pickable.collision_mask = 0 + _pickable.enabled = (net_held_by == 0) + + +## 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): + return + NetworkManager.request_item_authority.rpc_id(1, _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(): + return + NetworkManager.release_item_authority.rpc_id( + 1, _pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity + ) diff --git a/Net/net_pickable.gd.uid b/Net/net_pickable.gd.uid new file mode 100644 index 0000000..8dc4951 --- /dev/null +++ b/Net/net_pickable.gd.uid @@ -0,0 +1 @@ +uid://bwd0pe2udb5xo diff --git a/Net/network_manager.gd b/Net/network_manager.gd index 4111e15..1270a19 100644 --- a/Net/network_manager.gd +++ b/Net/network_manager.gd @@ -24,9 +24,15 @@ signal connection_failed() 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() @@ -80,26 +86,43 @@ 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); works directly when offline. Returns the new node on the -## machine that owns spawning, else null. -func spawn_item(scene_path: String, xform: Transform3D) -> Node: +## 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} + var data := {"scene": scene_path, "xform": xform, "name": node_name, "props": props} if is_online() and _items_spawner: return _items_spawner.spawn(data) - # Offline: instantiate directly under the world. + # 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 _world and inst: - _world.add_child(inst) + 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 owns_world() and is_instance_valid(node): + 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: @@ -110,9 +133,32 @@ func _spawn_item_from_data(data: Variant) -> Node: 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) + + # --- Item grab-authority transfer ----------------------------------------- ## A client (or host) requests authority over an item it just grabbed. Runs on @@ -123,10 +169,17 @@ func request_item_authority(item_path: NodePath) -> void: if not is_server(): return var sender := multiplayer.get_remote_sender_id() + 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. + force_release_item.rpc_id(sender, item_path) + return # 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) - var item := get_node_or_null(item_path) if item: _release_from_snap_zones(item) @@ -147,13 +200,14 @@ func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> _try_snap_into_station(item) -# All station snap zones in the world (nodes in the "station" group). +# 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"): - var zone = station.get_node_or_null("XRToolsSnapZone") - if zone: - zones.append(zone) + for child in station.get_children(): + if child is XRToolsSnapZone: + zones.append(child) return zones @@ -190,6 +244,15 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void: np.apply_held_state() +## Server tells a specific client that its optimistic grab was rejected (the +## item was already legitimately held by someone else). The client drops it. +@rpc("authority", "reliable") +func force_release_item(item_path: NodePath) -> void: + 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() @@ -223,16 +286,37 @@ func submit_work(station_path: NodePath, amount: float) -> void: station.add_work(multiplayer.get_remote_sender_id(), amount) -## Called by main.gd once the world scene is ready, passing its spawners. +## 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. @@ -286,11 +370,13 @@ func _on_connected_to_server() -> void: 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() diff --git a/Net/world_layout.gd b/Net/world_layout.gd new file mode 100644 index 0000000..45d5bd5 --- /dev/null +++ b/Net/world_layout.gd @@ -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": {}} diff --git a/Net/world_layout.gd.uid b/Net/world_layout.gd.uid new file mode 100644 index 0000000..c7c973a --- /dev/null +++ b/Net/world_layout.gd.uid @@ -0,0 +1 @@ +uid://donvkica3drtx diff --git a/Player/net_player.gd b/Player/net_player.gd new file mode 100644 index 0000000..9162742 --- /dev/null +++ b/Player/net_player.gd @@ -0,0 +1,50 @@ +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: + 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 by peer id so they don't start stacked on + # top of each other (host/peer 1 keeps the original spot). + var peer_id := str(name).to_int() + origin.position += Vector3((peer_id - 1) * 1.5, 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 diff --git a/Player/net_player.gd.uid b/Player/net_player.gd.uid new file mode 100644 index 0000000..9b25add --- /dev/null +++ b/Player/net_player.gd.uid @@ -0,0 +1 @@ +uid://bncuxh7j7ix5q diff --git a/Player/net_player.tscn b/Player/net_player.tscn new file mode 100644 index 0000000..1913654 --- /dev/null +++ b/Player/net_player.tscn @@ -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 diff --git a/Prefabs/combinable_item.gd b/Prefabs/combinable_item.gd index 9669aa9..8fc0393 100644 --- a/Prefabs/combinable_item.gd +++ b/Prefabs/combinable_item.gd @@ -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 @@ -78,19 +81,16 @@ func _combine(other_body: Node3D, result: PackedScene) -> void: _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) diff --git a/Scenes/mainMenu.tscn b/Scenes/mainMenu.tscn index 715890a..731dc2b 100644 --- a/Scenes/mainMenu.tscn +++ b/Scenes/mainMenu.tscn @@ -19,7 +19,7 @@ [ext_resource type="PackedScene" path="res://UI/main_menu_panel.tscn" id="17_panel"] [sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"] -size = Vector3(5, 0.1, 5) +size = Vector3(115, 0.1, 15) [sub_resource type="BoxMesh" id="BoxMesh_24d3s"] material = ExtResource("3_m56cs") diff --git a/Scenes/multiPlayer.tscn b/Scenes/multiPlayer.tscn index 075a06c..be95e81 100644 --- a/Scenes/multiPlayer.tscn +++ b/Scenes/multiPlayer.tscn @@ -3,22 +3,10 @@ [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="PackedScene" uid="uid://j7caslh27nor" path="res://Stations/Hob.tscn" id="4_y6cjq"] [ext_resource type="Sky" uid="uid://c1abtpwhdm2d5" path="res://Textures/sky/NightSkyHDRI009_16K.tres" id="5_c3xgf"] -[ext_resource type="PackedScene" uid="uid://c0lknik4noobs" path="res://Items/BurgerBuns.tscn" id="6_urxur"] -[ext_resource type="PackedScene" uid="uid://c6rift56ql3f8" path="res://Stations/BurgerBunsDispenser.tscn" id="7_3a3tq"] -[ext_resource type="PackedScene" uid="uid://bp3v1jl8pctro" path="res://Containers/plate.tscn" id="8_8xmyt"] -[ext_resource type="PackedScene" uid="uid://dpot5qie20vf6" path="res://Items/burger.tscn" id="9_6lx3u"] -[ext_resource type="PackedScene" uid="uid://b3m2ag8g5rj4r" path="res://Items/hamburger.tscn" id="10_p5bqy"] -[ext_resource type="PackedScene" uid="uid://e4i6o5oriecx" path="res://Items/PickupCube.tscn" id="11_jl027"] -[ext_resource type="PackedScene" uid="uid://dvrk268s7gkxh" path="res://Stations/sink.tscn" id="12_j3sw5"] -[ext_resource type="PackedScene" uid="uid://cnjwtnhwh0i8q" path="res://Stations/dirt_station.tscn" id="13_sbg0f"] -[ext_resource type="PackedScene" uid="uid://cfc4ho67u4r5e" path="res://Items/cooked_burger.tscn" id="14_lt5wx"] -[ext_resource type="PackedScene" uid="uid://efaec6ymgabo" path="res://Stations/Counter.tscn" id="15_7pgth"] -[ext_resource type="PackedScene" uid="uid://caf0xanmxbshy" path="res://Stations/table.tscn" id="16_8yyag"] [sub_resource type="BoxShape3D" id="BoxShape3D_vlqg6"] -size = Vector3(5, 0.1, 5) +size = Vector3(15, 0.1, 15) [sub_resource type="BoxMesh" id="BoxMesh_24d3s"] material = ExtResource("3_75ecy") @@ -36,7 +24,7 @@ sdfgi_enabled = true [node name="Main" type="Node3D" unique_id=1312265607] script = ExtResource("1_kdan8") -[node name="XROrigin3D" parent="." unique_id=2055526621 instance=ExtResource("2_g30gi")] +[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] @@ -52,114 +40,16 @@ shape = SubResource("BoxShape3D_vlqg6") transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.00018164515, 0.0006352663, -0.002532661) mesh = SubResource("BoxMesh_24d3s") -[node name="Hob" parent="." unique_id=1687971542 instance=ExtResource("4_y6cjq")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.29336345, 0.8981018, -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_urxur")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.8162017, 1.6081157, -1.4714175) +[node name="WorldContent" type="Node3D" parent="."] -[node name="BurgerBunsDispenser" parent="." unique_id=1720683779 instance=ExtResource("7_3a3tq")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.832131, 0.40028095, -1.4813508) +[node name="Players" type="Node3D" parent="."] -[node name="Plate" parent="." unique_id=190487773 instance=ExtResource("8_8xmyt")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5717233, 1.5454081, 1.5373346) +[node name="ItemsSpawner" type="MultiplayerSpawner" parent="."] +spawn_path = NodePath("../WorldContent") -[node name="burger" parent="." unique_id=1417604760 instance=ExtResource("9_6lx3u")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6888188, 1.4195822, -1.7110313) - -[node name="burger2" parent="." unique_id=1584460510 instance=ExtResource("9_6lx3u")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.6931299, 1.5300478, -1.71225) - -[node name="burger3" parent="." unique_id=1884095916 instance=ExtResource("9_6lx3u")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69027674, 1.4969791, -1.7210286) - -[node name="burger4" parent="." unique_id=1844818864 instance=ExtResource("9_6lx3u")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0.69134104, 1.4543622, -1.7210286) - -[node name="Hamburger" parent="." unique_id=761091445 instance=ExtResource("10_p5bqy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9352558, 1.623975, 1.2447833) - -[node name="PickableObject" parent="." unique_id=1675596942 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.6225724, 1.4792972, -1.0473135) - -[node name="PickableObject2" parent="." unique_id=713087634 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.6359743, 1.4639391, -1.1673055) - -[node name="PickableObject3" parent="." unique_id=879935619 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.5142721, 1.4792972, -1.0473135) - -[node name="PickableObject4" parent="." unique_id=198541902 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.5276739, 1.4639391, -1.1673055) - -[node name="Hamburger2" parent="." unique_id=1349934579 instance=ExtResource("10_p5bqy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.9857153, 1.5329368, 0.9472374) - -[node name="PickableObject5" parent="." unique_id=1021333509 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, 0.73287535, 1.4792972, -1.0473135) - -[node name="PickableObject6" parent="." unique_id=1268843669 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, 0.7462772, 1.4639391, -1.1673055) - -[node name="Sink" parent="." unique_id=2055277359 instance=ExtResource("12_j3sw5")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.3140475, 0.9061539, -1.4941733) - -[node name="DirtStation" parent="." unique_id=160842153 instance=ExtResource("13_sbg0f")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 2.0864775, 0.8981018, -1.244947) - -[node name="Plate2" parent="." unique_id=356790445 instance=ExtResource("8_8xmyt")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5730225, 1.499024, 0.59790254) - -[node name="CookedBurger" parent="." unique_id=1127807542 instance=ExtResource("14_lt5wx")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4131018, -1.0928738) - -[node name="CookedBurger2" parent="." unique_id=160088590 instance=ExtResource("14_lt5wx")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.4496142, -1.0928738) - -[node name="CookedBurger3" parent="." unique_id=992183367 instance=ExtResource("14_lt5wx")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3344773, 1.4398065, -0.7096845) - -[node name="CookedBurger4" parent="." unique_id=1837607086 instance=ExtResource("14_lt5wx")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.30671906, 1.525444, -1.0928738) - -[node name="BurgerBuns2" parent="." unique_id=1210358958 instance=ExtResource("6_urxur")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.3596323, 1.4110342, -0.22482127) - -[node name="PickableObject7" parent="." unique_id=641653545 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.3556751, 1.4792972, 0.28881657) - -[node name="PickableObject8" parent="." unique_id=1733819363 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.3422732, 1.4639391, 0.16882455) - -[node name="PickableObject9" parent="." unique_id=12419746 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, 0, -2.6077032e-08, 1.4901161e-08, 1, -1.4639754, 1.4792972, 0.28881657) - -[node name="PickableObject10" parent="." unique_id=313160217 instance=ExtResource("11_jl027")] -transform = Transform3D(1, 0, -2.2351742e-08, -2.9802322e-08, 1.0000001, -5.293956e-23, -2.6077032e-08, 1.4901161e-08, 1, -1.4505737, 1.4639391, 0.16882455) - -[node name="Counter" parent="." unique_id=1487893288 instance=ExtResource("15_7pgth")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.7124918, 0.90304357, -1.4886917) - -[node name="Counter2" parent="." unique_id=368890752 instance=ExtResource("15_7pgth")] -transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, -0.4458799) - -[node name="Counter3" parent="." unique_id=1498817646 instance=ExtResource("15_7pgth")] -transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, 0.55367994) - -[node name="Counter4" parent="." unique_id=906748771 instance=ExtResource("15_7pgth")] -transform = Transform3D(-4.371139e-08, 0, 1, 0, 1, 0, -1, 0, -4.371139e-08, -1.7648025, 0.90304357, 1.5539298) - -[node name="Table" parent="." unique_id=1863572470 instance=ExtResource("16_8yyag")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.633146, 0.9030438, 1.1343781) -initial_thinking_time = 8.0 -initial_primary_time = 40.0 -initial_friend_time = 3.0 -initial_eating_time = 3.0 - -[node name="Hamburger3" parent="." unique_id=369573848 instance=ExtResource("10_p5bqy")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.7201865, 1.5329367, 0.14773655) - -[node name="Plate3" parent="." unique_id=2004250522 instance=ExtResource("8_8xmyt")] -transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5818124, 1.499024, -0.4634577) +[node name="PlayersSpawner" type="MultiplayerSpawner" parent="."] +_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn") +spawn_path = NodePath("../Players") diff --git a/Scenes/multiplayer_world.gd b/Scenes/multiplayer_world.gd index 0c056b4..2917a22 100644 --- a/Scenes/multiplayer_world.gd +++ b/Scenes/multiplayer_world.gd @@ -1,13 +1,21 @@ extends Node3D ## World script for the multiplayer scene. Consumes the host/join request set -## by the main menu, and returns to the menu if the session ends. +## 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. ## -## Connection-level only for now: no PlayersSpawner/ItemsSpawner here since the -## networked Player/NetPickable scenes aren't ported yet (out of scope for this -## change) — peers connect, but avatars/items don't sync. +## 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") var xr_interface: XRInterface +var _populated := false func _ready() -> void: @@ -16,11 +24,60 @@ func _ready() -> void: 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) + + # Offline (single player / dev run): owns_world() is already true, so + # populate immediately. Online host case is handled by session_started. + _populate_world_if_owner() + NetworkManager.world_ready() +func _exit_tree() -> void: + NetworkManager.unregister_world() + + +func _on_session_started(_is_server: bool) -> void: + _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: + return + _populated = true + GameManager.meals_in_play = ["hamburger"] + for d in WorldLayout.get_stations(): + NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"]) + for d in WorldLayout.get_items(): + NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"]) + + +## 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) + + +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() + + func _on_session_ended() -> void: get_tree().change_scene_to_file("res://Scenes/mainMenu.tscn") diff --git a/Stations/BurgerBunsDispenser.tscn b/Stations/BurgerBunsDispenser.tscn index d753374..9f3fc68 100644 --- a/Stations/BurgerBunsDispenser.tscn +++ b/Stations/BurgerBunsDispenser.tscn @@ -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] diff --git a/Stations/Hob.tscn b/Stations/Hob.tscn index cec9ac3..06c1c51 100644 --- a/Stations/Hob.tscn +++ b/Stations/Hob.tscn @@ -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") diff --git a/Stations/dirt_station.gd b/Stations/dirt_station.gd index 1fb945e..1606464 100644 --- a/Stations/dirt_station.gd +++ b/Stations/dirt_station.gd @@ -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) diff --git a/Stations/hob.gd b/Stations/hob.gd index f925bed..8b08c2e 100644 --- a/Stations/hob.gd +++ b/Stations/hob.gd @@ -38,23 +38,24 @@ 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 if not old_pickable: 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() + snap_zone.drop_object() + NetworkManager.despawn_item(old_pickable) snap_zone.pick_up_object(new_scene_instance) diff --git a/Stations/item_dispenser.gd b/Stations/item_dispenser.gd index 22794eb..39a09e2 100644 --- a/Stations/item_dispenser.gd +++ b/Stations/item_dispenser.gd @@ -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) diff --git a/Stations/sink.tscn b/Stations/sink.tscn index cec152b..072d446 100644 --- a/Stations/sink.tscn +++ b/Stations/sink.tscn @@ -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") diff --git a/Stations/table.gd b/Stations/table.gd index a57ab7a..7d39d1d 100644 --- a/Stations/table.gd +++ b/Stations/table.gd @@ -25,9 +25,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: @@ -59,8 +66,8 @@ func _on_object_picked_up(_item) -> void: func _on_object_dropped() -> void: print("Table: object dropped ") - - + + func _get_plate_from_item(_item: Node) -> PlateController: return _item.get_children().filter(func(c): return c is PlateController).front() as PlateController @@ -70,9 +77,9 @@ func _absorb_item_if_correct(_item: Node) -> void: var plate = _get_plate_from_item(_item) if not plate: print("Table: held object is not a Plate") - return + return print("Table: held a Plate!") - + # Absorm items from the plate we want for food_item in plate.container.contained_items: print("Table: held a plate with FoodItem: ", food_item.id) @@ -81,12 +88,11 @@ 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(): _setState(TableState.EATING) - + func place_order() -> void: print("Table: place_order()") @@ -94,56 +100,78 @@ 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)] - - +## 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]) - 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 + 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 - 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.THINKING: + place_order() + _setState(TableState.WAITING_PRIMARY) + + TableState.WAITING_PRIMARY: label_3d.text = lbl_gameover - + TableState.WAITING_FRIEND: label_3d.text = lbl_gameover @@ -158,4 +186,3 @@ func _process(delta: float) -> void: plate.container.clear() plate.is_dirty = true _setState(TableState.EMPTY) - diff --git a/Stations/table.tscn b/Stations/table.tscn index 7135048..f410db3 100644 --- a/Stations/table.tscn +++ b/Stations/table.tscn @@ -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,7 @@ 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="."] +root_path = NodePath("..") +replication_config = SubResource("SceneReplicationConfig_np_table") diff --git a/UI/main_menu_panel.gd b/UI/main_menu_panel.gd index 18f6fae..8f19639 100644 --- a/UI/main_menu_panel.gd +++ b/UI/main_menu_panel.gd @@ -7,6 +7,7 @@ extends Control 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" @@ -33,8 +34,15 @@ func _ready() -> void: _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 on the command line skip straight to the multiplayer @@ -64,6 +72,7 @@ func _show_root_view() -> void: 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: @@ -94,10 +103,26 @@ 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: diff --git a/UI/main_menu_panel.tscn b/UI/main_menu_panel.tscn index 6b43b39..07d0e08 100644 --- a/UI/main_menu_panel.tscn +++ b/UI/main_menu_panel.tscn @@ -33,6 +33,15 @@ 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 @@ -82,14 +91,6 @@ theme_override_font_sizes/font_size = 34 text = "127.0.0.1" horizontal_alignment = 1 -[node name="Status" type="Label" parent="Margin/JoinView"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_colors/font_color = Color(0.8, 0.8, 0.5, 1) -theme_override_font_sizes/font_size = 16 -text = "Enter host IP, then Join" -horizontal_alignment = 1 - [node name="Keypad" type="GridContainer" parent="Margin/JoinView"] unique_name_in_owner = true layout_mode = 2 diff --git a/main.gd b/main.gd index 55335ae..c40a985 100644 --- a/main.gd +++ b/main.gd @@ -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():