From e35eb0c9c8932858f7496112216325c36e56821c Mon Sep 17 00:00:00 2001 From: copygirl Date: Sun, 9 Feb 2025 21:57:09 +0100 Subject: [PATCH] Adjust player position via dragging Changes the mod settings UI to use a tabbed system, split up into "Connect", "Settings" and "Players". In the "Players" tab, the position of each remote player can be changed via the provided input controls. Additionally, while this tab is active, you can also click and drag players directly to reposition them. Removed the temporary positioning system. --- Resources/copy_multiplayer_settings.gd | 20 -- Resources/copy_multiplayer_settings.tscn | 113 ----------- Scenes/copy_multiplayer_settings.gd | 24 +++ Scenes/copy_multiplayer_settings.tscn | 159 ++++++++++++++++ Scenes/player_settings.gd | 41 ++++ Scenes/player_settings.tscn | 179 ++++++++++++++++++ .../sync_controller.gd | 108 ++++++++++- Scenes/sync_controller.tscn | 9 + stream_buffer.gd => Utility/stream_buffer.gd | 0 copyMultiplayer.gd | 100 ++++------ 10 files changed, 556 insertions(+), 197 deletions(-) delete mode 100644 Resources/copy_multiplayer_settings.gd delete mode 100644 Resources/copy_multiplayer_settings.tscn create mode 100644 Scenes/copy_multiplayer_settings.gd create mode 100644 Scenes/copy_multiplayer_settings.tscn create mode 100644 Scenes/player_settings.gd create mode 100644 Scenes/player_settings.tscn rename sync_controller.gd => Scenes/sync_controller.gd (63%) create mode 100644 Scenes/sync_controller.tscn rename stream_buffer.gd => Utility/stream_buffer.gd (100%) diff --git a/Resources/copy_multiplayer_settings.gd b/Resources/copy_multiplayer_settings.gd deleted file mode 100644 index 2d33668..0000000 --- a/Resources/copy_multiplayer_settings.gd +++ /dev/null @@ -1,20 +0,0 @@ -extends Container - -@export var visible_icon: Texture2D -@export var hidden_icon: Texture2D - -func _on_cache_dir_dialog_pressed() -> void: - var widget: LineEdit = $"Cache/LineEdit" - - var dialog: FileDialog = $"Cache/FileDialog" - dialog.size = get_window().size / 2 - dialog.position = get_window().size / 4 - dialog.current_dir = widget.text - dialog.popup() - - widget.text = await dialog.dir_selected - widget.text_changed.emit(widget.text) - -func _on_show_hide_address_toggled(toggled_on: bool) -> void: - $"Host/Address".secret = !toggled_on - $"Host/ShowHide".icon = visible_icon if toggled_on else hidden_icon diff --git a/Resources/copy_multiplayer_settings.tscn b/Resources/copy_multiplayer_settings.tscn deleted file mode 100644 index a64c046..0000000 --- a/Resources/copy_multiplayer_settings.tscn +++ /dev/null @@ -1,113 +0,0 @@ -[gd_scene load_steps=4 format=3 uid="uid://cdxnwsgmevndb"] - -[ext_resource type="Script" path="res://Mods/copyMultiplayer/Resources/copy_multiplayer_settings.gd" id="1_7d55i"] -[ext_resource type="Texture2D" uid="uid://qbho5oyu1kfa" path="res://Mods/copyMultiplayer/Resources/hidden.png" id="2_1u5pu"] -[ext_resource type="Texture2D" uid="uid://dcmljlb2v6p16" path="res://Mods/copyMultiplayer/Resources/visible.png" id="2_ibe7i"] - -[node name="copyMultiplayerSettings" type="VBoxContainer"] -anchors_preset = 15 -anchor_right = 1.0 -anchor_bottom = 1.0 -grow_horizontal = 2 -grow_vertical = 2 -size_flags_horizontal = 3 -size_flags_vertical = 3 -script = ExtResource("1_7d55i") -visible_icon = ExtResource("2_ibe7i") -hidden_icon = ExtResource("2_1u5pu") - -[node name="Cache" type="HBoxContainer" parent="."] -layout_mode = 2 - -[node name="Label" type="Label" parent="Cache"] -custom_minimum_size = Vector2(60, 0) -layout_mode = 2 -text = "Cache" - -[node name="LineEdit" type="LineEdit" parent="Cache"] -layout_mode = 2 -size_flags_horizontal = 3 -tooltip_text = "The directory other players' models will be loaded from. -Filenames must match exactly for this to work." - -[node name="Button" type="Button" parent="Cache"] -layout_mode = 2 -tooltip_text = "Browse Directory" -text = " ... " - -[node name="FileDialog" type="FileDialog" parent="Cache"] -title = "Open a Directory" -ok_button_text = "Select Current Folder" -file_mode = 2 -access = 2 - -[node name="Name" type="HBoxContainer" parent="."] -layout_mode = 2 - -[node name="Label" type="Label" parent="Name"] -custom_minimum_size = Vector2(60, 0) -layout_mode = 2 -text = "Name" - -[node name="LineEdit" type="LineEdit" parent="Name"] -layout_mode = 2 -size_flags_horizontal = 3 - -[node name="Host" type="HBoxContainer" parent="."] -layout_mode = 2 - -[node name="Label" type="Label" parent="Host"] -custom_minimum_size = Vector2(60, 0) -layout_mode = 2 -text = "Host" - -[node name="Address" type="LineEdit" parent="Host"] -layout_mode = 2 -size_flags_horizontal = 3 -tooltip_text = "Address or IP of the host player to connect to. -Not used when you're hosting." -placeholder_text = "127.0.0.1" -secret = true - -[node name="ShowHide" type="Button" parent="Host"] -layout_mode = 2 -tooltip_text = "Show / Hide Address" -toggle_mode = true -icon = ExtResource("2_1u5pu") -flat = true - -[node name="Port" type="SpinBox" parent="Host"] -layout_mode = 2 -tooltip_text = "Port to connect to / listen on. -Host must forward this port." -min_value = 1024.0 -max_value = 65000.0 -value = 52410.0 -alignment = 2 - -[node name="Buttons" type="HBoxContainer" parent="."] -layout_mode = 2 - -[node name="Join" type="Button" parent="Buttons"] -layout_mode = 2 -size_flags_horizontal = 3 -tooltip_text = "Join an existing session." -text = "Join" - -[node name="Host" type="Button" parent="Buttons"] -layout_mode = 2 -size_flags_horizontal = 3 -tooltip_text = "Open a new session, allowing -users to connect to your public IP." -text = "Host" - -[node name="Disconnect" type="Button" parent="Buttons"] -layout_mode = 2 -size_flags_horizontal = 3 -tooltip_text = "Disconnect from / close the current session." -disabled = true -text = "Disconnect -" - -[connection signal="pressed" from="Cache/Button" to="." method="_on_cache_dir_dialog_pressed"] -[connection signal="toggled" from="Host/ShowHide" to="." method="_on_show_hide_address_toggled"] diff --git a/Scenes/copy_multiplayer_settings.gd b/Scenes/copy_multiplayer_settings.gd new file mode 100644 index 0000000..4c00abe --- /dev/null +++ b/Scenes/copy_multiplayer_settings.gd @@ -0,0 +1,24 @@ +class_name copyMultiplayerSettings +extends TabContainer + +@export var visible_icon: Texture2D +@export var hidden_icon: Texture2D + +func is_tab_selected(tab_name: String) -> bool: + return get_current_tab_control().name == tab_name + +func _on_cache_dir_dialog_pressed() -> void: + var widget: LineEdit = $"Settings/VBoxContainer/Cache/LineEdit" + + var dialog: FileDialog = $"Settings/VBoxContainer/Cache/FileDialog" + dialog.size = get_window().size / 2 + dialog.position = get_window().size / 4 + dialog.current_dir = widget.text + dialog.popup() + + widget.text = await dialog.dir_selected + widget.text_changed.emit(widget.text) + +func _on_show_hide_address_toggled(toggled_on: bool) -> void: + $"Connect/VBoxContainer/Host/Address".secret = !toggled_on + $"Connect/VBoxContainer/Host/ShowHide".icon = visible_icon if toggled_on else hidden_icon diff --git a/Scenes/copy_multiplayer_settings.tscn b/Scenes/copy_multiplayer_settings.tscn new file mode 100644 index 0000000..5c53bf2 --- /dev/null +++ b/Scenes/copy_multiplayer_settings.tscn @@ -0,0 +1,159 @@ +[gd_scene load_steps=4 format=3 uid="uid://cdxnwsgmevndb"] + +[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/copy_multiplayer_settings.gd" id="1_7d55i"] +[ext_resource type="Texture2D" uid="uid://qbho5oyu1kfa" path="res://Mods/copyMultiplayer/Resources/hidden.png" id="2_1u5pu"] +[ext_resource type="Texture2D" uid="uid://dcmljlb2v6p16" path="res://Mods/copyMultiplayer/Resources/visible.png" id="2_ibe7i"] + +[node name="copyMultiplayerSettings" type="TabContainer"] +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 +size_flags_horizontal = 3 +size_flags_vertical = 3 +current_tab = 2 +script = ExtResource("1_7d55i") +visible_icon = ExtResource("2_ibe7i") +hidden_icon = ExtResource("2_1u5pu") + +[node name="Connect" type="MarginContainer" parent="."] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 +metadata/_tab_index = 0 + +[node name="VBoxContainer" type="VBoxContainer" parent="Connect"] +layout_mode = 2 + +[node name="Join" type="HBoxContainer" parent="Connect/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Connect/VBoxContainer/Join"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Address:" +horizontal_alignment = 2 + +[node name="Address" type="LineEdit" parent="Connect/VBoxContainer/Join"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Address or IP of the host player to connect to. +Not used when you're hosting." +placeholder_text = "127.0.0.1" +secret = true + +[node name="ShowHide" type="Button" parent="Connect/VBoxContainer/Join"] +layout_mode = 2 +tooltip_text = "Show / Hide Address" +toggle_mode = true +icon = ExtResource("2_1u5pu") +flat = true + +[node name="Button" type="Button" parent="Connect/VBoxContainer/Join"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +tooltip_text = "Join an existing session." +text = "Join" + +[node name="Host" type="HBoxContainer" parent="Connect/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Connect/VBoxContainer/Host"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Port:" +horizontal_alignment = 2 + +[node name="Port" type="SpinBox" parent="Connect/VBoxContainer/Host"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Port to connect to / listen on. +Host must forward this port." +min_value = 1024.0 +max_value = 65000.0 +value = 52410.0 +alignment = 2 + +[node name="Button" type="Button" parent="Connect/VBoxContainer/Host"] +custom_minimum_size = Vector2(120, 0) +layout_mode = 2 +tooltip_text = "Open a new session, allowing +users to connect to your public IP." +text = "Host" + +[node name="Disconnect" type="Button" parent="Connect/VBoxContainer"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "Disconnect from / close the current session." +disabled = true +text = "Disconnect +" + +[node name="Settings" type="MarginContainer" parent="."] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 +metadata/_tab_index = 1 + +[node name="VBoxContainer" type="VBoxContainer" parent="Settings"] +layout_mode = 2 + +[node name="Cache" type="HBoxContainer" parent="Settings/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Settings/VBoxContainer/Cache"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Cache:" +horizontal_alignment = 2 + +[node name="LineEdit" type="LineEdit" parent="Settings/VBoxContainer/Cache"] +layout_mode = 2 +size_flags_horizontal = 3 +tooltip_text = "The directory other players' models will be loaded from. +Filenames must match exactly for this to work." + +[node name="Button" type="Button" parent="Settings/VBoxContainer/Cache"] +layout_mode = 2 +tooltip_text = "Browse Directory" +text = " ... " + +[node name="FileDialog" type="FileDialog" parent="Settings/VBoxContainer/Cache"] +title = "Open a Directory" +ok_button_text = "Select Current Folder" +file_mode = 2 +access = 2 + +[node name="Name" type="HBoxContainer" parent="Settings/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="Settings/VBoxContainer/Name"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Name:" +horizontal_alignment = 2 + +[node name="LineEdit" type="LineEdit" parent="Settings/VBoxContainer/Name"] +layout_mode = 2 +size_flags_horizontal = 3 + +[node name="Players" type="MarginContainer" parent="."] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 +metadata/_tab_index = 2 + +[node name="VBoxContainer" type="VBoxContainer" parent="Players"] +layout_mode = 2 + +[connection signal="toggled" from="Connect/VBoxContainer/Join/ShowHide" to="." method="_on_show_hide_address_toggled"] +[connection signal="pressed" from="Settings/VBoxContainer/Cache/Button" to="." method="_on_cache_dir_dialog_pressed"] diff --git a/Scenes/player_settings.gd b/Scenes/player_settings.gd new file mode 100644 index 0000000..7425f92 --- /dev/null +++ b/Scenes/player_settings.gd @@ -0,0 +1,41 @@ +class_name PlayerSettings +extends Container + +signal value_changed(Transform3D) + +@export var offset_x: SpinBox +@export var offset_y: SpinBox +@export var offset_z: SpinBox + +@export var rotation_x: SpinBox +@export var rotation_y: SpinBox +@export var rotation_z: SpinBox + +@export var scale_xyz: SpinBox + +func set_nickname(value: String) -> void: + $Nickname.text = value + +func on_transform_changed(value: Transform3D) -> void: + offset_x.value = value.origin.x + offset_y.value = value.origin.y + offset_z.value = value.origin.z + + var rot := value.basis.get_euler() + rotation_x.value = rad_to_deg(rot.x) + rotation_y.value = rad_to_deg(rot.y) + rotation_z.value = rad_to_deg(rot.z) + + scale_xyz.value = value.basis.get_scale().x + +func _on_value_changed(_value: float) -> void: + var origin := Vector3(offset_x.value, offset_y.value, offset_z.value) + var rot := Vector3(rotation_x.value, rotation_y.value, rotation_z.value) / 360 * TAU + var basis := Basis.from_euler(rot) * Basis.from_scale(Vector3.ONE * scale_xyz.value) + value_changed.emit(Transform3D(basis, origin)) + + # Ensure that rotation inputs are always in 0-359 range. + rotation_x.set_value_no_signal(fposmod(rotation_x.value, 360)) + rotation_y.set_value_no_signal(fposmod(rotation_y.value, 360)) + rotation_z.set_value_no_signal(fposmod(rotation_z.value, 360)) + diff --git a/Scenes/player_settings.tscn b/Scenes/player_settings.tscn new file mode 100644 index 0000000..317725c --- /dev/null +++ b/Scenes/player_settings.tscn @@ -0,0 +1,179 @@ +[gd_scene load_steps=2 format=3 uid="uid://fbfasiqs3d88"] + +[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/player_settings.gd" id="1_ybttq"] + +[node name="PlayerSettings" type="VBoxContainer" node_paths=PackedStringArray("offset_x", "offset_y", "offset_z", "rotation_x", "rotation_y", "rotation_z", "scale_xyz")] +script = ExtResource("1_ybttq") +offset_x = NodePath("PanelContainer/MarginContainer/VBoxContainer/Offset/X") +offset_y = NodePath("PanelContainer/MarginContainer/VBoxContainer/Offset/Y") +offset_z = NodePath("PanelContainer/MarginContainer/VBoxContainer/Offset/Z") +rotation_x = NodePath("PanelContainer/MarginContainer/VBoxContainer/Rotation/X") +rotation_y = NodePath("PanelContainer/MarginContainer/VBoxContainer/Rotation/Y") +rotation_z = NodePath("PanelContainer/MarginContainer/VBoxContainer/Rotation/Z") +scale_xyz = NodePath("PanelContainer/MarginContainer/VBoxContainer/Scale/SpinBox") + +[node name="Nickname" type="Label" parent="."] +layout_mode = 2 +text = "Nickname" +horizontal_alignment = 1 + +[node name="PanelContainer" type="PanelContainer" parent="."] +layout_mode = 2 + +[node name="MarginContainer" type="MarginContainer" parent="PanelContainer"] +layout_mode = 2 +theme_override_constants/margin_left = 4 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 4 +theme_override_constants/margin_bottom = 4 + +[node name="VBoxContainer" type="VBoxContainer" parent="PanelContainer/MarginContainer"] +layout_mode = 2 + +[node name="Offset" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Offset:" +horizontal_alignment = 2 + +[node name="LabelX" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.917647, 0.211765, 0.317647, 1) +text = "X" +horizontal_alignment = 2 + +[node name="X" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = -100.0 +step = 0.01 +alignment = 2 +custom_arrow_step = 0.05 + +[node name="LabelY" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.529412, 0.839216, 0.0117647, 1) +text = "Y" +horizontal_alignment = 2 + +[node name="Y" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = -100.0 +step = 0.01 +alignment = 2 +custom_arrow_step = 0.05 + +[node name="LabelZ" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.180392, 0.529412, 0.917647, 1) +text = "Z" +horizontal_alignment = 2 + +[node name="Z" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Offset"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = -100.0 +step = 0.01 +alignment = 2 +custom_arrow_step = 0.05 + +[node name="Rotation" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Rotation:" +horizontal_alignment = 2 + +[node name="LabelX" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.917647, 0.211765, 0.317647, 1) +text = "X" +horizontal_alignment = 2 + +[node name="X" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +layout_mode = 2 +size_flags_horizontal = 3 +max_value = 359.0 +step = 0.1 +allow_greater = true +allow_lesser = true +alignment = 2 +custom_arrow_step = 2.0 + +[node name="LabelY" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.529412, 0.839216, 0.0117647, 1) +text = "Y" +horizontal_alignment = 2 + +[node name="Y" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +layout_mode = 2 +size_flags_horizontal = 3 +max_value = 359.0 +step = 0.1 +allow_greater = true +allow_lesser = true +alignment = 2 +custom_arrow_step = 2.0 + +[node name="LabelZ" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.180392, 0.529412, 0.917647, 1) +text = "Z" +horizontal_alignment = 2 + +[node name="Z" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Rotation"] +layout_mode = 2 +size_flags_horizontal = 3 +max_value = 359.0 +step = 0.1 +allow_greater = true +allow_lesser = true +alignment = 2 +custom_arrow_step = 2.0 + +[node name="Scale" type="HBoxContainer" parent="PanelContainer/MarginContainer/VBoxContainer"] +layout_mode = 2 + +[node name="Label" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"] +custom_minimum_size = Vector2(80, 0) +layout_mode = 2 +text = "Scale:" +horizontal_alignment = 2 + +[node name="Padding" type="Label" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"] +custom_minimum_size = Vector2(20, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.917647, 0.211765, 0.317647, 1) +text = " +" +horizontal_alignment = 2 + +[node name="SpinBox" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"] +layout_mode = 2 +size_flags_horizontal = 3 +min_value = 0.5 +max_value = 2.0 +step = 0.01 +alignment = 2 +custom_arrow_step = 0.05 + +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Offset/X" to="." method="_on_value_changed"] +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Offset/Y" to="." method="_on_value_changed"] +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Offset/Z" to="." method="_on_value_changed"] +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Rotation/X" to="." method="_on_value_changed"] +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Rotation/Y" to="." method="_on_value_changed"] +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Rotation/Z" to="." method="_on_value_changed"] +[connection signal="value_changed" from="PanelContainer/MarginContainer/VBoxContainer/Scale/SpinBox" to="." method="_on_value_changed"] diff --git a/sync_controller.gd b/Scenes/sync_controller.gd similarity index 63% rename from sync_controller.gd rename to Scenes/sync_controller.gd index a49350d..4eba397 100644 --- a/sync_controller.gd +++ b/Scenes/sync_controller.gd @@ -1,9 +1,11 @@ class_name SyncController -extends Node +extends Area3D +@export var shape: CollisionShape3D + +var peer_id: int var module: copyMultiplayer var model_controller: ModelController -var peer_id: int var nickname: String @@ -16,14 +18,27 @@ var model : Node var skeleton : Skeleton3D var anim_player : AnimationPlayer var anim_root : Node +var settings : PlayerSettings + +var is_dragging := false +var drag_current: Vector3 # Reusable buffer to write data for synchronizing models. static var write_stream: StreamBuffer = StreamBuffer.with_capacity(2048) func _ready() -> void: - module = get_parent().get_parent() - model_controller = get_parent() - peer_id = model_controller.name.to_int() + peer_id = name.to_int() + module = get_parent() + model_controller = ModelController.new() + model_controller.name = "ModelController" + add_child(model_controller) + +func _process(_delta: float) -> void: + update_collision_shape_position() + handle_dragging() + +func _exit_tree() -> void: + if settings: settings.queue_free() func get_display_name() -> String: if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ] @@ -32,9 +47,16 @@ func get_display_name() -> String: func change_nickname(new_nickname: String) -> void: new_nickname = new_nickname.strip_edges() if new_nickname == "": return # Ignore empty nicknames. + if new_nickname == nickname: return # Ignore unchanged nicknames. module.print_log("%s is now known as '%s'" % [ get_display_name(), new_nickname ]) nickname = new_nickname + if !settings: + settings = module.new_player_settings() + settings.on_transform_changed(transform) + settings.value_changed.connect(func(value): transform = value) + settings.set_nickname(nickname) + ## Attempts to change the model of this player. func change_model( new_version: int, @@ -65,6 +87,8 @@ func change_model( anim_player = model.find_child("AnimationPlayer", false, false) anim_root = anim_player.get_node(anim_player.root_node) + recalculate_collision_shape() + module.print_log("%s switched to '%s'" % [ get_display_name(), filename ]) func sync_model_animation( @@ -158,3 +182,77 @@ static func send_model_animation(module: copyMultiplayer) -> void: # module.set_status("Packet size: %d bytes (%d uncompressed)" % [ compressed_buffer.size(), write_stream.size ]) module.sync_model_animation.rpc(module.version, write_stream.size, compressed_buffer) write_stream.clear() + + +# Updates the collision shapes used for allowing the player model to be +# repositioned based on the current model's skeleton. This functionality +# is only enabled when the "Players" tab is selected. +func recalculate_collision_shape() -> void: + var head_y := _get_bone_position("Head").y + var left_foot_y := _get_bone_position("LeftFoot").y + var right_foot_y := _get_bone_position("RightFoot").y + var avg_foot_y := (left_foot_y + right_foot_y) / 2 + var shoulder_x := _get_bone_position("LeftUpperArm").x + + var capsule := CapsuleShape3D.new() + capsule.height = (head_y - avg_foot_y) * 1.5 + capsule.radius = shoulder_x * 1.75 + shape.shape = capsule + +func update_collision_shape_position() -> void: + if not skeleton: return + var head := _get_bone_position("Head") + var left_foot := _get_bone_position("LeftFoot") + var right_foot := _get_bone_position("RightFoot") + var avg_foot := (left_foot + right_foot) / 2 + shape.global_position = skeleton.global_position + (head + avg_foot) / 2 + +func _get_bone_position(bone_name: String) -> Vector3: + var idx := skeleton.find_bone(bone_name) + var pose := skeleton.get_bone_global_pose(idx) + return pose.origin + + +func _input_event(_camera: Node, event: InputEvent, + pos: Vector3, _normal: Vector3, _idx: int) -> void: + if (module.can_move_players + && event is InputEventMouseButton + && event.button_index == MOUSE_BUTTON_LEFT + && event.pressed): + is_dragging = true + drag_current = pos + +func _unhandled_input(event: InputEvent) -> void: + if (is_dragging + && event is InputEventMouseButton + && event.button_index == MOUSE_BUTTON_LEFT + && not event.pressed): + is_dragging = false + get_viewport().set_input_as_handled() + +func handle_dragging() -> void: + if not is_dragging: return + + var camera := get_viewport().get_camera_3d() + var mouse := get_viewport().get_mouse_position() + var origin := camera.project_ray_origin(mouse) + var dir := camera.project_ray_normal(mouse) + + if dir.z == 0: return + var distance := (drag_current.z - origin.z) / dir.z + var target := origin + dir * distance + + position += target - drag_current + drag_current = target + + sortof_face_the_camera() + settings.on_transform_changed(transform) + +# FIXME: Kind of a hack, find a better way. +## Rotates the model a little bit towards the camera +## so it doesn't appear to be staring into nowhere. +func sortof_face_the_camera() -> void: + var camera := get_viewport().get_camera_3d() + var from_2d := Vector2(global_position.x, global_position.z) + var to_2d := Vector2(camera.global_position.x, camera.global_position.z) + rotation.y = -from_2d.angle_to_point(to_2d) + TAU/4 diff --git a/Scenes/sync_controller.tscn b/Scenes/sync_controller.tscn new file mode 100644 index 0000000..72a8e9c --- /dev/null +++ b/Scenes/sync_controller.tscn @@ -0,0 +1,9 @@ +[gd_scene load_steps=2 format=3 uid="uid://b8t8fgu7ht8rn"] + +[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/sync_controller.gd" id="1_v4dbb"] + +[node name="SyncController" type="Area3D" node_paths=PackedStringArray("shape")] +script = ExtResource("1_v4dbb") +shape = NodePath("CollisionShape3D") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="."] diff --git a/stream_buffer.gd b/Utility/stream_buffer.gd similarity index 100% rename from stream_buffer.gd rename to Utility/stream_buffer.gd diff --git a/copyMultiplayer.gd b/copyMultiplayer.gd index 25e235d..e473881 100644 --- a/copyMultiplayer.gd +++ b/copyMultiplayer.gd @@ -15,12 +15,10 @@ var version: int = -1 var bone_lookup: Array[String] = [] var blendshape_lookup: Array[NodePath] = [] -# Temporary positioning system. -# TODO: Add a setting that allows syncing model positions, as an alternative -# to letting the local player choose where each model is going to appear. -var player_order: Array[int] = [] -var starting_offset := Vector3(-1.25, 0.0, 0.0) -var accumulative_offset := Vector3( 0.35, 0.0, 0.0) +# TODO: Remember the position of each remote player by name. +## Whether the position of remote players can be ajusted +## by clicking their controller's collider and dragging. +var can_move_players := false # FIXME: There is an edge case where you can load the settings while connected, # resulting in out-of-sync information with connected players, but so far @@ -49,14 +47,16 @@ func _ready() -> void: for phalange in [ "Proximal", "Intermediate", "Distal" ]: tracked_bones.append("%s%s%s" % [ side, finger, phalange ]) - setup_setting_widget("cache" , "Cache/LineEdit", true ) - setup_setting_widget("nickname", "Name/LineEdit" , false) - setup_setting_widget("address" , "Host/Address" , true ) - setup_setting_widget("port" , "Host/Port" , true ) + setup_setting_widget("address" , "Connect/VBoxContainer/Join/Address" , true ) + setup_setting_widget("port" , "Connect/VBoxContainer/Host/Port" , true ) + + setup_setting_widget("cache" , "Settings/VBoxContainer/Cache/LineEdit", true ) + setup_setting_widget("nickname", "Settings/VBoxContainer/Name/LineEdit" , false) + setup_button_connections() # Filter whitespace characters from nickname before saving it to "nickname" field. - var nickname_widget: LineEdit = get_settings_window().get_node("Name/LineEdit") + var nickname_widget: LineEdit = get_settings_window().get_node("Settings/VBoxContainer/Name/LineEdit") nickname_widget.text_changed.connect(func(new_text): modify_setting("nickname", new_text.strip_edges())) nickname_widget.text_submitted.connect(func(_new_text): nickname_widget.text = nickname) nickname_widget.focus_exited.connect(func(): nickname_widget.text = nickname) @@ -71,7 +71,7 @@ func _exit_tree() -> void: multiplayer.multiplayer_peer.close() func _create_settings_window() -> Control: - return load("res://Mods/copyMultiplayer/Resources/copy_multiplayer_settings.tscn").instantiate() + return load("res://Mods/copyMultiplayer/Scenes/copy_multiplayer_settings.tscn").instantiate() func setup_setting_widget(setting_name: String, path: NodePath, setup_events: bool) -> void: var settings = get_settings_window() @@ -88,13 +88,20 @@ func setup_setting_widget(setting_name: String, path: NodePath, setup_events: bo func setup_button_connections() -> void: var window = get_settings_window() - window.get_node("Buttons/Join").pressed.connect(on_join_pressed) - window.get_node("Buttons/Host").pressed.connect(on_host_pressed) - window.get_node("Buttons/Disconnect").pressed.connect(on_disconnect_pressed) + window.get_node("Connect/VBoxContainer/Join/Button").pressed.connect(on_join_pressed) + window.get_node("Connect/VBoxContainer/Host/Button").pressed.connect(on_host_pressed) + window.get_node("Connect/VBoxContainer/Disconnect" ).pressed.connect(on_disconnect_pressed) + +var player_settings_scene: PackedScene = load("res://Mods/copyMultiplayer/Scenes/player_settings.tscn") +func new_player_settings() -> PlayerSettings: + var container = get_settings_window().get_node("Players/VBoxContainer") + var result := player_settings_scene.instantiate() + container.add_child(result) + return result func on_join_pressed() -> void: - var address_widget: LineEdit = get_settings_window().get_node("Host/Address") + var address_widget: LineEdit = get_settings_window().get_node("Connect/VBoxContainer/Join/Address") var default_address: String = address_widget.placeholder_text var actual_address := default_address if address.is_empty() else address @@ -127,16 +134,11 @@ func on_disconnect_pressed() -> void: multiplayer.multiplayer_peer.close() +var sync_controller_scene: PackedScene = load("res://Mods/copyMultiplayer/Scenes/sync_controller.tscn"); func on_peer_connected(id: int) -> void: - var model_controller := ModelController.new() - model_controller.name = str(id) - var sync_controller := SyncController.new() - sync_controller.name = "SyncController" - model_controller.add_child(sync_controller) - add_child(model_controller) - - player_order.append(id) - update_model_transforms() + var controller := sync_controller_scene.instantiate() + controller.name = str(id) + add_child(controller) # Send information to the newly connected player about ourselves. # (Technically this doesn't need to be relayed through the server, but oh well.) @@ -145,17 +147,14 @@ func on_peer_connected(id: int) -> void: if filename.is_valid_filename(): change_model.rpc_id(id, version, bone_lookup, blendshape_lookup, filename) update_status() - print_log("%s connected" % sync_controller.get_display_name()) + print_log("%s connected" % controller.get_display_name()) func on_peer_disconnected(id: int) -> void: var controller = get_sync_controller(id) if not controller: return - remove_child(controller.model_controller) - controller.model_controller.queue_free() - - player_order.remove_at(player_order.find(id)) - update_model_transforms() + remove_child(controller) + controller.queue_free() update_status() print_log("Player %s disconnected" % id) @@ -177,12 +176,12 @@ func on_server_disconnected() -> void: func update_enabled_state(is_online: bool) -> void: var window = get_settings_window() - window.get_node("Name/LineEdit").editable = !is_online - window.get_node("Host/Address" ).editable = !is_online - window.get_node("Host/Port" ).editable = !is_online - window.get_node("Buttons/Join" ).disabled = is_online - window.get_node("Buttons/Host" ).disabled = is_online - window.get_node("Buttons/Disconnect").disabled = !is_online + window.get_node("Settings/VBoxContainer/Name/LineEdit").editable = !is_online + window.get_node("Connect/VBoxContainer/Join/Address" ).editable = !is_online + window.get_node("Connect/VBoxContainer/Join/Button" ).disabled = is_online + window.get_node("Connect/VBoxContainer/Host/Port" ).editable = !is_online + window.get_node("Connect/VBoxContainer/Host/Button" ).disabled = is_online + window.get_node("Connect/VBoxContainer/Disconnect" ).disabled = !is_online func update_status() -> void: var num_players := 1 + multiplayer.get_peers().size() @@ -191,34 +190,19 @@ func update_status() -> void: set_status("%s: %d player%s" % [side, num_players, s]) -# FIXME: Temporary hardcoded way to assign some offset to other players. -func update_model_transforms() -> void: - var offset := starting_offset - for id in player_order: - var controller: ModelController = get_node(str(id)) - controller.position = offset - offset += accumulative_offset - update_model_rotation(controller) - -## Rotates the model a little bit towards the camera -## so it doesn't appear to be staring into nowhere. -func update_model_rotation(controller: ModelController) -> void: - var camera := get_viewport().get_camera_3d() - var from_2d := Vector2(controller.position.x, controller.position.z) - var to_2d := Vector2(camera.position.x, camera.position.z) - controller.rotation.y = from_2d.angle_to_point(to_2d) / 4 # Magic value, probably depends on FOV. - ## Removes all networked player models. func clear_player_models() -> void: for controller in get_children(): - if controller is ModelController: + if controller is SyncController: remove_child(controller) controller.queue_free() - player_order.clear() func _process(_delta: float) -> void: SyncController.send_model_animation(self) + var settings: copyMultiplayerSettings = get_settings_window() + # TODO: Add a setting to allow moving players even if "Players" tab is not active. + can_move_players = settings.is_visible_in_tree() && settings.is_tab_selected("Players") # Called when module is initialized or load_vrm is called. func scene_init() -> void: @@ -254,9 +238,7 @@ func scene_init() -> void: ## Gets the SyncController for the player with the specified peer id. func get_sync_controller(peer_id: int) -> SyncController: - var model_controller := get_node_or_null(str(peer_id)) as ModelController - if not model_controller: return null - return model_controller.get_node_or_null("SyncController") as SyncController + return get_node_or_null(str(peer_id)) as SyncController @rpc("any_peer", "reliable") func change_nickname(new_nickname: String) -> void: