Remove AI multiplayer

This commit is contained in:
JonShard
2026-07-29 10:57:35 +02:00
parent ea492f914b
commit 0f1f0c6a93
102 changed files with 1146 additions and 960 deletions
+149
View File
@@ -0,0 +1,149 @@
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
_populated = true
GameManager.meals_in_play = ["hamburger"]
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")
# 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")
+1
View File
@@ -0,0 +1 @@
uid://d1cefhe3yyyds
+208 -208
View File
@@ -1,209 +1,209 @@
extends MultiplayerSynchronizer
## Networked sync component for a pickable item. Added as a child literally
## named "NetPickable" (network_manager.gd's authority RPCs already expect
## this) of every net-synced pickable, replicating its transform and held
## state. Only the current multiplayer authority (the server while loose, or
## whichever peer is holding it) actually simulates physics for the item;
## every other peer freezes their local copy and just follows the synced
## transform.
## 0 = loose/server-simulated; otherwise the peer id currently holding it.
## Replicated at spawn and on change so a late joiner sees the current holder.
var net_held_by: int = 0: set = _set_net_held_by
var _pickable: XRToolsPickable
# This item's own baked freeze_mode (e.g. plate.tscn bakes KINEMATIC, not the
# RigidBody3D default of STATIC) — captured once so it can be restored when
# this peer regains ownership, instead of getting stuck on whatever
# apply_held_state() last forced it to while non-authority.
var _original_freeze_mode: int
# Same idea for the pickable's authored `enabled` flag, which the non-authority
# branch of apply_held_state() clears while someone else is holding the item.
var _original_enabled: bool
# Whether the "our own hand still holds this" guard has already been logged for
# the current grab. apply_held_state() runs every network tick, so without this
# the guard message repeats for as long as you hold the item.
var _grab_race_logged := false
func _ready() -> void:
_pickable = get_parent() as XRToolsPickable
if not _pickable:
push_error("NetPickable must be a child of an XRToolsPickable")
return
_original_freeze_mode = _pickable.freeze_mode
_original_enabled = _pickable.enabled
_pickable.picked_up.connect(_on_picked_up)
_pickable.dropped.connect(_on_dropped)
# Deferred: the pickable root captures its own original_collision_mask/
# original_collision_layer via @onready, which runs AFTER this child's
# _ready() but BEFORE the root's _ready() body. Calling apply_held_state
# synchronously here would freeze/mask the item before that capture runs,
# permanently corrupting the "restore on drop" values.
apply_held_state.call_deferred()
func _set_net_held_by(value: int) -> void:
var old := net_held_by
net_held_by = value
if old != value and NetworkManager.is_online():
print("%s net_held_by: %d -> %d (local state: %s, authority=%d)" % [
_pickable.name, old, value, _holder_desc(), get_multiplayer_authority()
])
apply_held_state()
## Puts the item in the right physics state for whether this peer currently
## owns it. Called locally after net_held_by changes, and directly by
## NetworkManager._set_item_authority right after an authority handoff.
##
## IMPORTANT: this runs on every network tick, not just on a real change.
## net_held_by is replicated in ALWAYS mode, so the synchronizer assigns it every
## tick on non-authority peers — unchanged value included — and that assignment
## lands in _set_net_held_by(), which calls this. So every branch here has to be
## idempotent and silent when there is nothing to do: otherwise each item logs a
## line and rewrites four physics properties every tick on every peer that
## doesn't own it.
func apply_held_state() -> void:
if not _pickable:
return
if not NetworkManager.is_online() or is_multiplayer_authority():
_grab_race_logged = false
# We own this item's simulation (offline, loose+server, or currently
# holding it). If it's not actively in our own hand right now, make
# sure it isn't still left frozen/collision-less from a previous
# non-authority period (e.g. right after regaining authority when a
# client released it) — while actually held, XRToolsPickable's own
# pick_up()/let_go() already manage these fields, so leave those be.
if not _pickable.is_picked_up():
var changed := _pickable.freeze_mode != _original_freeze_mode \
or _pickable.collision_mask != _pickable.original_collision_mask
if changed:
if NetworkManager.is_online():
print(
"%s: reclaiming ownership, restoring freeze_mode %d->%d collision_mask %d->%d" % [
_pickable.name, _pickable.freeze_mode, _original_freeze_mode,
_pickable.collision_mask, _pickable.original_collision_mask
]
)
_pickable.freeze_mode = _original_freeze_mode
_pickable.collision_mask = _pickable.original_collision_mask
# Unlike freeze/collision (which XRToolsPickable manages itself while
# held), `enabled` is only ever written by the non-authority branch
# below, so it must be restored here or it stays false forever: once a
# client grabbed this item, every other peer set enabled=false, and
# regaining authority left it that way. On the server that silently
# broke everything downstream — hands couldn't pick the item up again,
# and a station snap zone would "snap" it (emitting has_picked_up, so
# e.g. a plate still got marked dirty) while pick_up() bailed out on
# the disabled item, leaving the zone holding an item with no grab
# driver that then fell out of the station.
if _pickable.enabled != _original_enabled:
if NetworkManager.is_online():
print("%s: reclaiming ownership, restoring enabled %s->%s" % [
_pickable.name, _pickable.enabled, _original_enabled
])
_pickable.enabled = _original_enabled
return
# A net_held_by/position sync update can race ahead of the
# authority-handoff RPC that's about to confirm a grab we just made
# optimistically (they travel on different channels with no ordering
# guarantee). Don't let a stale sync value yank an item out of our own
# hand mid-grab — only an explicit force_release_item rejection, or
# actually losing authority for real, should end a grab we initiated.
if _pickable.is_picked_up() and _pickable.get_picked_up_by() is XRToolsFunctionPickup:
# Log once per grab, not once per tick.
if NetworkManager.is_online() and not _grab_race_logged:
_grab_race_logged = true
print(
"%s: ignoring non-authority sync (net_held_by=%d) — still actively held by our own hand (grab-race guard)" % [
_pickable.name, net_held_by
]
)
return
_grab_race_logged = false
# Someone else owns it: stop simulating locally, just follow the sync.
if _pickable.is_picked_up():
if NetworkManager.is_online():
print(
"%s: was held by %s on this peer, but authority now says peer %d owns it — force-dropping" % [
_pickable.name, _holder_desc(), net_held_by
]
)
_pickable.drop()
# Bail out when we're already in the follow-the-sync state. Without this the
# writes below (and the line logged with them) repeated every tick for every
# item on every non-authority peer — 90% of the log, plus four redundant
# physics-property writes per item per tick. The comparison also means we
# still re-apply if something else perturbs the state (e.g. let_go()
# restoring the collision mask after a force-drop).
var want_enabled := (net_held_by == 0)
if _pickable.freeze \
and _pickable.freeze_mode == RigidBody3D.FREEZE_MODE_KINEMATIC \
and _pickable.collision_mask == 0 \
and _pickable.enabled == want_enabled:
return
if NetworkManager.is_online():
print("%s: freezing (non-authority, owner=peer %d)" % [_pickable.name, net_held_by])
_pickable.freeze = true
_pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
_pickable.collision_mask = 0
_pickable.enabled = want_enabled
## Local hand grab (not a station snap zone, which is server-only): request
## authority immediately so the throw/drop can be reconciled, but let the grab
## happen instantly here rather than waiting on the round trip.
func _on_picked_up(_p) -> void:
var by := _pickable.get_picked_up_by()
if not (by is XRToolsFunctionPickup):
# e.g. a station snap zone grabbed it (server-side auto-snap, or the
# addon's own "grab out of a snap zone" shortcut mid-cascade) — not a
# player-initiated hand grab, so no authority request from here.
if NetworkManager.is_online():
print("%s picked up by %s (not a hand) — no authority request" % [_pickable.name, _holder_desc()])
return
if NetworkManager.is_online():
print("%s grabbed by hand (authority was peer %d), requesting authority" % [_pickable.name, net_held_by])
NetworkManager.request_item_authority_from(_pickable.get_path())
func _on_dropped(_p) -> void:
# Only forward if we're actually still the authority — a drop caused by
# apply_held_state() losing authority (see above) must not re-report.
if not is_multiplayer_authority():
if NetworkManager.is_online():
print("%s dropped locally, but we aren't its authority (peer %d is) — not reporting" % [_pickable.name, net_held_by])
return
if NetworkManager.is_online():
print("%s dropped, reporting release to server (lin=%s ang=%s)" % [
_pickable.name, _pickable.linear_velocity, _pickable.angular_velocity
])
# Send our own final transform too: we were the authority until now, and the
# server's copy may not have received our last position sync yet.
NetworkManager.release_item_authority_from(
_pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity,
_pickable.global_transform
)
## Human-readable description of what's currently holding this item on THIS
## peer, for diagnosing desyncs between a hand's own "what am I holding"
## bookkeeping and the item's actual grab state (see the snap-zone-grab race
## in NetworkManager._try_snap_into_station for a real example).
func _holder_desc() -> String:
if not _pickable or not _pickable.is_picked_up():
return "loose"
var by := _pickable.get_picked_up_by()
if not by:
return "held(no grabber?)"
if by is XRToolsFunctionPickup:
return "hand(%s)" % by.get_path()
if by is XRToolsSnapZone:
var station := by.get_parent()
return "zone(%s)" % (station.name if station else str(by.get_path()))
return "other(%s: %s)" % [by.get_class(), by.get_path()]
#
### Networked sync component for a pickable item. Added as a child literally
### named "NetPickable" (network_manager.gd's authority RPCs already expect
### this) of every net-synced pickable, replicating its transform and held
### state. Only the current multiplayer authority (the server while loose, or
### whichever peer is holding it) actually simulates physics for the item;
### every other peer freezes their local copy and just follows the synced
### transform.
#
### 0 = loose/server-simulated; otherwise the peer id currently holding it.
### Replicated at spawn and on change so a late joiner sees the current holder.
#var net_held_by: int = 0: set = _set_net_held_by
#
#var _pickable: XRToolsPickable
#
## This item's own baked freeze_mode (e.g. plate.tscn bakes KINEMATIC, not the
## RigidBody3D default of STATIC) — captured once so it can be restored when
## this peer regains ownership, instead of getting stuck on whatever
## apply_held_state() last forced it to while non-authority.
#var _original_freeze_mode: int
#
## Same idea for the pickable's authored `enabled` flag, which the non-authority
## branch of apply_held_state() clears while someone else is holding the item.
#var _original_enabled: bool
#
## Whether the "our own hand still holds this" guard has already been logged for
## the current grab. apply_held_state() runs every network tick, so without this
## the guard message repeats for as long as you hold the item.
#var _grab_race_logged := false
#
#
#func _ready() -> void:
#_pickable = get_parent() as XRToolsPickable
#if not _pickable:
#push_error("NetPickable must be a child of an XRToolsPickable")
#return
#_original_freeze_mode = _pickable.freeze_mode
#_original_enabled = _pickable.enabled
#_pickable.picked_up.connect(_on_picked_up)
#_pickable.dropped.connect(_on_dropped)
## Deferred: the pickable root captures its own original_collision_mask/
## original_collision_layer via @onready, which runs AFTER this child's
## _ready() but BEFORE the root's _ready() body. Calling apply_held_state
## synchronously here would freeze/mask the item before that capture runs,
## permanently corrupting the "restore on drop" values.
#apply_held_state.call_deferred()
#
#
#func _set_net_held_by(value: int) -> void:
#var old := net_held_by
#net_held_by = value
#if old != value and NetworkManager.is_online():
#print("%s net_held_by: %d -> %d (local state: %s, authority=%d)" % [
#_pickable.name, old, value, _holder_desc(), get_multiplayer_authority()
#])
#apply_held_state()
#
#
### Puts the item in the right physics state for whether this peer currently
### owns it. Called locally after net_held_by changes, and directly by
### NetworkManager._set_item_authority right after an authority handoff.
###
### IMPORTANT: this runs on every network tick, not just on a real change.
### net_held_by is replicated in ALWAYS mode, so the synchronizer assigns it every
### tick on non-authority peers — unchanged value included — and that assignment
### lands in _set_net_held_by(), which calls this. So every branch here has to be
### idempotent and silent when there is nothing to do: otherwise each item logs a
### line and rewrites four physics properties every tick on every peer that
### doesn't own it.
#func apply_held_state() -> void:
#if not _pickable:
#return
#if not NetworkManager.is_online() or is_multiplayer_authority():
#_grab_race_logged = false
## We own this item's simulation (offline, loose+server, or currently
## holding it). If it's not actively in our own hand right now, make
## sure it isn't still left frozen/collision-less from a previous
## non-authority period (e.g. right after regaining authority when a
## client released it) — while actually held, XRToolsPickable's own
## pick_up()/let_go() already manage these fields, so leave those be.
#if not _pickable.is_picked_up():
#var changed := _pickable.freeze_mode != _original_freeze_mode \
#or _pickable.collision_mask != _pickable.original_collision_mask
#if changed:
#if NetworkManager.is_online():
#print(
#"%s: reclaiming ownership, restoring freeze_mode %d->%d collision_mask %d->%d" % [
#_pickable.name, _pickable.freeze_mode, _original_freeze_mode,
#_pickable.collision_mask, _pickable.original_collision_mask
#]
#)
#_pickable.freeze_mode = _original_freeze_mode
#_pickable.collision_mask = _pickable.original_collision_mask
## Unlike freeze/collision (which XRToolsPickable manages itself while
## held), `enabled` is only ever written by the non-authority branch
## below, so it must be restored here or it stays false forever: once a
## client grabbed this item, every other peer set enabled=false, and
## regaining authority left it that way. On the server that silently
## broke everything downstream — hands couldn't pick the item up again,
## and a station snap zone would "snap" it (emitting has_picked_up, so
## e.g. a plate still got marked dirty) while pick_up() bailed out on
## the disabled item, leaving the zone holding an item with no grab
## driver that then fell out of the station.
#if _pickable.enabled != _original_enabled:
#if NetworkManager.is_online():
#print("%s: reclaiming ownership, restoring enabled %s->%s" % [
#_pickable.name, _pickable.enabled, _original_enabled
#])
#_pickable.enabled = _original_enabled
#return
## A net_held_by/position sync update can race ahead of the
## authority-handoff RPC that's about to confirm a grab we just made
## optimistically (they travel on different channels with no ordering
## guarantee). Don't let a stale sync value yank an item out of our own
## hand mid-grab — only an explicit force_release_item rejection, or
## actually losing authority for real, should end a grab we initiated.
#if _pickable.is_picked_up() and _pickable.get_picked_up_by() is XRToolsFunctionPickup:
## Log once per grab, not once per tick.
#if NetworkManager.is_online() and not _grab_race_logged:
#_grab_race_logged = true
#print(
#"%s: ignoring non-authority sync (net_held_by=%d) — still actively held by our own hand (grab-race guard)" % [
#_pickable.name, net_held_by
#]
#)
#return
#_grab_race_logged = false
## Someone else owns it: stop simulating locally, just follow the sync.
#if _pickable.is_picked_up():
#if NetworkManager.is_online():
#print(
#"%s: was held by %s on this peer, but authority now says peer %d owns it — force-dropping" % [
#_pickable.name, _holder_desc(), net_held_by
#]
#)
#_pickable.drop()
## Bail out when we're already in the follow-the-sync state. Without this the
## writes below (and the line logged with them) repeated every tick for every
## item on every non-authority peer — 90% of the log, plus four redundant
## physics-property writes per item per tick. The comparison also means we
## still re-apply if something else perturbs the state (e.g. let_go()
## restoring the collision mask after a force-drop).
#var want_enabled := (net_held_by == 0)
#if _pickable.freeze \
#and _pickable.freeze_mode == RigidBody3D.FREEZE_MODE_KINEMATIC \
#and _pickable.collision_mask == 0 \
#and _pickable.enabled == want_enabled:
#return
#if NetworkManager.is_online():
#print("%s: freezing (non-authority, owner=peer %d)" % [_pickable.name, net_held_by])
#_pickable.freeze = true
#_pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
#_pickable.collision_mask = 0
#_pickable.enabled = want_enabled
#
#
### Local hand grab (not a station snap zone, which is server-only): request
### authority immediately so the throw/drop can be reconciled, but let the grab
### happen instantly here rather than waiting on the round trip.
#func _on_picked_up(_p) -> void:
#var by := _pickable.get_picked_up_by()
#if not (by is XRToolsFunctionPickup):
## e.g. a station snap zone grabbed it (server-side auto-snap, or the
## addon's own "grab out of a snap zone" shortcut mid-cascade) — not a
## player-initiated hand grab, so no authority request from here.
#if NetworkManager.is_online():
#print("%s picked up by %s (not a hand) — no authority request" % [_pickable.name, _holder_desc()])
#return
#if NetworkManager.is_online():
#print("%s grabbed by hand (authority was peer %d), requesting authority" % [_pickable.name, net_held_by])
#NetworkManager.request_item_authority_from(_pickable.get_path())
#
#
#func _on_dropped(_p) -> void:
## Only forward if we're actually still the authority — a drop caused by
## apply_held_state() losing authority (see above) must not re-report.
#if not is_multiplayer_authority():
#if NetworkManager.is_online():
#print("%s dropped locally, but we aren't its authority (peer %d is) — not reporting" % [_pickable.name, net_held_by])
#return
#if NetworkManager.is_online():
#print("%s dropped, reporting release to server (lin=%s ang=%s)" % [
#_pickable.name, _pickable.linear_velocity, _pickable.angular_velocity
#])
## Send our own final transform too: we were the authority until now, and the
## server's copy may not have received our last position sync yet.
#NetworkManager.release_item_authority_from(
#_pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity,
#_pickable.global_transform
#)
#
#
### Human-readable description of what's currently holding this item on THIS
### peer, for diagnosing desyncs between a hand's own "what am I holding"
### bookkeeping and the item's actual grab state (see the snap-zone-grab race
### in NetworkManager._try_snap_into_station for a real example).
#func _holder_desc() -> String:
#if not _pickable or not _pickable.is_picked_up():
#return "loose"
#var by := _pickable.get_picked_up_by()
#if not by:
#return "held(no grabber?)"
#if by is XRToolsFunctionPickup:
#return "hand(%s)" % by.get_path()
#if by is XRToolsSnapZone:
#var station := by.get_parent()
#return "zone(%s)" % (station.name if station else str(by.get_path()))
#return "other(%s: %s)" % [by.get_class(), by.get_path()]
-155
View File
@@ -1,155 +0,0 @@
extends Node
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.
func get_node_data(node):
# {"scene": "res://Stations/Hob.tscn", "name": "Hob",
# "xform": Transform3D(Basis(), Vector3(0.29336345, 0.8981018, -1.484)), "props": {}},
var data = {}
data["scene"] = node.scene_file_path
data["name"] = node.name
# global, not local: the copies are respawned under WorldContent, so an
# authored node that was nested inside another (JonScene parents Counter5
# under Counter3) would otherwise land in the wrong place.
data["xform"] = node.global_transform
data["props"] = {}
var scrip = node.get_script()
if scrip:
for i in scrip.get_script_property_list():
# Only authored configuration — i.e. @export vars, which are the ones
# the editor exposes. Plain script variables are live runtime state:
# copying those and replaying them into a fresh instance re-runs their
# setters before the node is in the tree, so any setter touching an
# @onready reference blows up (Table's _state calls into its progress
# bar, which is still null at that point).
if not (i.usage & PROPERTY_USAGE_EDITOR):
continue
if str(i["name"]).begins_with("_"):
continue
data["props"][i["name"]] = node.get(i["name"])
return data
## The authored station nodes sitting in the current scene: anything instanced
## from res://Stations/. Exposed as nodes (not just data) because every peer has
## to remove these originals — the server replaces them with replicated copies,
## and a client that kept its own would end up showing two of everything.
func get_station_nodes() -> Array[Node]:
return _authored_nodes("res://Stations/", "StaticBody3D")
## Likewise for authored items. Containers/ counts as items too — plates and
## trays are things the player carries, and a client that never received one
## would have an incomplete world.
func get_item_nodes() -> Array[Node]:
var found := _authored_nodes("res://Items/", "XRToolsPickable")
found.append_array(_authored_nodes("res://Containers/", "XRToolsPickable"))
return found
func _authored_nodes(dir: String, type: String) -> Array[Node]:
var scenes := []
for file in ResourceLoader.list_directory(dir):
scenes.append(dir + file)
var found: Array[Node] = []
for node in get_tree().root.find_children("*", type, true, false):
if node.scene_file_path in scenes and not found.has(node):
found.append(node)
return found
func get_stations() -> Array[Dictionary]:
var data: Array[Dictionary] = []
for node in get_station_nodes():
data.append(get_node_data(node))
return data
func get_items() -> Array[Dictionary]:
var data: Array[Dictionary] = []
for node in get_item_nodes():
data.append(get_node_data(node))
return data
static func get_stations_old() -> 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_old() -> 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": {}}
-1
View File
@@ -1 +0,0 @@
uid://donvkica3drtx