Compare commits

...

2 Commits

Author SHA1 Message Date
copygirl a3e8841667 Remove type information from own types 1 month ago
copygirl 2290e058b6 Add player stats tab 1 month ago
  1. 2
      Scenes/copy_multiplayer_settings.gd
  2. 49
      Scenes/copy_multiplayer_settings.tscn
  3. 2
      Scenes/player_settings.gd
  4. 10
      Scenes/player_settings.tscn
  5. 33
      Scenes/player_stats.gd
  6. 32
      Scenes/player_stats.tscn
  7. 71
      Scenes/sync_controller.gd
  8. 10
      Utility/stream_buffer.gd
  9. 100
      copyMultiplayer.gd

@ -1,4 +1,4 @@
class_name copyMultiplayerSettings
#class_name copyMultiplayerSettings
extends TabContainer
@export var visible_icon : Texture2D

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

@ -1,4 +1,4 @@
class_name PlayerSettings
#class_name PlayerSettings
extends Container
signal value_changed(Transform3D)

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

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

@ -1,10 +1,12 @@
class_name SyncController
#class_name SyncController
extends Area3D
static var StreamBuffer = load("res://Mods/copyMultiplayer/Utility/stream_buffer.gd")
@export var shape: CollisionShape3D
var peer_id: int
var module: copyMultiplayer
var module #: copyMultiplayer
var model_controller: ModelController
var nickname: String
@ -18,13 +20,16 @@ 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
# Reusable buffer to write data for synchronizing models.
static var write_stream: StreamBuffer = StreamBuffer.with_capacity(2048)
static var write_stream = StreamBuffer.with_capacity(2048)
func _ready() -> void:
peer_id = name.to_int()
@ -34,16 +39,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 +73,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,
@ -73,7 +93,7 @@ func change_model(
if not filename.is_valid_filename():
module.print_log("ERROR: '%s' is not a valid file name!" % filename)
return
var full_path := module.cache.path_join(filename)
var full_path: String = module.cache.path_join(filename)
if not FileAccess.file_exists(full_path):
module.print_log("%s wanted to switch to '%s', but it doesn't exist, skipping" % [ get_display_name(), filename ])
return
@ -91,6 +111,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,9 +121,10 @@ 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)
var stream = StreamBuffer.from_buffer(uncompressed_buffer)
model.transform = stream.read_transform16()
@ -118,10 +142,10 @@ func sync_model_animation(
# 256 bones (and blendshapes) should be enough, right?
for i in stream.read_uint8():
var lookup := stream.read_uint8()
var lookup: int = stream.read_uint8()
all_bones[lookup].pose = stream.read_bone_pose(all_bones[lookup].pose)
for i in stream.read_uint8():
var lookup := stream.read_uint8()
var lookup: int = stream.read_uint8()
all_blendshapes[lookup].value = stream.read_range16()
# Apply all the values to bones / blendshapes.
@ -134,14 +158,14 @@ func sync_model_animation(
anim_node.set("blend_shapes/" + blendshape.path.get_subname(0), blendshape.value)
@warning_ignore("shadowed_variable")
static func send_model_animation(module: copyMultiplayer) -> void:
static func send_model_animation(module) -> void:
# Check if there's other players we're connected to.
if module.multiplayer.get_peers().size() == 0: return
var model := module.get_model()
var skeleton := module.get_skeleton()
var anim_player := model.find_child("AnimationPlayer", false, false)
var anim_root := anim_player.get_node_or_null(anim_player.root_node)
var model = module.get_model()
var skeleton = module.get_skeleton()
var anim_player = model.find_child("AnimationPlayer", false, false)
var anim_root = anim_player.get_node_or_null(anim_player.root_node)
if (not model) or (not skeleton) or (not anim_root): return
write_stream.write_transform16(model.transform)
@ -152,16 +176,16 @@ static func send_model_animation(module: copyMultiplayer) -> void:
var active_blendshapes := []
for i in module.bone_lookup.size():
var bone_name := module.bone_lookup[i]
var bone_idx := skeleton.find_bone(bone_name)
var bone_pose := skeleton.get_bone_pose(bone_idx)
var bone_rest := skeleton.get_bone_rest(bone_idx)
var bone_name = module.bone_lookup[i]
var bone_idx = skeleton.find_bone(bone_name)
var bone_pose = skeleton.get_bone_pose(bone_idx)
var bone_rest = skeleton.get_bone_rest(bone_idx)
if not bone_pose.is_equal_approx(bone_rest):
restless_bones.append({ lookup = i, pose = bone_pose, rest = bone_rest })
for i in module.blendshape_lookup.size():
var anim_path := module.blendshape_lookup[i]
var anim_node := anim_root.get_node_or_null(anim_path)
var anim_path = module.blendshape_lookup[i]
var anim_node = anim_root.get_node_or_null(anim_path)
var value: float = anim_node.get("blend_shapes/" + anim_path.get_subname(0))
if not is_zero_approx(value):
active_blendshapes.append({ lookup = i, value = value })
@ -177,10 +201,11 @@ static func send_model_animation(module: copyMultiplayer) -> void:
write_stream.write_range16(blendshape.value)
# The compression still helps, so we'll keep it for now.
var compressed_buffer := write_stream.slice().compress(FileAccess.COMPRESSION_ZSTD);
var compressed_buffer = write_stream.slice().compress(FileAccess.COMPRESSION_ZSTD);
# 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 +239,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

@ -1,4 +1,4 @@
class_name StreamBuffer
#class_name StreamBuffer
extends Resource
# This maximum capacity is just to ensure we're not doing something wrong,
@ -18,15 +18,15 @@ func _init(_buffer: PackedByteArray) -> void:
## Creates a new StreamBuffer with the specified capacity.
## This is intended for writing / encoding data.
static func with_capacity(initial_capacity: int) -> StreamBuffer:
static func with_capacity(initial_capacity: int): #-> StreamBuffer:
var _buffer = PackedByteArray()
_buffer.resize(initial_capacity)
return StreamBuffer.new(_buffer)
return new(_buffer)
## Creates a new StreamBuffer from the specified buffer, pre-initializing "size".
## This is intended for reading / decoding data.
static func from_buffer(_buffer: PackedByteArray) -> StreamBuffer:
var stream := StreamBuffer.new(_buffer)
static func from_buffer(_buffer: PackedByteArray): #-> StreamBuffer:
var stream := new(_buffer)
stream.size = stream.capacity
return stream

@ -1,12 +1,18 @@
class_name copyMultiplayer
#class_name copyMultiplayer
extends Mod_Base
# NOTE: Due to issues with resource pack loading at runtime, we can't use
# typed references to this mod. This is a crime to all slime-kind.
static var SyncController = load("res://Mods/copyMultiplayer/Scenes/sync_controller.gd")
@export var cache := ""
@export var nickname := ""
@export var address := ""
@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 +33,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.
@ -55,9 +62,9 @@ func _ready() -> void:
setup_button_connections()
# 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_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)
@ -73,11 +80,11 @@ func _create_settings_window() -> Control:
return load("res://Mods/copyMultiplayer/Scenes/copy_multiplayer_settings.tscn").instantiate()
# Override base method to provide type hint.
func get_settings_window() -> copyMultiplayerSettings:
return super.get_settings_window()
# func get_settings_window() -> copyMultiplayerSettings:
# return super.get_settings_window()
func setup_setting_widget(category: String, setting: String, setup_events: bool) -> void:
var settings := get_settings_window()
var settings = get_settings_window()
var widget: Control = settings.get("%s_%s" % [ category, setting ])
_settings_properties.append({ name = setting, args = { } })
@ -94,18 +101,29 @@ func setup_setting_widget(category: String, setting: String, setup_events: bool)
widget.set_meta("reset_button", { })
func setup_button_connections() -> void:
var settings := get_settings_window()
var settings = get_settings_window()
settings.connect_join.pressed.connect(on_join_pressed)
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:
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
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 +146,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 +160,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()
@ -182,13 +206,14 @@ func on_server_disconnected() -> void:
func update_enabled_state(is_online: bool) -> void:
var settings := get_settings_window()
var settings = get_settings_window()
settings.settings_nickname .editable = !is_online
settings.connect_address .editable = !is_online
settings.connect_port .editable = !is_online
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()
@ -199,17 +224,18 @@ func update_status() -> void:
## Removes all networked player models.
func clear_player_models() -> void:
for controller in get_children():
if controller is SyncController:
remove_child(controller)
controller.queue_free()
for controller in get_all_sync_controllers():
remove_child(controller)
controller.queue_free()
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,14 +269,46 @@ 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_sync_controller(peer_id: int): #-> SyncController:
return get_node_or_null(str(peer_id)) #as SyncController
func get_all_sync_controllers(): #-> Array[SyncController]:
return get_children()
# NOTE: Let's just assume all child nodes are 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()
var controller := get_sync_controller(peer_id)
var controller = get_sync_controller(peer_id)
if controller: controller.change_nickname(new_nickname)
@rpc("any_peer", "reliable")
@ -261,7 +319,7 @@ func change_model(
filename: String,
) -> void:
var peer_id := multiplayer.get_remote_sender_id()
var controller := get_sync_controller(peer_id)
var controller = get_sync_controller(peer_id)
if controller: controller.change_model(new_version, new_bone_lookup, new_blendshape_lookup, filename)
@rpc("any_peer", "unreliable_ordered")
@ -271,5 +329,5 @@ func sync_model_animation(
buffer: PackedByteArray,
) -> void:
var peer_id := multiplayer.get_remote_sender_id()
var controller := get_sync_controller(peer_id)
var controller = get_sync_controller(peer_id)
if controller: controller.sync_model_animation(current_version, uncompressed_length, buffer)

Loading…
Cancel
Save