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.
262 lines
9.3 KiB
262 lines
9.3 KiB
extends Mod_Base |
|
|
|
@export var cache := "" |
|
@export var nickname := "" # FIXME: Not used for anything yet. |
|
@export var address := "" |
|
@export var port := 52410 |
|
|
|
## Hardcoded list of bone names that will get syncronized. |
|
var tracked_bones: Array[String] |
|
|
|
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: 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", "Chest", "UpperChest", "Neck", "Head" ] |
|
for side in [ "Left", "Right" ]: |
|
# 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 ]) |
|
|
|
# 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 |
|
update_status() |
|
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) |
|
clear_player_models() |
|
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.append(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) |
|
clear_player_models() |
|
|
|
|
|
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_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([side, ": ", num_players, " player", s]) |
|
|
|
func clear_player_models() -> void: |
|
for controller in get_children(): |
|
if controller is ModelController: |
|
remove_child(controller) |
|
controller.queue_free() |
|
player_order.clear() |
|
|
|
|
|
@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 sync_model_animation( |
|
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 bone_name in tracked_bones: |
|
var bone_idx = skeleton.find_bone(bone_name) |
|
if bone_idx == -1: continue |
|
var bone_pose = skeleton.get_bone_pose(bone_idx) |
|
bone_poses[bone_name] = bone_pose |
|
sync_model_animation.rpc(model.transform, shape_dict, bone_poses)
|
|
|