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: