This commit is contained in:
JonShard
2026-07-25 20:48:39 +02:00
4 changed files with 48 additions and 9 deletions
+4
View File
@@ -61,6 +61,8 @@ func _on_picked_up(_p) -> void:
var by := _pickable.get_picked_up_by()
if not (by is XRToolsFunctionPickup):
return
if NetworkManager.is_online():
NetworkManager.log_line("Grabbed %s by hand, requesting authority" % _pickable.name)
NetworkManager.request_item_authority.rpc_id(1, _pickable.get_path())
@@ -69,6 +71,8 @@ func _on_dropped(_p) -> void:
# apply_held_state() losing authority (see above) must not re-report.
if not is_multiplayer_authority():
return
if NetworkManager.is_online():
NetworkManager.log_line("Dropped %s, reporting release to server" % _pickable.name)
NetworkManager.release_item_authority.rpc_id(
1, _pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity
)
+11
View File
@@ -101,6 +101,8 @@ func spawn_item(scene_path: String, xform: Transform3D, node_name: String = "",
if is_online() and not is_server():
return null
var data := {"scene": scene_path, "xform": xform, "name": node_name, "props": props}
var via := "spawner" if (is_online() and _items_spawner) else "offline"
log_line("spawn_item: %s (name=%s, via=%s)" % [scene_path.get_file(), node_name, via])
if is_online() and _items_spawner:
return _items_spawner.spawn(data)
# Offline: instantiate directly under the registered content root, or (for
@@ -120,6 +122,7 @@ func spawn_item(scene_path: String, xform: Transform3D, node_name: String = "",
## the single seam for destroying spawned items (works offline too).
func despawn_item(node: Node) -> void:
if owns_world() and is_instance_valid(node):
log_line("despawn_item: %s" % node.name)
node.queue_free()
@@ -157,6 +160,7 @@ func _gate_station(node: Node) -> void:
child.enabled = false
child.set_process(false)
node.set_process(false)
log_line("gated station (non-owner peer): %s" % node.name)
# --- Item grab-authority transfer -----------------------------------------
@@ -175,8 +179,10 @@ func request_item_authority(item_path: NodePath) -> void:
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.
log_line("request_item_authority: DENIED %s to peer %d (already held by %d)" % [item.name, sender, np.net_held_by])
force_release_item.rpc_id(sender, item_path)
return
log_line("request_item_authority: granting %s to peer %d" % [str(item.name) if item else str(item_path), sender])
# 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)
@@ -191,6 +197,7 @@ func request_item_authority(item_path: NodePath) -> void:
func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> void:
if not is_server():
return
log_line("release_item_authority: %s released by peer %d" % [str(item_path), multiplayer.get_remote_sender_id()])
_set_item_authority.rpc(item_path, 1)
var item := get_node_or_null(item_path)
if item is RigidBody3D:
@@ -226,6 +233,7 @@ func _try_snap_into_station(item: Node) -> void:
if is_instance_valid(zone.picked_up_object):
continue
if zone.global_position.distance_to(item.global_position) <= zone.grab_distance:
log_line("snapped %s into %s" % [item.name, zone.get_parent().name])
zone.pick_up_object(item)
return
@@ -237,6 +245,7 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void:
var item := get_node_or_null(item_path)
if not item:
return
log_line("_set_item_authority: %s -> peer %d" % [item.name, peer])
item.set_multiplayer_authority(peer) # recursive: item + synchronizer + NetPickable
var np := item.get_node_or_null("NetPickable")
if np:
@@ -248,6 +257,7 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void:
## item was already legitimately held by someone else). The client drops it.
@rpc("authority", "reliable")
func force_release_item(item_path: NodePath) -> void:
log_line("force_release_item: dropping %s (server rejected our grab)" % str(item_path))
var item := get_node_or_null(item_path)
if item and item.has_method("drop"):
item.drop()
@@ -283,6 +293,7 @@ func submit_work(station_path: NodePath, amount: float) -> void:
return
var station := get_node_or_null(station_path)
if station and station.has_method("add_work"):
log_line("submit_work: peer %d contributed %.2f to %s" % [multiplayer.get_remote_sender_id(), amount, station.name])
station.add_work(multiplayer.get_remote_sender_id(), amount)
+4 -4
View File
@@ -43,13 +43,13 @@ mesh = SubResource("BoxMesh_24d3s")
[node name="WorldEnvironment" type="WorldEnvironment" parent="." unique_id=1177077250]
environment = SubResource("Environment_bvwq1")
[node name="WorldContent" type="Node3D" parent="."]
[node name="WorldContent" type="Node3D" parent="." unique_id=291550153]
[node name="Players" type="Node3D" parent="."]
[node name="Players" type="Node3D" parent="." unique_id=1595463693]
[node name="ItemsSpawner" type="MultiplayerSpawner" parent="."]
[node name="ItemsSpawner" type="MultiplayerSpawner" parent="." unique_id=627119248]
spawn_path = NodePath("../WorldContent")
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="."]
[node name="PlayersSpawner" type="MultiplayerSpawner" parent="." unique_id=106645565]
_spawnable_scenes = PackedStringArray("res://Player/net_player.tscn")
spawn_path = NodePath("../Players")
+29 -5
View File
@@ -31,11 +31,27 @@ func _ready() -> void:
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.
# 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()
NetworkManager.world_ready()
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:
@@ -53,10 +69,14 @@ func _populate_world_if_owner() -> void:
return
_populated = true
GameManager.meals_in_play = ["hamburger"]
for d in WorldLayout.get_stations():
var stations := WorldLayout.get_stations()
var items := WorldLayout.get_items()
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 WorldLayout.get_items():
for d in items:
NetworkManager.spawn_item(d["scene"], d["xform"], d["name"], d["props"])
NetworkManager.log_line("World populated")
## Only the server (or the single offline machine) materialises player
@@ -68,6 +88,7 @@ func _on_player_joined(peer_id: int) -> void:
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:
@@ -76,11 +97,14 @@ func _on_player_left(peer_id: int) -> void:
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")