From bf31f85b02cbc3a15927727eadb644d4cccf113b Mon Sep 17 00:00:00 2001 From: JonShard Date: Mon, 10 Aug 2026 13:02:19 +0200 Subject: [PATCH] Fix StationMovement in Multiplayer, still works singleplayer --- Prefabs/XROrigin.tscn | 48 ++++++------- Stations/station_movement.gd | 112 ++++++++++++++++++++++++++----- global/debug/move_windows.gd | 19 ++++++ global/debug/move_windows.gd.uid | 1 + global/global_key_events.gd | 6 +- 5 files changed, 144 insertions(+), 42 deletions(-) create mode 100644 global/debug/move_windows.gd create mode 100644 global/debug/move_windows.gd.uid diff --git a/Prefabs/XROrigin.tscn b/Prefabs/XROrigin.tscn index 76d52fd..9fb53f5 100644 --- a/Prefabs/XROrigin.tscn +++ b/Prefabs/XROrigin.tscn @@ -10,34 +10,34 @@ [ext_resource type="PackedScene" uid="uid://cqhw276realc" path="res://addons/godot-xr-tools/functions/function_pointer.tscn" id="6_ptr"] [ext_resource type="PackedScene" uid="uid://bjcxf427un2wp" path="res://addons/godot-xr-tools/player/poke/poke.tscn" id="7_1fmci"] -[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_ouruy"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_1fmci"] animation = &"Grip" -[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_i0gvw"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_itrmp"] animation = &"Grip" -[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_5mmou"] +[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_nrfrd"] filter_enabled = true filters = ["Armature/Skeleton3D:Little_Distal_L", "Armature/Skeleton3D:Little_Intermediate_L", "Armature/Skeleton3D:Little_Metacarpal_L", "Armature/Skeleton3D:Little_Proximal_L", "Armature/Skeleton3D:Middle_Distal_L", "Armature/Skeleton3D:Middle_Intermediate_L", "Armature/Skeleton3D:Middle_Metacarpal_L", "Armature/Skeleton3D:Middle_Proximal_L", "Armature/Skeleton3D:Ring_Distal_L", "Armature/Skeleton3D:Ring_Intermediate_L", "Armature/Skeleton3D:Ring_Metacarpal_L", "Armature/Skeleton3D:Ring_Proximal_L", "Armature/Skeleton3D:Thumb_Distal_L", "Armature/Skeleton3D:Thumb_Metacarpal_L", "Armature/Skeleton3D:Thumb_Proximal_L", "Armature/Skeleton:Little_Distal_L", "Armature/Skeleton:Little_Intermediate_L", "Armature/Skeleton:Little_Proximal_L", "Armature/Skeleton:Middle_Distal_L", "Armature/Skeleton:Middle_Intermediate_L", "Armature/Skeleton:Middle_Proximal_L", "Armature/Skeleton:Ring_Distal_L", "Armature/Skeleton:Ring_Intermediate_L", "Armature/Skeleton:Ring_Proximal_L", "Armature/Skeleton:Thumb_Distal_L", "Armature/Skeleton:Thumb_Proximal_L"] -[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_yfij0"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_73fu8"] animation = &"Grip 5" -[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_2ebvs"] +[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_ouruy"] filter_enabled = true filters = ["Armature/Skeleton3D:Index_Distal_L", "Armature/Skeleton3D:Index_Intermediate_L", "Armature/Skeleton3D:Index_Metacarpal_L", "Armature/Skeleton3D:Index_Proximal_L", "Armature/Skeleton:Index_Distal_L", "Armature/Skeleton:Index_Intermediate_L", "Armature/Skeleton:Index_Proximal_L"] -[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_l8v2a"] +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_i0gvw"] graph_offset = Vector2(-536, 11) -nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_ouruy") +nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_1fmci") nodes/ClosedHand1/position = Vector2(-600, 300) -nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_i0gvw") +nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_itrmp") nodes/ClosedHand2/position = Vector2(-360, 300) -nodes/Grip/node = SubResource("AnimationNodeBlend2_5mmou") +nodes/Grip/node = SubResource("AnimationNodeBlend2_nrfrd") nodes/Grip/position = Vector2(0, 20) -nodes/OpenHand/node = SubResource("AnimationNodeAnimation_yfij0") +nodes/OpenHand/node = SubResource("AnimationNodeAnimation_73fu8") nodes/OpenHand/position = Vector2(-600, 100) -nodes/Trigger/node = SubResource("AnimationNodeBlend2_2ebvs") +nodes/Trigger/node = SubResource("AnimationNodeBlend2_ouruy") nodes/Trigger/position = Vector2(-360, 20) node_connections = [&"output", 0, &"Grip", &"Grip", 0, &"Trigger", &"Grip", 1, &"ClosedHand2", &"Trigger", 0, &"OpenHand", &"Trigger", 1, &"ClosedHand1"] @@ -45,34 +45,34 @@ node_connections = [&"output", 0, &"Grip", &"Grip", 0, &"Trigger", &"Grip", 1, & transparency = 1 albedo_color = Color(1, 1, 1, 0.5882353) -[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_qpjda"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_5mmou"] animation = &"Grip" -[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_37hbl"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_yfij0"] animation = &"Grip" -[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_to5tn"] +[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_2ebvs"] filter_enabled = true filters = ["Armature/Skeleton3D:Little_Distal_R", "Armature/Skeleton3D:Little_Intermediate_R", "Armature/Skeleton3D:Little_Metacarpal_R", "Armature/Skeleton3D:Little_Proximal_R", "Armature/Skeleton3D:Middle_Distal_R", "Armature/Skeleton3D:Middle_Intermediate_R", "Armature/Skeleton3D:Middle_Metacarpal_R", "Armature/Skeleton3D:Middle_Proximal_R", "Armature/Skeleton3D:Ring_Distal_R", "Armature/Skeleton3D:Ring_Intermediate_R", "Armature/Skeleton3D:Ring_Metacarpal_R", "Armature/Skeleton3D:Ring_Proximal_R", "Armature/Skeleton3D:Thumb_Distal_R", "Armature/Skeleton3D:Thumb_Metacarpal_R", "Armature/Skeleton3D:Thumb_Proximal_R", "Armature/Skeleton:Little_Distal_R", "Armature/Skeleton:Little_Intermediate_R", "Armature/Skeleton:Little_Proximal_R", "Armature/Skeleton:Middle_Distal_R", "Armature/Skeleton:Middle_Intermediate_R", "Armature/Skeleton:Middle_Proximal_R", "Armature/Skeleton:Ring_Distal_R", "Armature/Skeleton:Ring_Intermediate_R", "Armature/Skeleton:Ring_Proximal_R", "Armature/Skeleton:Thumb_Distal_R", "Armature/Skeleton:Thumb_Proximal_R"] -[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_nlnu7"] +[sub_resource type="AnimationNodeAnimation" id="AnimationNodeAnimation_l8v2a"] animation = &"Grip 5" -[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_we6ah"] +[sub_resource type="AnimationNodeBlend2" id="AnimationNodeBlend2_qpjda"] filter_enabled = true filters = ["Armature/Skeleton3D:Index_Distal_R", "Armature/Skeleton3D:Index_Intermediate_R", "Armature/Skeleton3D:Index_Metacarpal_R", "Armature/Skeleton3D:Index_Proximal_R", "Armature/Skeleton:Index_Distal_R", "Armature/Skeleton:Index_Intermediate_R", "Armature/Skeleton:Index_Proximal_R"] -[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_dtgvk"] +[sub_resource type="AnimationNodeBlendTree" id="AnimationNodeBlendTree_37hbl"] graph_offset = Vector2(-552.664, 107.301) -nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_qpjda") +nodes/ClosedHand1/node = SubResource("AnimationNodeAnimation_5mmou") nodes/ClosedHand1/position = Vector2(-600, 300) -nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_37hbl") +nodes/ClosedHand2/node = SubResource("AnimationNodeAnimation_yfij0") nodes/ClosedHand2/position = Vector2(-360, 300) -nodes/Grip/node = SubResource("AnimationNodeBlend2_to5tn") +nodes/Grip/node = SubResource("AnimationNodeBlend2_2ebvs") nodes/Grip/position = Vector2(0, 40) -nodes/OpenHand/node = SubResource("AnimationNodeAnimation_nlnu7") +nodes/OpenHand/node = SubResource("AnimationNodeAnimation_l8v2a") nodes/OpenHand/position = Vector2(-600, 100) -nodes/Trigger/node = SubResource("AnimationNodeBlend2_we6ah") +nodes/Trigger/node = SubResource("AnimationNodeBlend2_qpjda") nodes/Trigger/position = Vector2(-360, 40) node_connections = [&"output", 0, &"Grip", &"Grip", 0, &"Trigger", &"Grip", 1, &"ClosedHand2", &"Trigger", 0, &"OpenHand", &"Trigger", 1, &"ClosedHand1"] @@ -120,7 +120,7 @@ bone_idx = 9 [node name="Poke" parent="XRControllerLeftHand/LeftHand/Hand_low_L/Armature/Skeleton3D/BoneAttachment3D" unique_id=259928841 instance=ExtResource("7_1fmci")] [node name="AnimationTree" parent="XRControllerLeftHand/LeftHand" index="1"] -tree_root = SubResource("AnimationNodeBlendTree_l8v2a") +tree_root = SubResource("AnimationNodeBlendTree_i0gvw") [node name="MovementDirect" parent="XRControllerLeftHand" unique_id=1865515938 instance=ExtResource("2_q1xvu")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -0.009340696, -0.0074635968, 0.024033919) @@ -181,7 +181,7 @@ bone_idx = 9 [node name="Poke" parent="XRControllerRightHand/RightHand/Hand_low_R/Armature/Skeleton3D/BoneAttachment3D" unique_id=303927399 instance=ExtResource("7_1fmci")] [node name="AnimationTree" parent="XRControllerRightHand/RightHand" index="1"] -tree_root = SubResource("AnimationNodeBlendTree_dtgvk") +tree_root = SubResource("AnimationNodeBlendTree_37hbl") [node name="MovementTurn" parent="XRControllerRightHand" unique_id=154529119 instance=ExtResource("4_rd8py")] transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -0.0074635968, 0.024033919) diff --git a/Stations/station_movement.gd b/Stations/station_movement.gd index 011bf73..669aab7 100644 --- a/Stations/station_movement.gd +++ b/Stations/station_movement.gd @@ -7,11 +7,15 @@ extends Node var move_handle_rigid: RigidBody3D var original_collision_layer: int = 0 -var original_y_pos: float = 0.0 +var original_handle_y_pos: float = 0.0 var original_station_transform: Transform3D = Transform3D.IDENTITY @export var moving: bool = false var ghost_materials: Array[StandardMaterial3D] = [] +# Ghost update throttling (clients send unreliable RPCs to server at this rate) +var ghost_send_interval: float = 0.1 # seconds (10 Hz) +var _ghost_send_accum: float = 0.0 + const GHOST_COLOR_VALID: Color = Color(0, 1, 0, 0.35) const GHOST_COLOR_INVALID: Color = Color(1, 0, 0, 0.35) @@ -42,7 +46,7 @@ func _ready() -> void: move_handle_rigid = move_handle as RigidBody3D original_collision_layer = station.collision_layer - original_y_pos = move_handle.transform.origin.y + original_handle_y_pos = move_handle.transform.origin.y original_station_transform = station.global_transform move_handle.picked_up.connect(_handle_pickup) move_handle.dropped.connect(_handle_drop) @@ -93,16 +97,21 @@ func _handle_pickup(_by: Node) -> void: if move_handle.get_picked_up_by() is XRToolsSnapZone: print("StationMovement handle pickup by snap zone, dropping and resetting position") move_handle.drop() - move_handle.global_position = station.global_position + Vector3(0, original_y_pos, 0) + move_handle.global_position = station.global_position + Vector3(0, original_handle_y_pos, 0) move_handle.rotation = station.rotation return - + moving = true - move_ghost.visible = true - station.visible = false station.collision_layer = 0 original_station_transform = station.global_transform move_ghost.global_transform = original_station_transform + if NetworkManager.is_online(): + # Ask the server to update visibility (reliable) + server_update_visibility.rpc_id(1, true, false) + else: + # If not connected to server (single-player or test), apply locally + move_ghost.visible = true + station.visible = false func _handle_drop(_by: Node) -> void: @@ -111,24 +120,43 @@ func _handle_drop(_by: Node) -> void: move_ghost.visible = false station.visible = true station.collision_layer = original_collision_layer - if _is_move_position_valid(): - station.global_transform = move_ghost.global_transform - else: - station.global_transform = original_station_transform - move_handle.global_position = station.global_position + Vector3(0, original_y_pos, 0) - move_handle.rotation = station.rotation + var is_valid: bool = _is_move_position_valid() + if NetworkManager.is_online(): + server_handle_drop.rpc_id(1, move_ghost.global_transform, is_valid) + else: + # If not connected to server (single-player or test), apply locally only when valid + if is_valid: + station.global_transform = move_ghost.global_transform + move_handle.global_transform = move_ghost.global_transform.translated(Vector3(0, original_handle_y_pos, 0)) + print("StationMovement handle drop (local apply), station authority: ", station.get_multiplayer_authority(), " move_handle authority: ", move_handle.get_multiplayer_authority()) + else: + station.global_transform = original_station_transform + move_handle.global_transform = original_station_transform.translated(Vector3(0, original_handle_y_pos, 0)) + print("StationMovement handle drop (local reject), restoring original position") func _process(_delta: float) -> void: if not moving: return + # Update the ghost locally for instant feedback and send throttled unreliable RPCs to the server + move_ghost.visible = true + station.visible = false + move_ghost.global_transform = Helper.get_snapped_transform(move_handle) move_ghost.global_transform.origin.y = original_station_transform.origin.y - if _is_move_position_valid(): - _set_ghost_color(GHOST_COLOR_VALID) - else: - _set_ghost_color(GHOST_COLOR_INVALID) + + var ghost_color = GHOST_COLOR_VALID if _is_move_position_valid() else GHOST_COLOR_INVALID + _set_ghost_color(ghost_color) + + # Throttle and send ghost transform updates to server so other clients see the preview + _ghost_send_accum += _delta + if _ghost_send_accum >= ghost_send_interval: + _ghost_send_accum = 0.0 + # Unreliable RPC to the server to update the server-side ghost; server will replicate to clients + # Use rpc_unreliable_id to avoid blocking traffic + if NetworkManager.is_online(): + server_update_move_ghost_transform.rpc_id(1, move_ghost.global_transform, ghost_color) func _is_move_position_valid() -> bool: @@ -146,9 +174,59 @@ func _is_move_position_valid() -> bool: return false for area in move_ghost.get_overlapping_areas(): - print("Overlapping area: ", area) + #print("Overlapping area: ", area) if area == move_ghost: continue return false return true + + +@rpc("any_peer", "call_remote", "reliable") +func server_handle_drop(new_transform: Transform3D, client_thinks_valid: bool): + if not multiplayer.is_server(): + return + if not (client_thinks_valid): + # Reset state for everyone, including server + var original_trans = original_station_transform # Make a copy to avoid server_update_station_transform setting it before move handle uses it + server_update_station_transform.rpc(original_trans) + server_update_move_ghost_transform.rpc(original_trans) + server_update_handle_transform.rpc(original_trans.translated(Vector3(0, original_handle_y_pos, 0))) + server_update_visibility.rpc(false, true) + print("StationMovement server_handle_drop: transform rejected (invalid)") + return + + server_update_station_transform.rpc(new_transform) + server_update_move_ghost_transform.rpc(new_transform) + server_update_handle_transform.rpc(new_transform.translated(Vector3(0, original_handle_y_pos, 0))) + server_update_visibility.rpc(false, true) + + print("StationMovement server_handle_drop: transform applied") + + +@rpc("any_peer", "call_local", "reliable") # Server updates clients +func server_update_station_transform(new_transform: Transform3D) -> void: + print("StationMovement server_update_station_transform") + station.global_transform = new_transform + original_station_transform = new_transform + + +@rpc("any_peer", "call_local", "reliable") # Server updates clients +func server_update_handle_transform(new_transform: Transform3D) -> void: + print("StationMovement server_update_handle_transform") + move_handle.global_transform = new_transform + + + +@rpc("any_peer", "call_local", "reliable") +func server_update_visibility(ghost_visible: bool, station_visible: bool) -> void: + print("StationMovement server_update_visibility") + move_ghost.visible = ghost_visible + station.visible = station_visible + + +@rpc("any_peer", "call_local", "unreliable") +func server_update_move_ghost_transform(new_transform: Transform3D, new_color: Color = Color.YELLOW) -> void: + print("StationMovement server_update_move_ghost_transform") + move_ghost.global_transform = new_transform + _set_ghost_color(new_color) diff --git a/global/debug/move_windows.gd b/global/debug/move_windows.gd new file mode 100644 index 0000000..5652c9d --- /dev/null +++ b/global/debug/move_windows.gd @@ -0,0 +1,19 @@ +extends Node + +func _ready() -> void: + if OS.has_feature("editor"): + # Force subwindows to be native OS windows instead of embedded viewports + get_tree().root.gui_embed_subwindows = false + get_tree().create_timer(0.05).timeout.connect(_position_window) + +func _position_window() -> void: + var is_server: bool = OS.get_cmdline_args().has("--server") + var screen_rect: Rect2i = DisplayServer.screen_get_usable_rect(DisplayServer.window_get_current_screen()) + + var window_width: int = screen_rect.size.x / 2 + var window_height: int = int(screen_rect.size.y * (2.0 / 3.0)) + + DisplayServer.window_set_size(Vector2i(window_width - 10, window_height)) + + var x_pos: int = screen_rect.position.x + window_width if is_server else screen_rect.position.x + DisplayServer.window_set_position(Vector2i(x_pos, screen_rect.position.y)) diff --git a/global/debug/move_windows.gd.uid b/global/debug/move_windows.gd.uid new file mode 100644 index 0000000..ca74d16 --- /dev/null +++ b/global/debug/move_windows.gd.uid @@ -0,0 +1 @@ +uid://de8j52uus1pbv diff --git a/global/global_key_events.gd b/global/global_key_events.gd index 2d45358..c51aa55 100644 --- a/global/global_key_events.gd +++ b/global/global_key_events.gd @@ -2,7 +2,7 @@ extends Node func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("quit_game"): - get_tree().quit() + all_quit_game.rpc() if event.is_action_pressed("satisfy_table_orders"): satisfy_table_orders() if event.is_action_pressed("set_building_mode"): @@ -12,6 +12,10 @@ func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("set_game_over"): set_game_over() +# Kill all clients +@rpc("any_peer", "call_local", "reliable") +func all_quit_game(): + get_tree().quit() # Finds any node of class/type Table and calls satisfyAllOrders() func satisfy_table_orders() -> void: