Add player stats tab

main
copygirl 2 weeks ago
parent b8e527f9c8
commit 2290e058b6
  1. 49
      Scenes/copy_multiplayer_settings.tscn
  2. 10
      Scenes/player_settings.tscn
  3. 33
      Scenes/player_stats.gd
  4. 32
      Scenes/player_stats.tscn
  5. 31
      Scenes/sync_controller.gd
  6. 61
      copyMultiplayer.gd

@ -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://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"] [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 anchors_preset = 15
anchor_right = 1.0 anchor_right = 1.0
anchor_bottom = 1.0 anchor_bottom = 1.0
@ -164,5 +164,52 @@ metadata/_tab_index = 2
[node name="VBoxContainer" type="VBoxContainer" parent="Players"] [node name="VBoxContainer" type="VBoxContainer" parent="Players"]
layout_mode = 2 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="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"] [connection signal="pressed" from="Settings/VBoxContainer/Cache/Button" to="." method="_on_cache_dir_dialog_pressed"]

@ -104,7 +104,7 @@ horizontal_alignment = 2
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
max_value = 359.0 max_value = 359.0
step = 0.1 step = 0.01
allow_greater = true allow_greater = true
allow_lesser = true allow_lesser = true
alignment = 2 alignment = 2
@ -121,7 +121,7 @@ horizontal_alignment = 2
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
max_value = 359.0 max_value = 359.0
step = 0.1 step = 0.01
allow_greater = true allow_greater = true
allow_lesser = true allow_lesser = true
alignment = 2 alignment = 2
@ -138,7 +138,7 @@ horizontal_alignment = 2
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
max_value = 359.0 max_value = 359.0
step = 0.1 step = 0.01
allow_greater = true allow_greater = true
allow_lesser = true allow_lesser = true
alignment = 2 alignment = 2
@ -164,8 +164,8 @@ horizontal_alignment = 2
[node name="SpinBox" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"] [node name="SpinBox" type="SpinBox" parent="PanelContainer/MarginContainer/VBoxContainer/Scale"]
layout_mode = 2 layout_mode = 2
size_flags_horizontal = 3 size_flags_horizontal = 3
min_value = 0.5 min_value = 0.1
max_value = 2.0 max_value = 10.0
step = 0.01 step = 0.01
value = 1.0 value = 1.0
alignment = 2 alignment = 2

@ -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

@ -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

@ -18,7 +18,10 @@ var model : Node
var skeleton : Skeleton3D var skeleton : Skeleton3D
var anim_player : AnimationPlayer var anim_player : AnimationPlayer
var anim_root : Node var anim_root : Node
var settings : PlayerSettings
var settings : PlayerSettings
var stats : PlayerStats
var pings : Dictionary
var is_dragging := false var is_dragging := false
var drag_current: Vector3 var drag_current: Vector3
@ -34,16 +37,27 @@ func _ready() -> void:
add_child(model_controller) add_child(model_controller)
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
update_ping_stat()
update_collision_shape_position() update_collision_shape_position()
handle_dragging() handle_dragging()
func _exit_tree() -> void: func _exit_tree() -> void:
if settings: settings.queue_free() if settings: settings.queue_free()
if stats: stats.queue_free()
func get_display_name() -> String: func get_display_name() -> String:
if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ] if nickname: return "Player '%s' (%d)" % [ nickname, peer_id ]
else: return "Player (%d)" % 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: func change_nickname(new_nickname: String) -> void:
new_nickname = new_nickname.strip_edges() new_nickname = new_nickname.strip_edges()
if new_nickname == "": return # Ignore empty nicknames. 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.value_changed.connect(func(value): transform = value)
settings.set_nickname(nickname) settings.set_nickname(nickname)
if !stats:
stats = module.new_player_stats()
stats.set_nickname(nickname)
## Attempts to change the model of this player. ## Attempts to change the model of this player.
func change_model( func change_model(
new_version: int, new_version: int,
@ -91,6 +109,9 @@ func change_model(
module.print_log("%s switched to '%s'" % [ get_display_name(), filename ]) 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( func sync_model_animation(
current_version: int, current_version: int,
uncompressed_length: int, uncompressed_length: int,
@ -98,6 +119,7 @@ func sync_model_animation(
) -> void: ) -> void:
if version != current_version: return if version != current_version: return
if (not model) or (not skeleton) or (not anim_root): 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 uncompressed_buffer := buffer.decompress(uncompressed_length, FileAccess.COMPRESSION_ZSTD);
var stream := StreamBuffer.from_buffer(uncompressed_buffer) 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? # 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.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.sync_model_animation.rpc(module.version, write_stream.size, compressed_buffer)
module.main_stats.push(compressed_buffer.size())
write_stream.clear() write_stream.clear()
@ -214,8 +237,10 @@ func _get_bone_position(bone_name: String) -> Vector3:
return pose.origin return pose.origin
func _input_event(_camera: Node, event: InputEvent, func _input_event(
pos: Vector3, _normal: Vector3, _idx: int) -> void: _camera: Node, event: InputEvent,
pos: Vector3, _normal: Vector3, _idx: int
) -> void:
if (module.can_move_players if (module.can_move_players
&& event is InputEventMouseButton && event is InputEventMouseButton
&& event.button_index == MOUSE_BUTTON_LEFT && event.button_index == MOUSE_BUTTON_LEFT

@ -7,6 +7,8 @@ extends Mod_Base
@export var port := 52410 @export var port := 52410
var main_controller: ModelController var main_controller: ModelController
var main_stats : PlayerStats
var ping_update_timer: Timer
## Hardcoded list of bone names that will get syncronized. ## Hardcoded list of bone names that will get syncronized.
var tracked_bones: Array[String] var tracked_bones: Array[String]
@ -27,6 +29,7 @@ var can_move_players := false
func _ready() -> void: func _ready() -> void:
# FIXME: Hardcoded way to get the main model controller. # FIXME: Hardcoded way to get the main model controller.
main_controller = $"/root/SnekStudio_Main/ModelController" main_controller = $"/root/SnekStudio_Main/ModelController"
main_stats = new_player_stats()
# FIXME: This is just thrown together. Dunno if this is an accurate list. # 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. # 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. # Filter whitespace characters from nickname before saving it to "nickname" field.
var nickname_widget := get_settings_window().settings_nickname 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_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) nickname_widget.focus_exited.connect(func(): nickname_widget.text = nickname)
multiplayer.peer_connected .connect(on_peer_connected) 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_host.pressed.connect(on_host_pressed)
settings.connect_disconnect.pressed.connect(on_disconnect_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") var player_settings_scene: PackedScene = load("res://Mods/copyMultiplayer/Scenes/player_settings.tscn")
func new_player_settings() -> PlayerSettings: func new_player_settings() -> PlayerSettings:
var container = get_settings_window().get_node("Players/VBoxContainer") var container = get_settings_window().get_node("Players/VBoxContainer")
@ -106,6 +113,13 @@ func new_player_settings() -> PlayerSettings:
container.add_child(result) container.add_child(result)
return 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: func on_join_pressed() -> void:
var address_widget: LineEdit = get_settings_window().get_node("Connect/VBoxContainer/Join/Address") var address_widget: LineEdit = get_settings_window().get_node("Connect/VBoxContainer/Join/Address")
@ -128,6 +142,10 @@ func on_host_pressed() -> void:
update_status() update_status()
print_log("Opened server") print_log("Opened server")
update_enabled_state(true) 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: else:
print_log("Unable to open server!") print_log("Unable to open server!")
@ -138,6 +156,8 @@ func on_disconnect_pressed() -> void:
print_log("Closed server") print_log("Closed server")
update_enabled_state(false) update_enabled_state(false)
clear_player_models() clear_player_models()
ping_update_timer.queue_free()
ping_update_timer = null
multiplayer.multiplayer_peer.close() multiplayer.multiplayer_peer.close()
@ -189,6 +209,7 @@ func update_enabled_state(is_online: bool) -> void:
settings.connect_join .disabled = is_online settings.connect_join .disabled = is_online
settings.connect_host .disabled = is_online settings.connect_host .disabled = is_online
settings.connect_disconnect.disabled = !is_online settings.connect_disconnect.disabled = !is_online
if not is_online: main_stats.set_ping(0)
func update_status() -> void: func update_status() -> void:
var num_players := 1 + multiplayer.get_peers().size() var num_players := 1 + multiplayer.get_peers().size()
@ -207,9 +228,11 @@ func clear_player_models() -> void:
func _process(_delta: float) -> void: func _process(_delta: float) -> void:
SyncController.send_model_animation(self) 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. # 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. # Called when module is initialized or load_vrm is called.
func scene_init() -> void: func scene_init() -> void:
@ -243,10 +266,40 @@ func scene_init() -> void:
change_model.rpc(version, bone_lookup, blendshape_lookup, filename) 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: func get_sync_controller(peer_id: int) -> SyncController:
return get_node_or_null(str(peer_id)) as 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") @rpc("any_peer", "reliable")
func change_nickname(new_nickname: String) -> void: func change_nickname(new_nickname: String) -> void:
var peer_id := multiplayer.get_remote_sender_id() var peer_id := multiplayer.get_remote_sender_id()

Loading…
Cancel
Save