|
|
|
@@ -1,26 +1,26 @@
|
|
|
|
|
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.
|
|
|
|
|
## 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 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.
|
|
|
|
|
## 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 := preload("res://Player/net_player.tscn")
|
|
|
|
|
const PLAYER_SCENE := "res://Player/net_player.tscn"
|
|
|
|
|
|
|
|
|
|
## Whether to spawn the full WorldLayout on the machine that owns the world.
|
|
|
|
|
## 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.
|
|
|
|
|
## 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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -30,34 +30,38 @@ func _ready() -> void:
|
|
|
|
|
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
|
|
|
|
|
get_viewport().use_xr = true
|
|
|
|
|
|
|
|
|
|
NetworkManager.register_world(self, $PlayersSpawner, $ItemsSpawner)
|
|
|
|
|
# Both roots go through the same generic replication hook, so a player avatar
|
|
|
|
|
# is just another replicated object — nothing about it is special-cased.
|
|
|
|
|
_net_world = NetWorld.new()
|
|
|
|
|
_net_world.name = "NetWorld"
|
|
|
|
|
add_child(_net_world)
|
|
|
|
|
_net_world.setup($ItemsSpawner, $WorldContent)
|
|
|
|
|
$PlayersSpawner.add_spawnable_scene(PLAYER_SCENE)
|
|
|
|
|
$Players.child_entered_tree.connect(_on_player_entered)
|
|
|
|
|
|
|
|
|
|
NetworkManager.register_world(self, _net_world)
|
|
|
|
|
NetworkManager.player_joined.connect(_on_player_joined)
|
|
|
|
|
NetworkManager.player_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
|
|
|
|
|
# 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() 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.
|
|
|
|
|
# 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()
|
|
|
|
|
|
|
|
|
|
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 _on_player_entered(node: Node) -> void:
|
|
|
|
|
if node is MultiplayerSynchronizer:
|
|
|
|
|
return
|
|
|
|
|
NetReplication.attach(node)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _exit_tree() -> void:
|
|
|
|
@@ -65,29 +69,23 @@ func _exit_tree() -> void:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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.
|
|
|
|
|
## 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
|
|
|
|
|
# needs to be an instance sitting in that tree — its methods can't be called
|
|
|
|
|
# on the class itself.
|
|
|
|
|
# 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_station_nodes()
|
|
|
|
|
authored.append_array(layout.get_item_nodes())
|
|
|
|
|
var authored := layout.get_authored_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.
|
|
|
|
|
# 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)
|
|
|
|
@@ -96,21 +94,17 @@ func _populate_world_if_owner() -> void:
|
|
|
|
|
|
|
|
|
|
_populated = true
|
|
|
|
|
GameManager.meals_in_play = ["hamburger"]
|
|
|
|
|
var stations := layout.get_stations()
|
|
|
|
|
var items := layout.get_items()
|
|
|
|
|
var objects := layout.describe(authored)
|
|
|
|
|
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.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")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# 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.
|
|
|
|
|
# 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):
|
|
|
|
@@ -118,15 +112,17 @@ func _remove_authored(nodes: Array[Node]) -> void:
|
|
|
|
|
node.free()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
## Only the server (or the single offline machine) materialises player
|
|
|
|
|
## avatars; MultiplayerSpawner replicates the result to everyone else,
|
|
|
|
|
## including late joiners.
|
|
|
|
|
## 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 := PLAYER_SCENE.instantiate()
|
|
|
|
|
var p: Node = load(PLAYER_SCENE).instantiate()
|
|
|
|
|
# The name is the peer id, which is how net_player.gd knows whose avatar it
|
|
|
|
|
# is on every peer — MultiplayerSpawner replicates the name.
|
|
|
|
|
p.name = str(peer_id)
|
|
|
|
|
$Players.add_child(p, true)
|
|
|
|
|
$Players.add_child(p)
|
|
|
|
|
NetworkManager.log_line("Spawned avatar for peer %d" % peer_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|