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