SnekStudio module for multiplayer / multiuser support
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

271 lines
9.9 KiB

class_name copyMultiplayer
extends Mod_Base
@export var cache := ""
@export var nickname := ""
@export var address := ""
@export var port := 52410
var main_controller: ModelController
## Hardcoded list of bone names that will get syncronized.
var tracked_bones: Array[String]
var version: int = -1
var bone_lookup: Array[String] = []
var blendshape_lookup: Array[NodePath] = []
# 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
# I don't believe this should be a source of issues.
func _ready() -> void:
# FIXME: Hardcoded way to get the main model controller.
main_controller = $"/root/SnekStudio_Main/ModelController"
# 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.
tracked_bones = [ "Hips", "Spine", "Chest", "UpperChest", "Neck", "Head" ]
for side in [ "Left", "Right" ]:
# Eyes
tracked_bones.append("%sEye" % side)
# Legs
for bone in [ "UpperLeg", "LowerLeg", "Foot" ]:
tracked_bones.append("%s%s" % [ side, bone ])
# Arms
for bone in [ "Shoulder", "UpperArm", "LowerArm", "Hand" ]:
tracked_bones.append("%s%s" % [ side, bone ])
# Fingers
for phalange in [ "Metacarpal", "Proximal", "Distal" ]:
tracked_bones.append("%sThumb%s" % [ side, phalange ])
for finger in [ "Index", "Middle", "Ring", "Little" ]:
for phalange in [ "Proximal", "Intermediate", "Distal" ]:
tracked_bones.append("%s%s%s" % [ side, finger, phalange ])
setup_setting_widget("connect" , "address" , true )
setup_setting_widget("connect" , "port" , true )
setup_setting_widget("settings", "cache" , true )
setup_setting_widget("settings", "nickname", false)
setup_button_connections()
# 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.focus_exited.connect(func(): nickname_widget.text = nickname)
multiplayer.peer_connected .connect(on_peer_connected)
multiplayer.peer_disconnected .connect(on_peer_disconnected)
multiplayer.connected_to_server.connect(on_connected_to_server)
multiplayer.connection_failed .connect(on_connection_failed)
multiplayer.server_disconnected.connect(on_server_disconnected)
func _exit_tree() -> void:
multiplayer.multiplayer_peer.close()
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 setup_setting_widget(category: String, setting: String, setup_events: bool) -> void:
var settings := get_settings_window()
var widget: Control = settings.get("%s_%s" % [ category, setting ])
_settings_properties.append({ name = setting, args = { } })
_settings_widgets_by_setting_name[setting] = widget
if setup_events:
if widget is LineEdit: widget.text_changed.connect(
func(text): modify_setting(setting, text))
if widget is SpinBox: widget.value_changed.connect(
func(number): modify_setting(setting, roundi(number)))
func setup_button_connections() -> void:
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)
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("Connect/VBoxContainer/Join/Address")
var default_address: String = address_widget.placeholder_text
var actual_address := default_address if address.is_empty() else address
var peer := ENetMultiplayerPeer.new()
if peer.create_client(actual_address, port) == OK:
multiplayer.multiplayer_peer = peer
set_status("Connecting ...")
print_log("Connecting to server")
update_enabled_state(true)
else:
print_log("Unable to connect!")
func on_host_pressed() -> void:
var peer := ENetMultiplayerPeer.new()
if peer.create_server(port) == OK:
multiplayer.multiplayer_peer = peer
update_status()
print_log("Opened server")
update_enabled_state(true)
else:
print_log("Unable to open server!")
func on_disconnect_pressed() -> void:
assert(multiplayer.multiplayer_peer)
if multiplayer.is_server():
set_status("")
print_log("Closed server")
update_enabled_state(false)
clear_player_models()
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 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.)
if nickname: change_nickname.rpc_id(id, nickname)
var filename = main_controller._last_loaded_vrm.get_file()
if filename.is_valid_filename(): change_model.rpc_id(id, version, bone_lookup, blendshape_lookup, filename)
update_status()
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)
controller.queue_free()
update_status()
print_log("Player %s disconnected" % id)
func on_connected_to_server() -> void:
print_log("Connected to server")
func on_connection_failed() -> void:
set_status("")
print_log("Connection failed!")
update_enabled_state(false)
func on_server_disconnected() -> void:
set_status("")
print_log("Disconnected from server")
update_enabled_state(false)
clear_player_models()
func update_enabled_state(is_online: bool) -> void:
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
func update_status() -> void:
var num_players := 1 + multiplayer.get_peers().size()
var side := "Hosting" if multiplayer.is_server() else "Connected"
var s := "s" if num_players != 1 else ""
set_status("%s: %d player%s" % [side, num_players, s])
## 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()
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:
var model := get_model()
var skeleton := get_skeleton()
# Do nothing, I guess? Unsure if source of potential bugs.
if (not model) or (not skeleton): return
# This "version" is used to ensure that sync updates
# are not applied to the incorrect lookup tables.
version = (version + 1) % 256
bone_lookup.clear()
for bone_name in tracked_bones:
var bone_idx := skeleton.find_bone(bone_name)
if bone_idx == -1: continue
bone_lookup.append(bone_name)
blendshape_lookup.clear()
var anim_player: AnimationPlayer = model.find_child("AnimationPlayer", false, false)
for anim_name in anim_player.get_animation_list():
if anim_name == "RESET": continue # Skip RESET animation?
var anim := anim_player.get_animation(anim_name)
for track_index in anim.get_track_count():
if anim.track_get_type(track_index) == Animation.TYPE_BLEND_SHAPE:
blendshape_lookup.append(anim.track_get_path(track_index))
continue
if multiplayer.get_peers().size() > 1:
var filename = main_controller._last_loaded_vrm.get_file()
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
@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)
if controller: controller.change_nickname(new_nickname)
@rpc("any_peer", "reliable")
func change_model(
new_version: int,
new_bone_lookup: Array[String],
new_blendshape_lookup: Array[NodePath],
filename: String,
) -> void:
var peer_id := multiplayer.get_remote_sender_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")
func sync_model_animation(
current_version: int,
uncompressed_length: int,
buffer: PackedByteArray,
) -> void:
var peer_id := multiplayer.get_remote_sender_id()
var controller := get_sync_controller(peer_id)
if controller: controller.sync_model_animation(current_version, uncompressed_length, buffer)