From 2290e058b6e38d58f5668efc758c2c5e38518f4c Mon Sep 17 00:00:00 2001 From: copygirl Date: Sat, 1 Mar 2025 10:48:07 +0100 Subject: [PATCH] Add player stats tab --- Scenes/copy_multiplayer_settings.tscn | 49 ++++++++++++++++++++- Scenes/player_settings.tscn | 10 ++--- Scenes/player_stats.gd | 33 +++++++++++++++ Scenes/player_stats.tscn | 32 ++++++++++++++ Scenes/sync_controller.gd | 31 ++++++++++++-- copyMultiplayer.gd | 61 +++++++++++++++++++++++++-- 6 files changed, 203 insertions(+), 13 deletions(-) create mode 100644 Scenes/player_stats.gd create mode 100644 Scenes/player_stats.tscn diff --git a/Scenes/copy_multiplayer_settings.tscn b/Scenes/copy_multiplayer_settings.tscn index 112d39b..0c009c8 100644 --- a/Scenes/copy_multiplayer_settings.tscn +++ b/Scenes/copy_multiplayer_settings.tscn @@ -4,7 +4,7 @@ [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" node_paths=PackedStringArray("connect_address", "connect_reveal", "connect_port", "connect_join", "connect_host", "connect_disconnect", "settings_cache", "settings_file_dialog", "settings_nickname", "stats_ping", "stats_average", "stats_maximum")] +[node name="copyMultiplayerSettings" type="TabContainer" node_paths=PackedStringArray("connect_address", "connect_reveal", "connect_port", "connect_join", "connect_host", "connect_disconnect", "settings_cache", "settings_file_dialog", "settings_nickname")] anchors_preset = 15 anchor_right = 1.0 anchor_bottom = 1.0 @@ -164,5 +164,52 @@ metadata/_tab_index = 2 [node name="VBoxContainer" type="VBoxContainer" parent="Players"] layout_mode = 2 +[node name="Stats" type="MarginContainer" parent="."] +visible = false +layout_mode = 2 +theme_override_constants/margin_left = 8 +theme_override_constants/margin_top = 4 +theme_override_constants/margin_right = 8 +theme_override_constants/margin_bottom = 4 +metadata/_tab_index = 3 + +[node name="VBoxContainer" type="VBoxContainer" parent="Stats"] +layout_mode = 2 + +[node name="Header" type="HBoxContainer" parent="Stats/VBoxContainer"] +layout_mode = 2 + +[node name="Player" type="Label" parent="Stats/VBoxContainer/Header"] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +theme_override_colors/font_color = Color(0.75, 0.75, 0.75, 1) +text = "Player" +horizontal_alignment = 1 + +[node name="Ping" type="Label" parent="Stats/VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.75, 0.75, 0.75, 1) +text = "Ping" +horizontal_alignment = 1 + +[node name="Average" type="Label" parent="Stats/VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.75, 0.75, 0.75, 1) +text = "Avg." +horizontal_alignment = 1 + +[node name="Maximum" type="Label" parent="Stats/VBoxContainer/Header"] +layout_mode = 2 +size_flags_horizontal = 3 +theme_override_colors/font_color = Color(0.75, 0.75, 0.75, 1) +text = "Max." +horizontal_alignment = 1 + +[node name="HSeparator" type="HSeparator" parent="Stats/VBoxContainer"] +layout_mode = 2 +theme_override_constants/separation = 1 + [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.tscn b/Scenes/player_settings.tscn index e7bed44..cf2c09d 100644 --- a/Scenes/player_settings.tscn +++ b/Scenes/player_settings.tscn @@ -104,7 +104,7 @@ horizontal_alignment = 2 layout_mode = 2 size_flags_horizontal = 3 max_value = 359.0 -step = 0.1 +step = 0.01 allow_greater = true allow_lesser = true alignment = 2 @@ -121,7 +121,7 @@ horizontal_alignment = 2 layout_mode = 2 size_flags_horizontal = 3 max_value = 359.0 -step = 0.1 +step = 0.01 allow_greater = true allow_lesser = true alignment = 2 @@ -138,7 +138,7 @@ horizontal_alignment = 2 layout_mode = 2 size_flags_horizontal = 3 max_value = 359.0 -step = 0.1 +step = 0.01 allow_greater = true allow_lesser = true alignment = 2 @@ -164,8 +164,8 @@ 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 +min_value = 0.1 +max_value = 10.0 step = 0.01 value = 1.0 alignment = 2 diff --git a/Scenes/player_stats.gd b/Scenes/player_stats.gd new file mode 100644 index 0000000..524c5e0 --- /dev/null +++ b/Scenes/player_stats.gd @@ -0,0 +1,33 @@ +class_name PlayerStats +extends Container + +var history: Array[Dictionary] = [] +var ping := 0 + +func set_nickname(value: String) -> void: + $Nickname.text = value + +func set_ping(value: int) -> void: + $Ping.text = str(value) + " ms" + ping = value + +# Called when model animation is sent / received. +func push(buffer_size: int) -> void: + history.append({ + time = Time.get_ticks_msec(), + size = buffer_size, + }) + +func _process(_delta: float) -> void: + var now := Time.get_ticks_msec() + while history.size() > 0 && now > history[0].time + 1000: + history.pop_front() + + var total := 0 + var maximum := 0 + for entry in history: + total += entry.size + maximum = maxi(maximum, entry.size) + + $Average.text = "%.1f kB/s" % (float(total) / 1000) + $Maximum.text = "%d bytes" % maximum diff --git a/Scenes/player_stats.tscn b/Scenes/player_stats.tscn new file mode 100644 index 0000000..cdc7fc3 --- /dev/null +++ b/Scenes/player_stats.tscn @@ -0,0 +1,32 @@ +[gd_scene load_steps=2 format=3 uid="uid://dlodft3egwy0p"] + +[ext_resource type="Script" path="res://Mods/copyMultiplayer/Scenes/player_stats.gd" id="1_rd8lb"] + +[node name="Stats" type="HBoxContainer"] +script = ExtResource("1_rd8lb") + +[node name="Nickname" type="Label" parent="."] +custom_minimum_size = Vector2(100, 0) +layout_mode = 2 +text = "Nickname" +horizontal_alignment = 1 +clip_text = true +text_overrun_behavior = 3 + +[node name="Ping" type="Label" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +text = "0 ms" +horizontal_alignment = 2 + +[node name="Average" type="Label" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +text = "0 kB/s" +horizontal_alignment = 2 + +[node name="Maximum" type="Label" parent="."] +layout_mode = 2 +size_flags_horizontal = 3 +text = "0 B" +horizontal_alignment = 2 diff --git a/Scenes/sync_controller.gd b/Scenes/sync_controller.gd index cf26b5b..bca3de9 100644 --- a/Scenes/sync_controller.gd +++ b/Scenes/sync_controller.gd @@ -18,7 +18,10 @@ var model : Node var skeleton : Skeleton3D var anim_player : AnimationPlayer var anim_root : Node -var settings : PlayerSettings + +var settings : PlayerSettings +var stats : PlayerStats +var pings : Dictionary var is_dragging := false var drag_current: Vector3 @@ -34,16 +37,27 @@ func _ready() -> void: add_child(model_controller) func _process(_delta: float) -> void: + update_ping_stat() update_collision_shape_position() handle_dragging() func _exit_tree() -> void: if settings: settings.queue_free() + if stats: stats.queue_free() + func get_display_name() -> String: if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ] else: return "Player (%d)" % peer_id +func update_ping_stat() -> void: + if not stats: return + var peer := multiplayer.multiplayer_peer.get_peer(peer_id) as ENetMultiplayerPeer + if not peer: return + var ping := int(peer.get_statistic(ENetPacketPeer.PEER_LAST_ROUND_TRIP_TIME)) + stats.set_ping(ping) + + func change_nickname(new_nickname: String) -> void: new_nickname = new_nickname.strip_edges() if new_nickname == "": return # Ignore empty nicknames. @@ -57,6 +71,10 @@ func change_nickname(new_nickname: String) -> void: settings.value_changed.connect(func(value): transform = value) settings.set_nickname(nickname) + if !stats: + stats = module.new_player_stats() + stats.set_nickname(nickname) + ## Attempts to change the model of this player. func change_model( new_version: int, @@ -91,6 +109,9 @@ func change_model( module.print_log("%s switched to '%s'" % [ get_display_name(), filename ]) +# TODO: Buffer and process 1 frame later if receiving 2 per frame or similar. +# Otherwise, we might receive 2 in a single frame and then 0 in the next, +# essentially halfing our framerate for double the bandwidth. func sync_model_animation( current_version: int, uncompressed_length: int, @@ -98,6 +119,7 @@ func sync_model_animation( ) -> void: if version != current_version: return if (not model) or (not skeleton) or (not anim_root): return + if stats: stats.push(buffer.size()) var uncompressed_buffer := buffer.decompress(uncompressed_length, FileAccess.COMPRESSION_ZSTD); var stream := StreamBuffer.from_buffer(uncompressed_buffer) @@ -181,6 +203,7 @@ static func send_model_animation(module: copyMultiplayer) -> void: # Uncomment this to see packet size. Can we hit < 256 bytes? # 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) + module.main_stats.push(compressed_buffer.size()) write_stream.clear() @@ -214,8 +237,10 @@ func _get_bone_position(bone_name: String) -> Vector3: return pose.origin -func _input_event(_camera: Node, event: InputEvent, - pos: Vector3, _normal: Vector3, _idx: int) -> void: +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 diff --git a/copyMultiplayer.gd b/copyMultiplayer.gd index b4b51fb..8e8a12f 100644 --- a/copyMultiplayer.gd +++ b/copyMultiplayer.gd @@ -7,6 +7,8 @@ extends Mod_Base @export var port := 52410 var main_controller: ModelController +var main_stats : PlayerStats +var ping_update_timer: Timer ## Hardcoded list of bone names that will get syncronized. var tracked_bones: Array[String] @@ -27,6 +29,7 @@ var can_move_players := false func _ready() -> void: # FIXME: Hardcoded way to get the main model controller. main_controller = $"/root/SnekStudio_Main/ModelController" + main_stats = new_player_stats() # FIXME: This is just thrown together. Dunno if this is an accurate list. # TODO: Allow specifying additional bones, with the help of a hierachical list of existing bones in the model. @@ -57,7 +60,7 @@ func _ready() -> void: # Filter whitespace characters from nickname before saving it to "nickname" field. var nickname_widget := get_settings_window().settings_nickname 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.text_submitted.connect(func(_new_text): nickname_widget.text = nickname; main_stats.set_nickname(nickname)) nickname_widget.focus_exited.connect(func(): nickname_widget.text = nickname) multiplayer.peer_connected .connect(on_peer_connected) @@ -99,6 +102,10 @@ func setup_button_connections() -> void: settings.connect_host.pressed.connect(on_host_pressed) settings.connect_disconnect.pressed.connect(on_disconnect_pressed) +func load_after(_old : Dictionary, _new : Dictionary) -> void: + main_stats.set_nickname(nickname) + + 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") @@ -106,6 +113,13 @@ func new_player_settings() -> PlayerSettings: container.add_child(result) return result +var player_stats_scene: PackedScene = load("res://Mods/copyMultiplayer/Scenes/player_stats.tscn") +func new_player_stats() -> PlayerStats: + var container = get_settings_window().get_node("Stats/VBoxContainer") + var result := player_stats_scene.instantiate() + container.add_child(result) + return result + func on_join_pressed() -> void: var address_widget: LineEdit = get_settings_window().get_node("Connect/VBoxContainer/Join/Address") @@ -128,6 +142,10 @@ func on_host_pressed() -> void: update_status() print_log("Opened server") update_enabled_state(true) + ping_update_timer = Timer.new() + ping_update_timer.timeout.connect(do_update_pings) + add_child(ping_update_timer) + ping_update_timer.start(1) else: print_log("Unable to open server!") @@ -138,6 +156,8 @@ func on_disconnect_pressed() -> void: print_log("Closed server") update_enabled_state(false) clear_player_models() + ping_update_timer.queue_free() + ping_update_timer = null multiplayer.multiplayer_peer.close() @@ -189,6 +209,7 @@ func update_enabled_state(is_online: bool) -> void: settings.connect_join .disabled = is_online settings.connect_host .disabled = is_online settings.connect_disconnect.disabled = !is_online + if not is_online: main_stats.set_ping(0) func update_status() -> void: var num_players := 1 + multiplayer.get_peers().size() @@ -207,9 +228,11 @@ func clear_player_models() -> void: 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") + # FIXME: Temporary fix until my pull request is accepted or at least the above TODO is implemented. + can_move_players = true + # var settings: copyMultiplayerSettings = get_settings_window() + # 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: @@ -243,10 +266,40 @@ func scene_init() -> void: change_model.rpc(version, bone_lookup, blendshape_lookup, filename) -## Gets the SyncController for the player with the specified peer id. func get_sync_controller(peer_id: int) -> SyncController: return get_node_or_null(str(peer_id)) as SyncController +func get_all_sync_controllers() -> Array[SyncController]: + var result: Array[SyncController] = [] + for controller in get_children(): + if controller is SyncController: + result.append(controller) + return result + +func get_player_stats(peer_id: int) -> PlayerStats: + if peer_id == multiplayer.get_unique_id(): return main_stats + var controller := get_sync_controller(peer_id) + if controller: return controller.stats + return null + + +@rpc("authority", "unreliable_ordered", "call_local") +func update_pings(pairs: Array[int]) -> void: + for i in range(0, pairs.size(), 2): + var peer_id := pairs[i] + var ping := pairs[i + 1] + var stats := get_player_stats(peer_id) + if stats: stats.set_ping(ping) + +func do_update_pings() -> void: + var pairs: Array[int] = [] + var server: ENetMultiplayerPeer = multiplayer.multiplayer_peer + for peer_id in multiplayer.get_peers(): + var peer := server.get_peer(peer_id) + var ping := peer.get_statistic(ENetPacketPeer.PEER_ROUND_TRIP_TIME) + pairs.append_array([ peer_id, int(ping) ]) + update_pings.rpc(pairs) + @rpc("any_peer", "reliable") func change_nickname(new_nickname: String) -> void: var peer_id := multiplayer.get_remote_sender_id()