asd
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
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
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_pickable = get_parent() as XRToolsPickable
|
||||
if not _pickable:
|
||||
push_error("NetPickable must be a child of an XRToolsPickable")
|
||||
return
|
||||
_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:
|
||||
net_held_by = value
|
||||
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.
|
||||
func apply_held_state() -> void:
|
||||
if not _pickable:
|
||||
return
|
||||
if not NetworkManager.is_online() or is_multiplayer_authority():
|
||||
# We own this item's simulation (offline, loose+server, or currently
|
||||
# holding it): leave physics alone, XRToolsPickable manages the rest.
|
||||
return
|
||||
# Someone else owns it: stop simulating locally, just follow the sync.
|
||||
if _pickable.is_picked_up():
|
||||
_pickable.drop()
|
||||
_pickable.freeze = true
|
||||
_pickable.freeze_mode = RigidBody3D.FREEZE_MODE_KINEMATIC
|
||||
_pickable.collision_mask = 0
|
||||
_pickable.enabled = (net_held_by == 0)
|
||||
|
||||
|
||||
## 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):
|
||||
return
|
||||
NetworkManager.request_item_authority.rpc_id(1, _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():
|
||||
return
|
||||
NetworkManager.release_item_authority.rpc_id(
|
||||
1, _pickable.get_path(), _pickable.linear_velocity, _pickable.angular_velocity
|
||||
)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwd0pe2udb5xo
|
||||
+99
-13
@@ -24,9 +24,15 @@ signal connection_failed()
|
||||
var _world: Node = null
|
||||
var _players_spawner: MultiplayerSpawner = null
|
||||
var _items_spawner: MultiplayerSpawner = null
|
||||
var _content_root: Node = null
|
||||
|
||||
var _log_file: FileAccess
|
||||
|
||||
## Reason the session ended, shown by the menu on the next _ready() (see
|
||||
## take_status). Avoids depending on a live signal connection to a panel that
|
||||
## doesn't exist yet at the moment the session actually ends.
|
||||
var last_status := ""
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
_open_log()
|
||||
@@ -80,26 +86,43 @@ func _go_offline() -> void:
|
||||
if multiplayer.multiplayer_peer:
|
||||
multiplayer.multiplayer_peer.close()
|
||||
multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new()
|
||||
unregister_world()
|
||||
|
||||
|
||||
# --- Item spawning ---------------------------------------------------------
|
||||
|
||||
## Spawn a networked item. Server-only when online (replicates to all peers via
|
||||
## the ItemsSpawner); works directly when offline. Returns the new node on the
|
||||
## machine that owns spawning, else null.
|
||||
func spawn_item(scene_path: String, xform: Transform3D) -> Node:
|
||||
## the ItemsSpawner, including late joiners); works directly when offline.
|
||||
## node_name gives the spawned node a deterministic, identical name on every
|
||||
## peer (needed for NodePath-based RPCs to resolve it); props are applied to
|
||||
## the instance before it enters the tree, so exported vars land correctly.
|
||||
## Returns the new node on the machine that owns spawning, else null.
|
||||
func spawn_item(scene_path: String, xform: Transform3D, node_name: String = "", props: Dictionary = {}) -> Node:
|
||||
if is_online() and not is_server():
|
||||
return null
|
||||
var data := {"scene": scene_path, "xform": xform}
|
||||
var data := {"scene": scene_path, "xform": xform, "name": node_name, "props": props}
|
||||
if is_online() and _items_spawner:
|
||||
return _items_spawner.spawn(data)
|
||||
# Offline: instantiate directly under the world.
|
||||
# Offline: instantiate directly under the registered content root, or (for
|
||||
# scenes that never call register_world, e.g. the offline menu/dev scenes)
|
||||
# the current scene, so this keeps working without every offline scene
|
||||
# needing to opt in.
|
||||
var inst := _spawn_item_from_data(data)
|
||||
if _world and inst:
|
||||
_world.add_child(inst)
|
||||
if inst:
|
||||
var parent: Node = _content_root if _content_root else get_tree().current_scene
|
||||
if parent:
|
||||
parent.add_child(inst)
|
||||
return inst
|
||||
|
||||
|
||||
## Despawn a server-spawned item. MultiplayerSpawner broadcasts a despawn to
|
||||
## every peer when a tracked node exits the tree on the authority, so this is
|
||||
## the single seam for destroying spawned items (works offline too).
|
||||
func despawn_item(node: Node) -> void:
|
||||
if owns_world() and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
|
||||
|
||||
# MultiplayerSpawner custom spawn function: runs on every peer to build the node
|
||||
# from the replicated payload.
|
||||
func _spawn_item_from_data(data: Variant) -> Node:
|
||||
@@ -110,9 +133,32 @@ func _spawn_item_from_data(data: Variant) -> Node:
|
||||
var inst := scene.instantiate()
|
||||
if inst is Node3D:
|
||||
inst.transform = data["xform"]
|
||||
if data.get("name", "") != "":
|
||||
inst.name = data["name"]
|
||||
for key in data.get("props", {}):
|
||||
inst.set(key, data["props"][key])
|
||||
if not owns_world():
|
||||
_gate_station(inst)
|
||||
return inst
|
||||
|
||||
|
||||
# Stations run their own logic and auto-grab (XRToolsSnapZone with
|
||||
# snap_mode=RANGE) identically on every peer by default, which would let each
|
||||
# peer independently grab/simulate the same shared object. Disable both on
|
||||
# every peer except the one that owns world logic; the server-authoritative
|
||||
# item-authority RPCs are what let clients still grab a server-held item by
|
||||
# hand. Runs before the node enters the tree, so its own _ready() sees the
|
||||
# final (disabled) state.
|
||||
func _gate_station(node: Node) -> void:
|
||||
if not (node is StaticBody3D):
|
||||
return
|
||||
for child in node.get_children():
|
||||
if child is XRToolsSnapZone:
|
||||
child.enabled = false
|
||||
child.set_process(false)
|
||||
node.set_process(false)
|
||||
|
||||
|
||||
# --- Item grab-authority transfer -----------------------------------------
|
||||
|
||||
## A client (or host) requests authority over an item it just grabbed. Runs on
|
||||
@@ -123,10 +169,17 @@ func request_item_authority(item_path: NodePath) -> void:
|
||||
if not is_server():
|
||||
return
|
||||
var sender := multiplayer.get_remote_sender_id()
|
||||
var item := get_node_or_null(item_path)
|
||||
if item:
|
||||
var np := item.get_node_or_null("NetPickable")
|
||||
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.
|
||||
force_release_item.rpc_id(sender, item_path)
|
||||
return
|
||||
# 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)
|
||||
var item := get_node_or_null(item_path)
|
||||
if item:
|
||||
_release_from_snap_zones(item)
|
||||
|
||||
@@ -147,13 +200,14 @@ func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) ->
|
||||
_try_snap_into_station(item)
|
||||
|
||||
|
||||
# All station snap zones in the world (nodes in the "station" group).
|
||||
# All station snap zones in the world (every XRToolsSnapZone child of a node in
|
||||
# the "station" group — some stations, e.g. Table, have more than one).
|
||||
func _station_snap_zones() -> Array:
|
||||
var zones := []
|
||||
for station in get_tree().get_nodes_in_group("station"):
|
||||
var zone = station.get_node_or_null("XRToolsSnapZone")
|
||||
if zone:
|
||||
zones.append(zone)
|
||||
for child in station.get_children():
|
||||
if child is XRToolsSnapZone:
|
||||
zones.append(child)
|
||||
return zones
|
||||
|
||||
|
||||
@@ -190,6 +244,15 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void:
|
||||
np.apply_held_state()
|
||||
|
||||
|
||||
## Server tells a specific client that its optimistic grab was rejected (the
|
||||
## item was already legitimately held by someone else). The client drops it.
|
||||
@rpc("authority", "reliable")
|
||||
func force_release_item(item_path: NodePath) -> void:
|
||||
var item := get_node_or_null(item_path)
|
||||
if item and item.has_method("drop"):
|
||||
item.drop()
|
||||
|
||||
|
||||
func is_server() -> bool:
|
||||
return is_online() and multiplayer.is_server()
|
||||
|
||||
@@ -223,16 +286,37 @@ func submit_work(station_path: NodePath, amount: float) -> void:
|
||||
station.add_work(multiplayer.get_remote_sender_id(), amount)
|
||||
|
||||
|
||||
## Called by main.gd once the world scene is ready, passing its spawners.
|
||||
## Called by the world scene once it's ready, passing its spawners. Must run
|
||||
## before world_ready()/host()/join() on every peer so the custom spawn
|
||||
## function is installed before any spawn packet can arrive.
|
||||
func register_world(world: Node, players_spawner: MultiplayerSpawner, items_spawner: MultiplayerSpawner) -> void:
|
||||
_world = world
|
||||
_players_spawner = players_spawner
|
||||
_items_spawner = items_spawner
|
||||
_content_root = items_spawner.get_node(items_spawner.spawn_path) if items_spawner else world
|
||||
if _items_spawner:
|
||||
_items_spawner.spawn_function = _spawn_item_from_data
|
||||
log_line("World registered (players_spawner=%s items_spawner=%s)" % [str(players_spawner != null), str(items_spawner != null)])
|
||||
|
||||
|
||||
## Called when the world scene goes away (disconnect, leaving the session) so
|
||||
## the autoload doesn't hold stale/freed references across a scene reload.
|
||||
func unregister_world() -> void:
|
||||
_world = null
|
||||
_content_root = null
|
||||
_players_spawner = null
|
||||
_items_spawner = null
|
||||
|
||||
|
||||
## Returns the reason the last session ended (if any) and clears it. The menu
|
||||
## pulls this on its own _ready() rather than depending on a live signal
|
||||
## connection to a panel that doesn't exist yet when the session ends.
|
||||
func take_status() -> String:
|
||||
var s := last_status
|
||||
last_status = ""
|
||||
return s
|
||||
|
||||
|
||||
## Called by main.gd after it has registered the world and connected its
|
||||
## player_joined/left listeners. Kicks off any menu- or command-line-driven
|
||||
## session so that session signals never fire before the world is listening.
|
||||
@@ -286,11 +370,13 @@ func _on_connected_to_server() -> void:
|
||||
func _on_connection_failed() -> void:
|
||||
log_line("connection_failed")
|
||||
_go_offline()
|
||||
last_status = "Could not connect"
|
||||
connection_failed.emit()
|
||||
|
||||
func _on_server_disconnected() -> void:
|
||||
log_line("server_disconnected")
|
||||
_go_offline()
|
||||
last_status = "Host disconnected"
|
||||
session_ended.emit()
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
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.
|
||||
|
||||
static func get_stations() -> 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() -> 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": {}}
|
||||
@@ -0,0 +1 @@
|
||||
uid://donvkica3drtx
|
||||
Reference in New Issue
Block a user