diff --git a/Net/join_panel.gd b/Net/join_panel.gd index 016da2a..4181ea5 100644 --- a/Net/join_panel.gd +++ b/Net/join_panel.gd @@ -8,7 +8,8 @@ signal host_pressed() signal join_pressed(ip: String) signal solo_pressed() -var _ip := "127.0.0.1" +#var _ip := "127.0.0.1" +var _ip := "51.175.10.161" @onready var _ip_label: Label = %IPLabel @onready var _status: Label = %Status diff --git a/Net/network_manager.gd b/Net/network_manager.gd index f49ddf6..add6e43 100644 --- a/Net/network_manager.gd +++ b/Net/network_manager.gd @@ -68,13 +68,20 @@ func join(address: String = "127.0.0.1", port: int = DEFAULT_PORT) -> Error: ## Leave the session and tear down transport. func leave() -> void: - if multiplayer.multiplayer_peer: - multiplayer.multiplayer_peer.close() - multiplayer.multiplayer_peer = null + _go_offline() log_line("Session ended") session_ended.emit() +# Restore Godot's default OfflineMultiplayerPeer (rather than leaving the peer +# null), so is_multiplayer_authority()/get_unique_id() keep working while we are +# back in single-player / menu state. +func _go_offline() -> void: + if multiplayer.multiplayer_peer: + multiplayer.multiplayer_peer.close() + multiplayer.multiplayer_peer = OfflineMultiplayerPeer.new() + + # --- Item spawning --------------------------------------------------------- ## Spawn a networked item. Server-only when online (replicates to all peers via @@ -108,17 +115,25 @@ func _spawn_item_from_data(data: Variant) -> Node: # --- Item grab-authority transfer ----------------------------------------- -## A client requests authority over an item it just grabbed. Runs on the server. +## A client (or host) requests authority over an item it just grabbed. Runs on +## the server. If the item was snapped into a station, the station releases it +## so the grabber cleanly takes ownership. @rpc("any_peer", "reliable") func request_item_authority(item_path: NodePath) -> void: if not is_server(): return var sender := multiplayer.get_remote_sender_id() + # 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) -## A client releases an item, forwarding its throw velocity so the server can -## resume simulating it. Runs on the server. +## A player releases an item, forwarding its throw velocity so the server can +## resume simulating it. Runs on the server. If released next to a station, the +## server snaps it in (server-authoritative placement). @rpc("any_peer", "reliable") func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> void: if not is_server(): @@ -129,6 +144,36 @@ func release_item_authority(item_path: NodePath, lin: Vector3, ang: Vector3) -> item.freeze = false item.linear_velocity = lin item.angular_velocity = ang + _try_snap_into_station(item) + + +# All station snap zones in the world (nodes in the "station" group). +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) + return zones + + +# If the item is snapped into any station, drop it from that station. +func _release_from_snap_zones(item: Node) -> void: + for zone in _station_snap_zones(): + if zone.picked_up_object == item: + zone.drop_object() + + +# Snap the item into the nearest empty station snap zone within grab range. +func _try_snap_into_station(item: Node) -> void: + if not (item is Node3D): + return + for zone in _station_snap_zones(): + if is_instance_valid(zone.picked_up_object): + continue + if zone.global_position.distance_to(item.global_position) <= zone.grab_distance: + zone.pick_up_object(item) + return # Server broadcasts an authority assignment so every peer agrees on who owns the @@ -142,6 +187,7 @@ func _set_item_authority(item_path: NodePath, peer: int) -> void: var np := item.get_node_or_null("NetPickable") if np: np.net_held_by = 0 if peer == 1 else peer + np.apply_held_state() func is_server() -> bool: @@ -213,12 +259,12 @@ func _on_connected_to_server() -> void: func _on_connection_failed() -> void: log_line("connection_failed") - multiplayer.multiplayer_peer = null + _go_offline() connection_failed.emit() func _on_server_disconnected() -> void: log_line("server_disconnected") - multiplayer.multiplayer_peer = null + _go_offline() session_ended.emit() @@ -262,7 +308,11 @@ func _open_log() -> void: log_line("=== NetworkManager log (pid %d) ===" % OS.get_process_id()) func log_line(s: String) -> void: - var line := "[NET %d] %s" % [multiplayer.get_unique_id() if multiplayer.multiplayer_peer else 0, s] + var id := 0 + var p := multiplayer.multiplayer_peer + if p != null and p.get_connection_status() == MultiplayerPeer.CONNECTION_CONNECTED: + id = multiplayer.get_unique_id() + var line := "[NET %d] %s" % [id, s] print(line) if _log_file: _log_file.store_line(line) diff --git a/Prefabs/net_pickable.gd b/Prefabs/net_pickable.gd index c1e11fb..fe1ce4a 100644 --- a/Prefabs/net_pickable.gd +++ b/Prefabs/net_pickable.gd @@ -24,6 +24,17 @@ func _ready() -> void: _apply_authority_state() +## Enable the item only where it is free or where we are its current holder, so +## a peer's snap zone / hand never grabs an item another peer is holding. Called +## by NetworkManager whenever net_held_by changes. Note: net_held_by is set per +## peer by RPC (not by a synchronizer), so the holder keeps its item enabled +## while everyone else disables their copy. +func apply_held_state() -> void: + if not _item: + return + _item.enabled = net_held_by == 0 or net_held_by == multiplayer.get_unique_id() + + func _physics_process(_delta: float) -> void: if not NetworkManager.is_online(): return diff --git a/Stations/Counter.tscn b/Stations/Counter.tscn index 7a0abb4..a5f6119 100644 --- a/Stations/Counter.tscn +++ b/Stations/Counter.tscn @@ -26,7 +26,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0) collision_layer = 65536 collision_mask = 65536 script = ExtResource("1_dlkho") -snap_mode = 1 +snap_mode = 0 metadata/_custom_type_script = "uid://cquqe4f1m1sw6" [node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=969340130] diff --git a/Stations/Hob.tscn b/Stations/Hob.tscn index f59a348..35f44ed 100644 --- a/Stations/Hob.tscn +++ b/Stations/Hob.tscn @@ -39,7 +39,7 @@ transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0.5270121, 0) collision_layer = 65536 collision_mask = 65536 script = ExtResource("2_er1dp") -snap_mode = 1 +snap_mode = 0 metadata/_custom_type_script = "uid://cquqe4f1m1sw6" [node name="CollisionShape3D2" type="CollisionShape3D" parent="XRToolsSnapZone" unique_id=370920392] diff --git a/main.gd b/main.gd index 274be2e..3fdb06c 100644 --- a/main.gd +++ b/main.gd @@ -23,6 +23,7 @@ func _ready() -> void: NetworkManager.player_left.connect(_on_player_left) NetworkManager.session_started.connect(_on_session_started) NetworkManager.connection_failed.connect(_on_connection_failed) + NetworkManager.session_ended.connect(_on_session_ended) _menu.host_pressed.connect(_on_menu_host) _menu.join_pressed.connect(_on_menu_join) _menu.solo_pressed.connect(_on_menu_solo) @@ -66,6 +67,9 @@ func _on_session_started(is_server: bool) -> void: if not is_server: # Connected to a host as a client. _menu.hide_menu() + # Stations are server-authoritative: clients must not run snap zones, or + # they create divergent local copies of snapped items. + _disable_client_snap_zones() func _on_connection_failed() -> void: @@ -74,6 +78,19 @@ func _on_connection_failed() -> void: _menu.show_menu() +# The client lost the server (or left): reload the scene to return to a clean +# main-menu state. +func _on_session_ended() -> void: + get_tree().reload_current_scene.call_deferred() + + +func _disable_client_snap_zones() -> void: + for station in get_tree().get_nodes_in_group("station"): + var zone = station.get_node_or_null("XRToolsSnapZone") + if zone: + zone.enabled = false + + # --- Player / item spawning ------------------------------------------------ func _on_player_joined(peer_id: int) -> void: