extends Node3D ## World script for the multiplayer scene. Starts the host/join the menu asked ## for, turns the authored kitchen into replicated objects, gives every connected ## peer an avatar, and returns to the menu when the session ends. ## ## The world's contents are NOT left baked into the scene file. Whatever the ## scene authors is treated as a template: the server spawns real copies of it ## through NetWorld and every peer drops its own authored originals. A client ## therefore shows the server's world rather than assuming its local copy of the ## scene matches — which is also exactly the path a player joining mid-session ## takes. const PLAYER_SCENE := "res://Player/net_player.tscn" ## Whether to build the full kitchen on the machine that owns the world. The real ## game scene wants this; focused debug scenes bake their own handful of objects ## and turn it off, so the thing under test is not sharing the world with a ## second copy of the whole kitchen. @export var populate_from_layout: bool = true var xr_interface: XRInterface var _net_world: NetWorld 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 # Both roots go through the same generic replication hook, so a player avatar # is just another replicated object — nothing about it is special-cased. _net_world = NetWorld.new() _net_world.name = "NetWorld" add_child(_net_world) _net_world.setup($ItemsSpawner, $WorldContent) $PlayersSpawner.add_spawnable_scene(PLAYER_SCENE) $Players.child_entered_tree.connect(_on_player_entered) NetworkManager.register_world(self, _net_world) NetworkManager.player_joined.connect(_on_player_joined) NetworkManager.player_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 command-line # equivalent). Populating before this point would be wrong for every case, not # just offline: is_online() is still false until host()/join() runs, so # owns_world() reads true for a joining client too and it would build its own # copy instead of receiving the server's. host() emits session_started # synchronously, which populates via _on_session_started; the call after # world_ready() only covers the case where neither ran — this scene opened # directly, offline. NetworkManager.world_ready() _populate_world_if_owner() func _on_player_entered(node: Node) -> void: if node is MultiplayerSynchronizer: return NetReplication.attach(node) func _exit_tree() -> void: NetworkManager.unregister_world() func _on_session_started(_is_server: bool) -> void: _populate_world_if_owner() ## Turns the authored template into replicated objects, exactly once, on the ## machine that owns world logic. Safe to call from several entry points. func _populate_world_if_owner() -> void: if _populated or not populate_from_layout: return # WorldLayout reads the live scene tree to find what to replicate, so it has # to be an instance sitting in that tree. var layout := WorldLayout.new() add_child(layout) var authored := layout.get_authored_nodes() # Every peer drops its authored copies. The client would otherwise show its # local originals on top of the server's replicated ones, and the two sets # would drift apart because only the server's are synced. if not NetworkManager.owns_world(): NetworkManager.log_line("Clearing %d authored nodes; the server's copies replace them" % authored.size()) _remove_authored(authored) layout.queue_free() return _populated = true GameManager.meals_in_play = ["hamburger"] var objects := layout.describe(authored) layout.queue_free() _remove_authored(authored) NetworkManager.log_line("Populating world: %d objects" % objects.size()) for d in objects: NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"]) NetworkManager.log_line("World populated") # Freed immediately rather than queue_free()d, so the names are released before # the replicated copies are spawned under the same ones. func _remove_authored(nodes: Array[Node]) -> void: for node in nodes: if is_instance_valid(node): node.get_parent().remove_child(node) node.free() ## Only the server (or the single offline machine) creates avatars; ## MultiplayerSpawner replicates the result to everyone else, late joiners ## included. func _on_player_joined(peer_id: int) -> void: if not NetworkManager.owns_world() or $Players.has_node(str(peer_id)): return var p: Node = load(PLAYER_SCENE).instantiate() # The name is the peer id, which is how net_player.gd knows whose avatar it # is on every peer — MultiplayerSpawner replicates the name. p.name = str(peer_id) $Players.add_child(p) 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")