Add player profile and appearance foundation
This commit is contained in:
parent
e53ad1ac38
commit
0b5b78a553
25 changed files with 1424 additions and 76 deletions
37
network/network_profile_protocol.gd
Normal file
37
network/network_profile_protocol.gd
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
class_name NetworkProfileProtocol
|
||||
extends RefCounted
|
||||
|
||||
const RELIABLE_CHANNEL: int = 0
|
||||
const MAX_SUGGESTIONS: int = 3
|
||||
const MAX_REQUEST_ID_LENGTH: int = 64
|
||||
|
||||
|
||||
static func valid_request_id(value: Variant) -> bool:
|
||||
return (
|
||||
typeof(value) == TYPE_STRING
|
||||
and not str(value).is_empty()
|
||||
and str(value).length() <= MAX_REQUEST_ID_LENGTH
|
||||
)
|
||||
|
||||
|
||||
static func valid_snapshot(value: Variant) -> bool:
|
||||
return CharacterCustomizationCatalog.validate_snapshot(value)
|
||||
|
||||
|
||||
static func valid_check_request(data: Variant) -> bool:
|
||||
return (
|
||||
typeof(data) == TYPE_DICTIONARY
|
||||
and valid_request_id(data.get("request_id"))
|
||||
and typeof(data.get("session_id")) == TYPE_STRING
|
||||
and typeof(data.get("display_name")) == TYPE_STRING
|
||||
and NetworkProfilePreferences.is_valid_display_name(data["display_name"])
|
||||
)
|
||||
|
||||
|
||||
static func valid_apply_request(data: Variant) -> bool:
|
||||
return (
|
||||
valid_check_request(data)
|
||||
and typeof(data.get("appearance")) == TYPE_DICTIONARY
|
||||
and valid_snapshot(data["appearance"])
|
||||
and typeof(data.get("use_anyway")) == TYPE_BOOL
|
||||
)
|
||||
1
network/network_profile_protocol.gd.uid
Normal file
1
network/network_profile_protocol.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dhv483bfl4get
|
||||
402
network/network_profile_service.gd
Normal file
402
network/network_profile_service.gd
Normal file
|
|
@ -0,0 +1,402 @@
|
|||
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()
|
||||
|
||||
|
||||
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,
|
||||
}
|
||||
_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
|
||||
_process_apply_request(sender_id, data)
|
||||
|
||||
|
||||
func _process_apply_request(peer_id: int, data: Dictionary) -> void:
|
||||
var name := str(data["display_name"]).strip_edges()
|
||||
var conflict := _name_conflicts(peer_id, name)
|
||||
var accepted := not conflict or bool(data["use_anyway"])
|
||||
var suggestions := _make_suggestions(peer_id, name) if conflict else PackedStringArray()
|
||||
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,
|
||||
"display_name": name,
|
||||
"appearance": Dictionary(data["appearance"]).duplicate(true),
|
||||
}
|
||||
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)
|
||||
var name := str(pending["display_name"])
|
||||
var appearance := Dictionary(pending["appearance"])
|
||||
_session.apply_canonical_profile(sender_id, name, appearance)
|
||||
_apply_to_avatar(sender_id, appearance)
|
||||
broadcast_profile_snapshot.rpc(sender_id, name, appearance)
|
||||
|
||||
|
||||
@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(),
|
||||
)
|
||||
broadcast_profile_snapshot.rpc(
|
||||
_session.get_local_peer_id(),
|
||||
_preferences.display_name,
|
||||
_appearance_store.get_snapshot(),
|
||||
)
|
||||
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,
|
||||
) -> void:
|
||||
if (
|
||||
not NetworkProfilePreferences.is_valid_display_name(display_name)
|
||||
or not CharacterCustomizationCatalog.validate_snapshot(appearance)
|
||||
):
|
||||
return
|
||||
_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,
|
||||
)
|
||||
|
||||
|
||||
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()
|
||||
1
network/network_profile_service.gd.uid
Normal file
1
network/network_profile_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dc80t6nmksvm2
|
||||
|
|
@ -33,6 +33,7 @@ static func make_client_hello(
|
|||
profile_id: String,
|
||||
display_name: String,
|
||||
client_nonce: String,
|
||||
cosmetic_snapshot: Dictionary = {},
|
||||
) -> Dictionary:
|
||||
return {
|
||||
"protocol_version": PROTOCOL_VERSION,
|
||||
|
|
@ -41,7 +42,7 @@ static func make_client_hello(
|
|||
"display_name": display_name,
|
||||
"client_nonce": client_nonce,
|
||||
"capability_flags": PackedStringArray(),
|
||||
"cosmetic_snapshot": {},
|
||||
"cosmetic_snapshot": cosmetic_snapshot,
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -118,6 +119,7 @@ static func make_server_hello(
|
|||
"equipment_v1",
|
||||
"chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
]),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,11 @@ signal peer_removed(peer_id: int)
|
|||
signal host_openness_changed(is_open: bool)
|
||||
signal peer_count_changed(player_count: int, max_players: int)
|
||||
signal peer_display_name_changed(peer_id: int, display_name: String)
|
||||
signal peer_profile_changed(
|
||||
peer_id: int,
|
||||
display_name: String,
|
||||
appearance: Dictionary,
|
||||
)
|
||||
signal join_authenticated
|
||||
signal server_lost
|
||||
signal remote_recovery_requested(peer_id: int, entry_position: Vector3)
|
||||
|
|
@ -62,6 +67,9 @@ var _last_server_display_name: String = ""
|
|||
var _last_server_protocol_version: int = 0
|
||||
var _server_capabilities: PackedStringArray = PackedStringArray()
|
||||
var _profile_ready: bool = false
|
||||
var _local_appearance_snapshot: Dictionary = (
|
||||
CharacterCustomizationCatalog.default_snapshot()
|
||||
)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -116,8 +124,12 @@ func start_private_host(port: int = DEFAULT_PORT) -> bool:
|
|||
_profile.display_name,
|
||||
NetworkProtocol.PROTOCOL_VERSION
|
||||
)
|
||||
_registry.update_appearance(1, _local_appearance_snapshot)
|
||||
_spawn_service.clear_remote_players()
|
||||
_spawn_service.register_local_player(1)
|
||||
var host_avatar := _spawn_service.get_avatar(1)
|
||||
if host_avatar != null:
|
||||
host_avatar.apply_appearance_snapshot(_local_appearance_snapshot)
|
||||
_set_state(State.PRIVATE_HOST, "Private game • UDP %d" % port)
|
||||
host_openness_changed.emit(false)
|
||||
_emit_peer_count()
|
||||
|
|
@ -278,6 +290,7 @@ func supports_server_capability(capability: StringName) -> bool:
|
|||
"movement_v1", "fishing_v1", "sale_v1", "shop_v1",
|
||||
"item_use_v1", "equipment_v1", "chat_v1",
|
||||
"mail_v1",
|
||||
"profile_v1",
|
||||
])
|
||||
return str(capability) in _server_capabilities
|
||||
|
||||
|
|
@ -302,6 +315,33 @@ func update_local_display_name(value: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func set_local_appearance_snapshot(snapshot: Dictionary) -> void:
|
||||
if CharacterCustomizationCatalog.validate_snapshot(snapshot):
|
||||
_local_appearance_snapshot = snapshot.duplicate(true)
|
||||
var local_id := get_local_peer_id()
|
||||
if local_id > 0:
|
||||
_registry.update_appearance(local_id, _local_appearance_snapshot)
|
||||
|
||||
|
||||
func apply_canonical_profile(
|
||||
peer_id: int,
|
||||
display_name: String,
|
||||
appearance: Dictionary,
|
||||
) -> bool:
|
||||
if (
|
||||
not NetworkProfilePreferences.is_valid_display_name(display_name)
|
||||
or not CharacterCustomizationCatalog.validate_snapshot(appearance)
|
||||
):
|
||||
return false
|
||||
var name_changed := _registry.update_display_name(peer_id, display_name)
|
||||
var appearance_changed := _registry.update_appearance(peer_id, appearance)
|
||||
if name_changed:
|
||||
peer_display_name_changed.emit(peer_id, display_name)
|
||||
if name_changed and appearance_changed:
|
||||
peer_profile_changed.emit(peer_id, display_name, appearance.duplicate(true))
|
||||
return name_changed and appearance_changed
|
||||
|
||||
|
||||
@rpc("any_peer", "call_remote", "reliable", 0)
|
||||
func submit_display_name(value: String) -> void:
|
||||
var sender_id := multiplayer.get_remote_sender_id()
|
||||
|
|
@ -398,7 +438,8 @@ func _on_connected_to_server() -> void:
|
|||
var hello: Dictionary = NetworkProtocol.make_client_hello(
|
||||
_profile.profile_id,
|
||||
_profile.display_name,
|
||||
_client_nonce
|
||||
_client_nonce,
|
||||
_local_appearance_snapshot,
|
||||
)
|
||||
submit_client_hello.rpc_id(1, hello)
|
||||
|
||||
|
|
@ -477,6 +518,10 @@ func submit_client_hello(data: Dictionary) -> void:
|
|||
NetworkProtocol.RejectionCode.MALFORMED_HANDSHAKE
|
||||
)
|
||||
return
|
||||
var submitted_appearance := CharacterCustomizationCatalog.sanitized_snapshot(
|
||||
data["cosmetic_snapshot"]
|
||||
)
|
||||
_registry.update_appearance(sender_id, submitted_appearance)
|
||||
_pending_authentication.erase(sender_id)
|
||||
var spawn_index: int = _registry.get_peer_ids().find(sender_id)
|
||||
var spawn_transform: Transform3D = (
|
||||
|
|
@ -582,8 +627,12 @@ func receive_server_hello(data: Dictionary) -> void:
|
|||
_profile.display_name,
|
||||
NetworkProtocol.PROTOCOL_VERSION
|
||||
)
|
||||
_registry.update_appearance(local_peer_id, _local_appearance_snapshot)
|
||||
_spawn_service.clear_remote_players()
|
||||
_spawn_service.register_local_player(local_peer_id)
|
||||
var local_avatar := _spawn_service.get_avatar(local_peer_id)
|
||||
if local_avatar != null:
|
||||
local_avatar.apply_appearance_snapshot(_local_appearance_snapshot)
|
||||
_connection_deadline = 0.0
|
||||
_set_state(
|
||||
State.JOINED_CLIENT,
|
||||
|
|
@ -652,6 +701,11 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
|
|||
entry["display_name"],
|
||||
NetworkProtocol.PROTOCOL_VERSION
|
||||
)
|
||||
if typeof(entry.get("appearance")) == TYPE_DICTIONARY:
|
||||
var appearance := CharacterCustomizationCatalog.sanitized_snapshot(
|
||||
entry["appearance"]
|
||||
)
|
||||
_registry.update_appearance(peer_id, appearance)
|
||||
var transform: Transform3D = _spawn_service.get_spawn_transform_for_index(0)
|
||||
var position: Array = entry["position"]
|
||||
if position.size() != 3:
|
||||
|
|
@ -662,7 +716,10 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
|
|||
float(position[2])
|
||||
)
|
||||
transform.basis = Basis(Vector3.UP, float(entry["yaw"]))
|
||||
_spawn_service.spawn_remote_player(peer_id, transform, false)
|
||||
var avatar := _spawn_service.spawn_remote_player(peer_id, transform, false)
|
||||
var record := _registry.get_peer(peer_id)
|
||||
if avatar != null and record != null:
|
||||
avatar.apply_appearance_snapshot(record.appearance_snapshot)
|
||||
|
||||
|
||||
func _build_spawn_list() -> Array[Dictionary]:
|
||||
|
|
@ -695,6 +752,11 @@ func _make_spawn_entry(
|
|||
transform.origin.z,
|
||||
],
|
||||
"yaw": transform.basis.get_euler().y,
|
||||
"appearance": (
|
||||
record.appearance_snapshot.duplicate(true)
|
||||
if record != null
|
||||
else CharacterCustomizationCatalog.default_snapshot()
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,9 @@ class PeerRecord:
|
|||
var display_name: String = ""
|
||||
var protocol_version: int = 0
|
||||
var joined_at_unix: int = 0
|
||||
var appearance_snapshot: Dictionary = (
|
||||
CharacterCustomizationCatalog.default_snapshot()
|
||||
)
|
||||
|
||||
|
||||
var _records: Dictionary[int, PeerRecord] = {}
|
||||
|
|
@ -50,6 +53,14 @@ func update_display_name(peer_id: int, display_name: String) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func update_appearance(peer_id: int, snapshot: Dictionary) -> bool:
|
||||
var record: PeerRecord = _records.get(peer_id)
|
||||
if record == null or not CharacterCustomizationCatalog.validate_snapshot(snapshot):
|
||||
return false
|
||||
record.appearance_snapshot = snapshot.duplicate(true)
|
||||
return true
|
||||
|
||||
|
||||
func has_peer(peer_id: int) -> bool:
|
||||
return _records.has(peer_id)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue