2026-07-29 21:31:42 -04:00
|
|
|
class_name NetworkProfileService
|
|
|
|
|
extends Node
|
|
|
|
|
|
|
|
|
|
signal conflict_result(
|
|
|
|
|
request_id: String,
|
|
|
|
|
has_conflict: bool,
|
|
|
|
|
suggestions: PackedStringArray,
|
|
|
|
|
)
|
|
|
|
|
signal apply_finished(accepted: bool, message: String)
|
|
|
|
|
signal profile_snapshot_changed(
|
|
|
|
|
peer_id: int,
|
|
|
|
|
display_name: String,
|
|
|
|
|
appearance: Dictionary,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
var _session: NetworkSession
|
|
|
|
|
var _preferences: NetworkProfilePreferences
|
|
|
|
|
var _appearance_store: PlayerAppearanceStore
|
|
|
|
|
var _spawn_service: PlayerSpawnService
|
|
|
|
|
var _pending_apply: Dictionary[String, Dictionary] = {}
|
|
|
|
|
var _host_pending_apply: Dictionary[String, Dictionary] = {}
|
|
|
|
|
var _latest_check_id: String = ""
|
|
|
|
|
var _latest_check_name: String = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func setup(
|
|
|
|
|
session: NetworkSession,
|
|
|
|
|
preferences: NetworkProfilePreferences,
|
|
|
|
|
appearance_store: PlayerAppearanceStore,
|
|
|
|
|
spawn_service: PlayerSpawnService,
|
|
|
|
|
) -> void:
|
|
|
|
|
_session = session
|
|
|
|
|
_preferences = preferences
|
|
|
|
|
_appearance_store = appearance_store
|
|
|
|
|
_spawn_service = spawn_service
|
|
|
|
|
_appearance_store.load_preferences()
|
|
|
|
|
_session.peer_authenticated.connect(_on_peer_authenticated)
|
|
|
|
|
_session.peer_removed.connect(_on_peer_removed)
|
|
|
|
|
_session.peer_display_name_changed.connect(_on_peer_display_name_changed)
|
|
|
|
|
_session.join_authenticated.connect(_on_join_authenticated)
|
|
|
|
|
_session.state_changed.connect(_on_session_state_changed)
|
|
|
|
|
_apply_to_avatar(1, _appearance_store.get_snapshot())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_persisted_name() -> String:
|
|
|
|
|
return _preferences.display_name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_persisted_appearance() -> Dictionary:
|
|
|
|
|
return _appearance_store.get_snapshot()
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 22:18:14 -04:00
|
|
|
func get_identity_fingerprint() -> String:
|
|
|
|
|
return (
|
|
|
|
|
_session.get_local_identity_fingerprint()
|
|
|
|
|
if _session != null else ""
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 21:31:42 -04:00
|
|
|
func request_name_check(display_name: String) -> String:
|
|
|
|
|
var request_id := _new_id()
|
|
|
|
|
_latest_check_id = request_id
|
|
|
|
|
var clean_name := display_name.strip_edges()
|
|
|
|
|
_latest_check_name = clean_name
|
|
|
|
|
if not NetworkProfilePreferences.is_valid_display_name(clean_name):
|
|
|
|
|
conflict_result.emit(request_id, false, PackedStringArray())
|
|
|
|
|
return request_id
|
|
|
|
|
if _session == null or not _session.is_gameplay_session_active():
|
|
|
|
|
conflict_result.emit(request_id, false, PackedStringArray())
|
|
|
|
|
elif _session.is_host():
|
|
|
|
|
var result := _make_conflict_result(
|
|
|
|
|
_session.get_local_peer_id(), clean_name, request_id
|
|
|
|
|
)
|
|
|
|
|
_emit_conflict_result(result)
|
|
|
|
|
elif _session.supports_server_capability(&"profile_v1"):
|
|
|
|
|
submit_name_check.rpc_id(1, {
|
|
|
|
|
"request_id": request_id,
|
|
|
|
|
"session_id": _session.get_session_id(),
|
|
|
|
|
"display_name": clean_name,
|
|
|
|
|
})
|
|
|
|
|
else:
|
|
|
|
|
conflict_result.emit(request_id, false, PackedStringArray())
|
|
|
|
|
return request_id
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func apply_profile(
|
|
|
|
|
display_name: String,
|
|
|
|
|
appearance: Dictionary,
|
|
|
|
|
use_anyway: bool,
|
|
|
|
|
) -> bool:
|
|
|
|
|
var clean_name := display_name.strip_edges()
|
|
|
|
|
if (
|
|
|
|
|
not NetworkProfilePreferences.is_valid_display_name(clean_name)
|
|
|
|
|
or not CharacterCustomizationCatalog.validate_snapshot(appearance)
|
|
|
|
|
):
|
|
|
|
|
apply_finished.emit(false, "Check the player name and appearance choices.")
|
|
|
|
|
return false
|
|
|
|
|
var request_id := _new_id()
|
|
|
|
|
var request := {
|
|
|
|
|
"request_id": request_id,
|
|
|
|
|
"session_id": _session.get_session_id() if _session != null else "",
|
|
|
|
|
"display_name": clean_name,
|
|
|
|
|
"appearance": appearance.duplicate(true),
|
|
|
|
|
"use_anyway": use_anyway,
|
2026-07-29 22:18:14 -04:00
|
|
|
"sender_fingerprint": _session.get_local_identity_fingerprint(),
|
2026-07-29 21:31:42 -04:00
|
|
|
}
|
2026-07-29 22:18:14 -04:00
|
|
|
request["sender_signature"] = _session.sign_local_action(
|
|
|
|
|
"profile_update", NetworkProfileProtocol.signature_fields(request)
|
|
|
|
|
)
|
2026-07-29 21:31:42 -04:00
|
|
|
_pending_apply[request_id] = request
|
|
|
|
|
if _session == null or not _session.is_gameplay_session_active():
|
|
|
|
|
_apply_local_result(request_id, true, "", false, PackedStringArray())
|
|
|
|
|
elif _session.is_host():
|
|
|
|
|
_process_apply_request(_session.get_local_peer_id(), request)
|
|
|
|
|
elif _session.supports_server_capability(&"profile_v1"):
|
|
|
|
|
submit_profile_apply.rpc_id(1, request)
|
|
|
|
|
else:
|
|
|
|
|
_apply_local_result(request_id, true, "", false, PackedStringArray())
|
|
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
|
|
|
|
func submit_name_check(data: Dictionary) -> void:
|
|
|
|
|
var sender_id := multiplayer.get_remote_sender_id()
|
|
|
|
|
if (
|
|
|
|
|
not _session.is_host()
|
|
|
|
|
or not _session.is_authenticated_peer(sender_id)
|
|
|
|
|
or not NetworkProfileProtocol.valid_check_request(data)
|
|
|
|
|
or str(data["session_id"]) != _session.get_session_id()
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
receive_name_check.rpc_id(
|
|
|
|
|
sender_id,
|
|
|
|
|
_make_conflict_result(sender_id, str(data["display_name"]), str(data["request_id"])),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
|
|
|
|
func receive_name_check(data: Dictionary) -> void:
|
|
|
|
|
if (
|
|
|
|
|
typeof(data.get("request_id")) != TYPE_STRING
|
|
|
|
|
or typeof(data.get("has_conflict")) != TYPE_BOOL
|
|
|
|
|
or typeof(data.get("suggestions")) != TYPE_ARRAY
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
_emit_conflict_result(data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
|
|
|
|
func submit_profile_apply(data: Dictionary) -> void:
|
|
|
|
|
var sender_id := multiplayer.get_remote_sender_id()
|
|
|
|
|
if (
|
|
|
|
|
not _session.is_host()
|
|
|
|
|
or not _session.is_authenticated_peer(sender_id)
|
|
|
|
|
or not NetworkProfileProtocol.valid_apply_request(data)
|
|
|
|
|
or str(data["session_id"]) != _session.get_session_id()
|
|
|
|
|
):
|
|
|
|
|
return
|
2026-07-29 22:18:14 -04:00
|
|
|
var record := _session.get_peer_record(sender_id)
|
|
|
|
|
if (
|
|
|
|
|
record == null
|
|
|
|
|
or record.identity_fingerprint != str(data["sender_fingerprint"])
|
|
|
|
|
or not _session.verify_peer_action(
|
|
|
|
|
sender_id,
|
|
|
|
|
"profile_update",
|
|
|
|
|
NetworkProfileProtocol.signature_fields(data),
|
|
|
|
|
data["sender_signature"],
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
return
|
2026-07-29 21:31:42 -04:00
|
|
|
_process_apply_request(sender_id, data)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _process_apply_request(peer_id: int, data: Dictionary) -> void:
|
2026-07-30 00:54:53 -04:00
|
|
|
var display_name: String = str(data["display_name"]).strip_edges()
|
|
|
|
|
var conflict: bool = _name_conflicts(peer_id, display_name)
|
|
|
|
|
var accepted: bool = not conflict or bool(data["use_anyway"])
|
|
|
|
|
var suggestions: PackedStringArray = (
|
|
|
|
|
_make_suggestions(peer_id, display_name)
|
|
|
|
|
if conflict
|
|
|
|
|
else PackedStringArray()
|
|
|
|
|
)
|
2026-07-29 21:31:42 -04:00
|
|
|
if peer_id == _session.get_local_peer_id():
|
|
|
|
|
_apply_local_result(
|
|
|
|
|
str(data["request_id"]), accepted, "", conflict, suggestions
|
|
|
|
|
)
|
|
|
|
|
else:
|
|
|
|
|
if accepted:
|
|
|
|
|
_host_pending_apply[str(data["request_id"])] = {
|
|
|
|
|
"peer_id": peer_id,
|
2026-07-30 00:54:53 -04:00
|
|
|
"display_name": display_name,
|
2026-07-29 21:31:42 -04:00
|
|
|
"appearance": Dictionary(data["appearance"]).duplicate(true),
|
2026-07-29 22:18:14 -04:00
|
|
|
"authorization": {
|
|
|
|
|
"request_id": data["request_id"],
|
|
|
|
|
"session_id": data["session_id"],
|
|
|
|
|
"sender_fingerprint": data["sender_fingerprint"],
|
|
|
|
|
"sender_signature": data["sender_signature"],
|
|
|
|
|
"use_anyway": data["use_anyway"],
|
|
|
|
|
},
|
2026-07-29 21:31:42 -04:00
|
|
|
}
|
|
|
|
|
receive_profile_result.rpc_id(peer_id, {
|
|
|
|
|
"request_id": data["request_id"],
|
|
|
|
|
"accepted": accepted,
|
|
|
|
|
"conflict": conflict,
|
|
|
|
|
"suggestions": Array(suggestions),
|
|
|
|
|
"message": "" if accepted else "That name is already in use in this game.",
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@rpc("any_peer", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
|
|
|
|
func confirm_profile_saved(request_id: String) -> void:
|
|
|
|
|
var sender_id := multiplayer.get_remote_sender_id()
|
|
|
|
|
var pending: Dictionary = _host_pending_apply.get(request_id, {})
|
|
|
|
|
if (
|
|
|
|
|
not _session.is_host()
|
|
|
|
|
or pending.is_empty()
|
|
|
|
|
or int(pending["peer_id"]) != sender_id
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
_host_pending_apply.erase(request_id)
|
2026-07-30 00:54:53 -04:00
|
|
|
var display_name: String = str(pending["display_name"])
|
|
|
|
|
var appearance: Dictionary = Dictionary(pending["appearance"])
|
|
|
|
|
_session.apply_canonical_profile(sender_id, display_name, appearance)
|
|
|
|
|
var record: PeerRegistry.PeerRecord = _session.get_peer_record(sender_id)
|
2026-07-29 22:18:14 -04:00
|
|
|
if record != null:
|
|
|
|
|
record.profile_authorization = pending.get("authorization", {}).duplicate(true)
|
2026-07-29 21:31:42 -04:00
|
|
|
_apply_to_avatar(sender_id, appearance)
|
2026-07-29 22:18:14 -04:00
|
|
|
broadcast_profile_snapshot.rpc(
|
|
|
|
|
sender_id,
|
|
|
|
|
name,
|
|
|
|
|
appearance,
|
|
|
|
|
pending.get("authorization", {}),
|
|
|
|
|
)
|
2026-07-29 21:31:42 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
|
|
|
|
func receive_profile_result(data: Dictionary) -> void:
|
|
|
|
|
if (
|
|
|
|
|
typeof(data.get("request_id")) != TYPE_STRING
|
|
|
|
|
or typeof(data.get("accepted")) != TYPE_BOOL
|
|
|
|
|
or typeof(data.get("conflict")) != TYPE_BOOL
|
|
|
|
|
or typeof(data.get("suggestions")) != TYPE_ARRAY
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
var suggestions := PackedStringArray(data["suggestions"])
|
|
|
|
|
_apply_local_result(
|
|
|
|
|
str(data["request_id"]),
|
|
|
|
|
bool(data["accepted"]),
|
|
|
|
|
str(data.get("message", "")),
|
|
|
|
|
bool(data["conflict"]),
|
|
|
|
|
suggestions,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _apply_local_result(
|
|
|
|
|
request_id: String,
|
|
|
|
|
accepted: bool,
|
|
|
|
|
message: String,
|
|
|
|
|
conflict: bool,
|
|
|
|
|
suggestions: PackedStringArray,
|
|
|
|
|
) -> void:
|
|
|
|
|
var request: Dictionary = _pending_apply.get(request_id, {})
|
|
|
|
|
if request.is_empty():
|
|
|
|
|
return
|
|
|
|
|
if not accepted:
|
|
|
|
|
_pending_apply.erase(request_id)
|
|
|
|
|
conflict_result.emit(request_id, conflict, suggestions)
|
|
|
|
|
apply_finished.emit(false, message)
|
|
|
|
|
return
|
|
|
|
|
var previous_name := _preferences.display_name
|
|
|
|
|
if not _preferences.set_display_name(str(request["display_name"])):
|
|
|
|
|
_pending_apply.erase(request_id)
|
|
|
|
|
apply_finished.emit(false, "Profile could not be saved.")
|
|
|
|
|
return
|
|
|
|
|
if not _appearance_store.save_snapshot(request["appearance"]):
|
|
|
|
|
_preferences.set_display_name(previous_name)
|
|
|
|
|
_pending_apply.erase(request_id)
|
|
|
|
|
apply_finished.emit(false, "Profile could not be saved.")
|
|
|
|
|
return
|
|
|
|
|
_pending_apply.erase(request_id)
|
|
|
|
|
if _session != null and _session.is_gameplay_session_active():
|
|
|
|
|
if _session.is_host():
|
|
|
|
|
_session.apply_canonical_profile(
|
|
|
|
|
_session.get_local_peer_id(),
|
|
|
|
|
_preferences.display_name,
|
|
|
|
|
_appearance_store.get_snapshot(),
|
|
|
|
|
)
|
2026-07-29 22:18:14 -04:00
|
|
|
var record := _session.get_peer_record(
|
|
|
|
|
_session.get_local_peer_id()
|
|
|
|
|
)
|
|
|
|
|
if record != null:
|
|
|
|
|
record.profile_authorization = request.duplicate(true)
|
2026-07-29 21:31:42 -04:00
|
|
|
broadcast_profile_snapshot.rpc(
|
|
|
|
|
_session.get_local_peer_id(),
|
|
|
|
|
_preferences.display_name,
|
|
|
|
|
_appearance_store.get_snapshot(),
|
2026-07-29 22:18:14 -04:00
|
|
|
request,
|
2026-07-29 21:31:42 -04:00
|
|
|
)
|
|
|
|
|
elif _session.supports_server_capability(&"profile_v1"):
|
|
|
|
|
confirm_profile_saved.rpc_id(1, request_id)
|
|
|
|
|
elif _session.supports_server_capability(&"chat_v1"):
|
|
|
|
|
_session.update_local_display_name(_preferences.display_name)
|
|
|
|
|
_apply_to_avatar(
|
|
|
|
|
_session.get_local_peer_id() if _session != null else 1,
|
|
|
|
|
_appearance_store.get_snapshot(),
|
|
|
|
|
)
|
|
|
|
|
apply_finished.emit(true, "Profile saved.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@rpc("authority", "call_remote", "reliable", NetworkProfileProtocol.RELIABLE_CHANNEL)
|
|
|
|
|
func broadcast_profile_snapshot(
|
|
|
|
|
peer_id: int,
|
|
|
|
|
display_name: String,
|
|
|
|
|
appearance: Dictionary,
|
2026-07-29 22:18:14 -04:00
|
|
|
authorization: Dictionary = {},
|
2026-07-29 21:31:42 -04:00
|
|
|
) -> void:
|
|
|
|
|
if (
|
|
|
|
|
not NetworkProfilePreferences.is_valid_display_name(display_name)
|
|
|
|
|
or not CharacterCustomizationCatalog.validate_snapshot(appearance)
|
|
|
|
|
):
|
|
|
|
|
return
|
2026-07-29 22:18:14 -04:00
|
|
|
if not authorization.is_empty():
|
|
|
|
|
var signed := authorization.duplicate(true)
|
|
|
|
|
signed["display_name"] = display_name
|
|
|
|
|
signed["appearance"] = appearance
|
|
|
|
|
var record := _session.get_peer_record(peer_id)
|
|
|
|
|
if (
|
|
|
|
|
record == null
|
|
|
|
|
or record.identity_fingerprint
|
|
|
|
|
!= str(signed.get("sender_fingerprint", ""))
|
|
|
|
|
or not _session.verify_peer_action(
|
|
|
|
|
peer_id,
|
|
|
|
|
"profile_update",
|
|
|
|
|
NetworkProfileProtocol.signature_fields(signed),
|
|
|
|
|
signed.get("sender_signature", PackedByteArray()),
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
return
|
2026-07-29 21:31:42 -04:00
|
|
|
_session.apply_canonical_profile(peer_id, display_name, appearance)
|
|
|
|
|
_apply_to_avatar(peer_id, appearance)
|
|
|
|
|
profile_snapshot_changed.emit(peer_id, display_name, appearance.duplicate(true))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_peer_authenticated(peer_id: int, _display_name: String) -> void:
|
|
|
|
|
if not _session.is_host():
|
|
|
|
|
return
|
|
|
|
|
for existing_id: int in _session.get_authenticated_peer_ids():
|
|
|
|
|
var record := _session.get_peer_record(existing_id)
|
|
|
|
|
if record == null:
|
|
|
|
|
continue
|
|
|
|
|
broadcast_profile_snapshot.rpc_id(
|
|
|
|
|
peer_id,
|
|
|
|
|
existing_id,
|
|
|
|
|
record.display_name,
|
|
|
|
|
record.appearance_snapshot,
|
2026-07-29 22:18:14 -04:00
|
|
|
record.profile_authorization,
|
2026-07-29 21:31:42 -04:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_join_authenticated() -> void:
|
|
|
|
|
_apply_to_avatar(
|
|
|
|
|
_session.get_local_peer_id(), _appearance_store.get_snapshot()
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_peer_removed(_peer_id: int) -> void:
|
|
|
|
|
for request_id: String in _host_pending_apply.keys():
|
|
|
|
|
if int(_host_pending_apply[request_id].get("peer_id", 0)) == _peer_id:
|
|
|
|
|
_host_pending_apply.erase(request_id)
|
|
|
|
|
call_deferred("_refresh_latest_check")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_peer_display_name_changed(
|
|
|
|
|
_peer_id: int,
|
|
|
|
|
_display_name: String,
|
|
|
|
|
) -> void:
|
|
|
|
|
call_deferred("_refresh_latest_check")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _refresh_latest_check() -> void:
|
|
|
|
|
if (
|
|
|
|
|
_latest_check_name.is_empty()
|
|
|
|
|
or _session == null
|
|
|
|
|
or not _session.is_gameplay_session_active()
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
var peer := multiplayer.multiplayer_peer
|
|
|
|
|
if (
|
|
|
|
|
_session.is_joined_client()
|
|
|
|
|
and (
|
|
|
|
|
peer == null
|
|
|
|
|
or peer.get_connection_status()
|
|
|
|
|
!= MultiplayerPeer.CONNECTION_CONNECTED
|
|
|
|
|
)
|
|
|
|
|
):
|
|
|
|
|
return
|
|
|
|
|
request_name_check(_latest_check_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_session_state_changed(state: NetworkSession.State) -> void:
|
|
|
|
|
if state == NetworkSession.State.INACTIVE:
|
|
|
|
|
_pending_apply.clear()
|
|
|
|
|
_host_pending_apply.clear()
|
|
|
|
|
_latest_check_id = ""
|
|
|
|
|
_latest_check_name = ""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _emit_conflict_result(data: Dictionary) -> void:
|
|
|
|
|
var request_id := str(data["request_id"])
|
|
|
|
|
if request_id != _latest_check_id:
|
|
|
|
|
return
|
|
|
|
|
conflict_result.emit(
|
|
|
|
|
request_id,
|
|
|
|
|
bool(data["has_conflict"]),
|
|
|
|
|
PackedStringArray(data["suggestions"]),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _make_conflict_result(
|
|
|
|
|
peer_id: int,
|
|
|
|
|
display_name: String,
|
|
|
|
|
request_id: String,
|
|
|
|
|
) -> Dictionary:
|
|
|
|
|
var conflict := _name_conflicts(peer_id, display_name)
|
|
|
|
|
return {
|
|
|
|
|
"request_id": request_id,
|
|
|
|
|
"has_conflict": conflict,
|
|
|
|
|
"suggestions": Array(
|
|
|
|
|
_make_suggestions(peer_id, display_name)
|
|
|
|
|
if conflict else PackedStringArray()
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _name_conflicts(peer_id: int, display_name: String) -> bool:
|
|
|
|
|
var normalized := display_name.strip_edges().to_lower()
|
|
|
|
|
for other_id: int in _session.get_authenticated_peer_ids():
|
|
|
|
|
if other_id == peer_id:
|
|
|
|
|
continue
|
|
|
|
|
var record := _session.get_peer_record(other_id)
|
|
|
|
|
if record != null and record.display_name.strip_edges().to_lower() == normalized:
|
|
|
|
|
return true
|
|
|
|
|
return false
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _make_suggestions(peer_id: int, base_name: String) -> PackedStringArray:
|
|
|
|
|
var result := PackedStringArray()
|
|
|
|
|
var suffix := 2
|
|
|
|
|
while result.size() < NetworkProfileProtocol.MAX_SUGGESTIONS and suffix < 1000:
|
|
|
|
|
var suffix_text := " %d" % suffix
|
|
|
|
|
var candidate := base_name.left(
|
|
|
|
|
NetworkProtocol.MAX_DISPLAY_NAME_LENGTH - suffix_text.length()
|
|
|
|
|
) + suffix_text
|
|
|
|
|
if not _name_conflicts(peer_id, candidate) and candidate not in result:
|
|
|
|
|
result.append(candidate)
|
|
|
|
|
suffix += 1
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _apply_to_avatar(peer_id: int, appearance: Dictionary) -> void:
|
|
|
|
|
if _spawn_service == null:
|
|
|
|
|
return
|
|
|
|
|
var avatar := _spawn_service.get_avatar(peer_id)
|
|
|
|
|
if avatar != null:
|
|
|
|
|
avatar.apply_appearance_snapshot(appearance)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func _new_id() -> String:
|
|
|
|
|
return Crypto.new().generate_random_bytes(16).hex_encode()
|