extends Mod_Base @export var cache := "" @export var nickname := "" # FIXME: Not used for anything yet. @export var address := "" @export var port := 52410 var main_controller: ModelController # Temporary positioning system. # TODO: Add a setting that allows syncing model positions, as an alternative # to letting the local player choose where each model is going to appear. var player_order: Array[int] = [] var starting_offset := Vector3(-1.25, 0.0, 0.0) var accumulative_offset := Vector3( 0.35, 0.0, 0.0) # Allows us to use the "apply_animations" function to apply blendshapes. var functions_blendshapes: Script = load("res://Mods/MediaPipe/MediaPipeController_BlendShapes.gd") func _ready() -> void: # FIXME: Hardcoded way to get the main model controller. main_controller = $"/root/SnekStudio_Main/ModelController" main_controller.child_entered_tree.connect(model_changed) setup_setting_widget("cache" , "Cache/LineEdit") setup_setting_widget("nickname", "Name/LineEdit" ) setup_setting_widget("address" , "Host/Address" ) setup_setting_widget("port" , "Host/Port" ) setup_button_connections() multiplayer.peer_connected .connect(peer_connected) multiplayer.peer_disconnected .connect(peer_disconnected) multiplayer.connected_to_server.connect(connected_to_server) multiplayer.connection_failed .connect(connection_failed) multiplayer.server_disconnected.connect(server_disconnected) func _exit_tree() -> void: multiplayer.multiplayer_peer.close() func _create_settings_window() -> Control: return load("res://Mods/copyMultiplayer/Resources/copy_multiplayer_settings.tscn").instantiate() func setup_setting_widget(setting_name: String, path: NodePath) -> void: var settings = get_settings_window() var widget: Control = settings.get_node(path) _settings_properties.append({ name = setting_name, args = { } }) _settings_widgets_by_setting_name[setting_name] = widget if widget is LineEdit: widget.text_changed.connect(func(text): modify_setting(setting_name, text)) if widget is SpinBox: widget.value_changed.connect(func(number): modify_setting(setting_name, roundi(number))) func setup_button_connections() -> void: var window = get_settings_window() window.get_node("Buttons/Join").pressed.connect(join_pressed) window.get_node("Buttons/Host").pressed.connect(host_pressed) window.get_node("Buttons/Disconnect").pressed.connect(disconnect_pressed) func join_pressed() -> void: var address_widget: LineEdit = get_settings_window().get_node("Host/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 host_pressed() -> void: var peer := ENetMultiplayerPeer.new() if peer.create_server(port) == OK: multiplayer.multiplayer_peer = peer set_status("Hosting: 0 players") print_log("Opened server") update_enabled_state(true) else: print_log("Unable to open server!") func disconnect_pressed() -> void: assert(multiplayer.multiplayer_peer) if multiplayer.is_server(): set_status("") print_log("Closed server") update_enabled_state(false) multiplayer.multiplayer_peer.close() func peer_connected(id: int) -> void: update_status() print_log(["Player ", id, " connected"]) var new_controller := ModelController.new() new_controller.name = str(id) add_child(new_controller) player_order.push_back(id) update_model_transforms() var filename = main_controller._last_loaded_vrm.get_file() if filename.is_valid_filename(): change_model.rpc_id(id, filename) func peer_disconnected(id: int) -> void: update_status() print_log(["Player ", id, " disconnected"]) var controller: ModelController = get_node(str(id)) remove_child(controller) controller.queue_free() player_order.remove_at(player_order.find(id)) update_model_transforms() func connected_to_server() -> void: print_log("Connected to server") func connection_failed() -> void: set_status("") print_log("Connection failed!") update_enabled_state(false) func server_disconnected() -> void: set_status("") print_log("Disconnected from server") update_enabled_state(false) for controller in get_children(): if controller is ModelController: remove_child(controller) controller.queue_free() player_order.clear() func update_enabled_state(is_online: bool) -> void: var window = get_settings_window() window.get_node("Name/LineEdit").editable = !is_online window.get_node("Host/Address" ).editable = !is_online window.get_node("Host/Port" ).editable = !is_online window.get_node("Buttons/Join" ).disabled = is_online window.get_node("Buttons/Host" ).disabled = is_online window.get_node("Buttons/Disconnect").disabled = !is_online func update_model_transforms() -> void: var offset := starting_offset for id in player_order: var controller: ModelController = get_node(str(id)) controller.position = offset offset += accumulative_offset update_model_rotation(controller) ## Rotates the model a little bit towards the camera ## so it doesn't appear to be staring into nowhere. func update_model_rotation(controller: ModelController) -> void: var camera := get_viewport().get_camera_3d() var from_2d := Vector2(controller.position.x, controller.position.z) var to_2d := Vector2(camera.position.x, camera.position.z) controller.rotation.y = from_2d.angle_to_point(to_2d) / 4 # Magic value, probably depends on FOV. func update_status() -> void: var num_player := multiplayer.get_peers().size() var side := "Hosting" if multiplayer.is_server() else "Connected" set_status([side, ": ", num_player, " players"]) @rpc("any_peer", "reliable") func change_model(filename: String) -> void: var player_id := multiplayer.get_remote_sender_id() var controller := get_node_or_null(str(player_id)) as ModelController if not controller: return if not filename.is_valid_filename(): print_log(["ERROR: '", filename, "' is not a valid file name!"]) return var full_path := cache.path_join(filename) if not FileAccess.file_exists(full_path): print_log(["Player ", player_id, " wanted to switch to '", filename, "', but it could not be found, skipping"]) return if controller.load_vrm(full_path): print_log(["Player ", player_id, " switched to '", filename, "'"]) else: print_log(["ERROR: Model '", filename, "' could not be loaded!"]) # Called when a node is added to the main ModelController. func model_changed(child: Node) -> void: if child.name != "Model": return # Wait for one frame, then "_last_loaded_vrm" is updated. await get_tree().process_frame var filename = main_controller._last_loaded_vrm.get_file() if filename.is_valid_filename(): change_model.rpc(filename) # FIXME: This sends way more information than necessary, but works as a proof-of-concept! @rpc("any_peer", "unreliable_ordered") func update( model_transform: Transform3D, shape_dict: Dictionary, # Dictionary[String, float] bone_poses: Dictionary, # Dictionary[String, Transform3D] ) -> void: var player_id := multiplayer.get_remote_sender_id() var controller := get_node_or_null(str(player_id)) if not controller: return var model := controller.get_node_or_null("Model") as Node3D if model: model.transform = model_transform functions_blendshapes.apply_animations(model, shape_dict) var skeleton := controller._get_model_skeleton() as Skeleton3D if skeleton: for bone_name in bone_poses: var pose: Transform3D = bone_poses[bone_name] var idx := skeleton.find_bone(bone_name) if idx != -1: skeleton.set_bone_pose(idx, pose) func _process(_delta: float) -> void: if multiplayer.get_peers().size() == 0: return var model := get_model() var skeleton := get_skeleton() var media_pipe_ctrl = $"../MediaPipeController" if (not model) or (not skeleton) or (not media_pipe_ctrl): return var shape_dict = media_pipe_ctrl.blend_shape_last_values var bone_poses = {} for idx in skeleton.get_bone_count(): var bone_name = skeleton.get_bone_name(idx) var bone_pose = skeleton.get_bone_pose(idx) bone_poses[bone_name] = bone_pose update.rpc(model.transform, shape_dict, bone_poses)