|
|
|
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] = []
|
|
|
|
|
|
|
|
# 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)
|
|
|
|
|
|
|
|
# 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: 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(on_model_changed)
|
|
|
|
on_model_changed(main_controller.get_node_or_null("Model"))
|
|
|
|
|
|
|
|
setup_setting_widget("cache" , "Cache/LineEdit", true )
|
|
|
|
setup_setting_widget("nickname", "Name/LineEdit" , false)
|
|
|
|
setup_setting_widget("address" , "Host/Address" , true )
|
|
|
|
setup_setting_widget("port" , "Host/Port" , true )
|
|
|
|
setup_button_connections()
|
|
|
|
|
|
|
|
# Filter whitespace characters from nickname before saving it to "nickname" field.
|
|
|
|
var nickname_widget: LineEdit = get_settings_window().get_node("Name/LineEdit")
|
|
|
|
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/Resources/copy_multiplayer_settings.tscn").instantiate()
|
|
|
|
|
|
|
|
func setup_setting_widget(setting_name: String, path: NodePath, setup_events: bool) -> 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 setup_events:
|
|
|
|
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(on_join_pressed)
|
|
|
|
window.get_node("Buttons/Host").pressed.connect(on_host_pressed)
|
|
|
|
window.get_node("Buttons/Disconnect").pressed.connect(on_disconnect_pressed)
|
|
|
|
|
|
|
|
|
|
|
|
func on_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 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()
|
|
|
|
|
|
|
|
|
|
|
|
func on_peer_connected(id: int) -> void:
|
|
|
|
var model_controller := ModelController.new()
|
|
|
|
model_controller.name = str(id)
|
|
|
|
var sync_controller := SyncController.new()
|
|
|
|
sync_controller.name = "SyncController"
|
|
|
|
model_controller.add_child(sync_controller)
|
|
|
|
add_child(model_controller)
|
|
|
|
|
|
|
|
player_order.append(id)
|
|
|
|
update_model_transforms()
|
|
|
|
|
|
|
|
# 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" % sync_controller.get_display_name())
|
|
|
|
|
|
|
|
func on_peer_disconnected(id: int) -> void:
|
|
|
|
var controller = get_sync_controller(id)
|
|
|
|
if not controller: return
|
|
|
|
|
|
|
|
remove_child(controller.model_controller)
|
|
|
|
controller.model_controller.queue_free()
|
|
|
|
|
|
|
|
player_order.remove_at(player_order.find(id))
|
|
|
|
update_model_transforms()
|
|
|
|
|
|
|
|
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 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_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])
|
|
|
|
|
|
|
|
|
|
|
|
# FIXME: Temporary hardcoded way to assign some offset to other players.
|
|
|
|
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.
|
|
|
|
|
|
|
|
## Removes all networked player models.
|
|
|
|
func clear_player_models() -> void:
|
|
|
|
for controller in get_children():
|
|
|
|
if controller is ModelController:
|
|
|
|
remove_child(controller)
|
|
|
|
controller.queue_free()
|
|
|
|
player_order.clear()
|
|
|
|
|
|
|
|
|
|
|
|
func _process(_delta: float) -> void:
|
|
|
|
SyncController.send_model_animation(self)
|
|
|
|
|
|
|
|
# Will be called once when the module is initialized, and also when a child is
|
|
|
|
# added to the main ModelController, which happens when the model is changed.
|
|
|
|
func on_model_changed(model: Node) -> void:
|
|
|
|
if (model == null) or (model.name != "Model"): return
|
|
|
|
|
|
|
|
var skeleton: Skeleton3D = main_controller._get_model_skeleton()
|
|
|
|
if not skeleton: return # Do nothing, I guess? Unsure if potential bug.
|
|
|
|
|
|
|
|
# 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
|
|
|
|
|
|
|
|
# Wait for one frame, then "_last_loaded_vrm" is updated.
|
|
|
|
await get_tree().process_frame
|
|
|
|
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:
|
|
|
|
var model_controller := get_node_or_null(str(peer_id)) as ModelController
|
|
|
|
if not model_controller: return null
|
|
|
|
return model_controller.get_node_or_null("SyncController") 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)
|