155 lines
6.1 KiB
GDScript
155 lines
6.1 KiB
GDScript
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 _populated or not populate_from_layout:
|
|
return
|
|
# WorldLayout reads the live scene tree to find what to replicate, so it
|
|
# needs to be an instance sitting in that tree — its methods can't be called
|
|
# on the class itself.
|
|
var layout := WorldLayout.new()
|
|
add_child(layout)
|
|
var authored := layout.get_station_nodes()
|
|
authored.append_array(layout.get_item_nodes())
|
|
|
|
# The stations and items authored into the scene file are a *template*, not
|
|
# the live world. Only the server turns them into real objects, spawned
|
|
# through NetworkManager so they replicate. Every peer therefore drops its
|
|
# own 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
|
|
|
|
_init_recipes()
|
|
_populated = true
|
|
var stations := layout.get_stations()
|
|
var items := layout.get_items()
|
|
layout.queue_free()
|
|
_remove_authored(authored)
|
|
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")
|
|
|
|
|
|
func _init_recipes():
|
|
GameManager.meals_in_play = ["hamburger"]
|
|
GameManager.sides_in_play = ["cube"]
|
|
|
|
|
|
# Free the authored template nodes. Done immediately rather than with
|
|
# queue_free() 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) 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")
|