class_name NetworkPlayerListService extends Node signal entries_changed signal moderation_finished(success: bool, message: String) signal friend_request_received( request_id: String, fingerprint: String, display_name: String, ) signal friend_action_finished(success: bool, message: String) const MAX_PENDING_FRIEND_REQUESTS := 16 const FRIEND_REQUEST_ID_LENGTH := 32 var _session: NetworkSession var _relationships: PlayerRelationshipStore var _bans: HostBanStore var _known: KnownPlayerStore var _spawn: PlayerSpawnService var _chat: NetworkChatService var _mail: NetworkMailService var _surface_drawing: NetworkSurfaceDrawingService var _revision := 0 var _host_block_pairs: Dictionary[String, bool] = {} var _peer_fingerprints: Dictionary[int, String] = {} var _remote_bans: Array[Dictionary] = [] var _ban_snapshot_requested: bool = false var _ban_snapshot_loaded: bool = false var _pending_outgoing_friends: Dictionary[String, Dictionary] = {} var _pending_incoming_friends: Dictionary[String, Dictionary] = {} var _host_friend_routes: Dictionary[String, Dictionary] = {} func setup( session: NetworkSession, relationships: PlayerRelationshipStore, bans: HostBanStore, known: KnownPlayerStore, spawn: PlayerSpawnService, chat: NetworkChatService, mail: NetworkMailService, ) -> void: _session = session _relationships = relationships _bans = bans _known = known _spawn = spawn _chat = chat _mail = mail _session.peer_authenticated.connect(_on_peer_authenticated) _session.peer_removed.connect(_on_peer_removed) _session.peer_display_name_changed.connect(func(_id: int, _name: String) -> void: _changed()) _session.peer_count_changed.connect( func(_count: int, _maximum: int) -> void: _sync_authenticated_peers() ) _session.state_changed.connect(_on_session_state_changed) _session.operator_status_changed.connect(_on_operator_status_changed) _relationships.relationship_changed.connect(_on_relationship_changed) _bans.bans_changed.connect(_changed) _chat.set_relationship_store(_relationships) _mail.set_relationship_policy(self) for peer_id: int in _session.get_authenticated_peer_ids(): _on_peer_authenticated(peer_id, "") func get_entries() -> Array[PlayerListEntry]: var result: Array[PlayerListEntry] = [] var local_id := _session.get_local_peer_id() for peer_id: int in _session.get_authenticated_peer_ids(): var record := _session.get_peer_record(peer_id) if record == null or not record.identity_authenticated: continue var blocked := _relationships.is_blocked(record.identity_fingerprint) if blocked and peer_id != local_id: continue var entry := PlayerListEntry.new() entry.peer_id = peer_id entry.full_fingerprint = record.identity_fingerprint entry.compact_fingerprint = NetworkIdentityCrypto.compact_suffix(record.identity_fingerprint) entry.display_name = record.display_name entry.is_host = peer_id == 1 entry.is_operator = _session.is_peer_operator(peer_id) entry.is_local_player = peer_id == local_id entry.continuity_state = _known.identity_status(record.identity_fingerprint, record.display_name) entry.ping_to_host_ms = _session.get_peer_rtt_ms(peer_id) entry.muted = _relationships.is_muted(record.identity_fingerprint) entry.blocked = blocked entry.is_friend = _relationships.is_friend(record.identity_fingerprint) entry.can_request_friend = ( not entry.is_local_player and not entry.is_friend and not entry.blocked and _session.supports_server_capability( NetworkProtocol.FRIENDS_CAPABILITY ) and _session.peer_supports_capability( peer_id, NetworkProtocol.FRIENDS_CAPABILITY ) ) entry.can_kick = ( _session.can_local_moderate() and peer_id != local_id and peer_id != 1 and (_session.is_host() or not entry.is_operator) ) entry.can_ban = entry.can_kick entry.can_clear_art = entry.can_kick entry.can_manage_operator = ( _session.can_manage_operators() and peer_id != local_id and peer_id != 1 ) entry.revision = _revision result.append(entry) result.sort_custom(_entry_before) return result func get_connected_count() -> int: return _session.get_player_count() func get_max_players() -> int: return _session.get_session_max_players() func is_local_host() -> bool: return _session.is_host() func is_local_moderator() -> bool: return _session.can_local_moderate() func can_manage_operators() -> bool: return _session.can_manage_operators() func is_open_host() -> bool: return _session.is_open_host() func set_host_open(is_open: bool) -> bool: return _session.set_host_open(is_open) func get_relationships() -> Array[Dictionary]: return _relationships.get_records() func get_friends() -> Array[Dictionary]: return _relationships.get_friends() func get_bans() -> Array[Dictionary]: if _session.is_host(): return _bans.get_bans(_session.get_host_identity_fingerprint()) if not _session.is_local_operator(): return [] _request_ban_snapshot() return _remote_bans.duplicate(true) func set_surface_drawing_service( service: NetworkSurfaceDrawingService, ) -> void: _surface_drawing = service if ( _surface_drawing != null and not _surface_drawing.session_artwork_changed.is_connected( _on_session_artwork_changed ) ): _surface_drawing.session_artwork_changed.connect( _on_session_artwork_changed ) _changed() func get_session_artwork_counts() -> Vector2i: if _surface_drawing == null: return Vector2i.ZERO return Vector2i( _surface_drawing.get_canvas_count(), _surface_drawing.get_painted_cell_count(), ) func reset_session_artwork() -> bool: if _session.is_host(): return _reset_session_artwork_on_host() if not _session.is_local_operator(): return false request_reset_session_artwork.rpc_id(1) return true func set_muted(fingerprint: String, display_name: String, value: bool) -> bool: if fingerprint == _session.get_local_identity_fingerprint(): return false return _relationships.set_muted(fingerprint, display_name, value) func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool: if fingerprint == _session.get_local_identity_fingerprint(): return false return _relationships.set_blocked(fingerprint, display_name, value) func remove_friend(fingerprint: String, display_name: String) -> bool: if fingerprint == _session.get_local_identity_fingerprint(): return false var ok := _relationships.remove_friend(fingerprint, display_name) friend_action_finished.emit( ok, "Friend removed." if ok else "Friend could not be removed.", ) return ok func send_friend_request( peer_id: int, fingerprint: String, display_name: String, ) -> bool: if ( _pending_outgoing_friends.size() >= MAX_PENDING_FRIEND_REQUESTS or fingerprint == _session.get_local_identity_fingerprint() or _relationships.is_blocked(fingerprint) or _relationships.is_friend(fingerprint) or not _valid_friend_target(peer_id, fingerprint) ): friend_action_finished.emit( false, "This person needs to be online to do this." ) return false var local_capabilities: Dictionary = ( _relationships.create_friend_capabilities() ) if local_capabilities.is_empty(): friend_action_finished.emit(false, "Friend request could not be created.") return false var request_id := NetworkIdentityCrypto.secure_id(16) _pending_outgoing_friends[request_id] = { "target_peer_id": peer_id, "target_fingerprint": fingerprint, "target_display_name": display_name, "local_capabilities": local_capabilities, } var public_capabilities := _public_friend_capabilities(local_capabilities) if _session.is_host(): _route_friend_request( _session.get_local_peer_id(), peer_id, request_id, public_capabilities ) else: request_friendship.rpc_id( 1, peer_id, request_id, public_capabilities ) friend_action_finished.emit(true, "Friend request sent.") return true func respond_friend_request(request_id: String, accepted: bool) -> bool: var pending: Dictionary = _pending_incoming_friends.get(request_id, {}) if pending.is_empty(): friend_action_finished.emit( false, "This friend request is no longer available." ) return false _pending_incoming_friends.erase(request_id) var local_capabilities: Dictionary = {} var public_capabilities: Dictionary = {} var response_accepted := accepted if accepted: local_capabilities = _relationships.create_friend_capabilities() response_accepted = ( not local_capabilities.is_empty() and _relationships.add_friend( str(pending["requester_fingerprint"]), str(pending["requester_display_name"]), local_capabilities, pending["remote_capabilities"], ) ) if response_accepted: public_capabilities = _public_friend_capabilities(local_capabilities) if _session.is_host(): _route_friend_response( _session.get_local_peer_id(), request_id, response_accepted, public_capabilities, ) else: respond_friendship.rpc_id( 1, request_id, response_accepted, public_capabilities ) friend_action_finished.emit( response_accepted or not accepted, "Friend added." if response_accepted else "Friend request declined." if not accepted else "Friend could not be saved.", ) return response_accepted or not accepted @rpc("any_peer", "call_remote", "reliable", 0) func request_friendship( target_peer_id: int, request_id: String, public_capabilities: Dictionary, ) -> void: _route_friend_request( multiplayer.get_remote_sender_id(), target_peer_id, request_id, public_capabilities, ) @rpc("authority", "call_remote", "reliable", 0) func receive_friend_request( request_id: String, requester_fingerprint: String, requester_display_name: String, public_capabilities: Dictionary, ) -> void: _receive_friend_request_local( request_id, requester_fingerprint, requester_display_name, public_capabilities, ) @rpc("any_peer", "call_remote", "reliable", 0) func respond_friendship( request_id: String, accepted: bool, public_capabilities: Dictionary, ) -> void: _route_friend_response( multiplayer.get_remote_sender_id(), request_id, accepted, public_capabilities, ) @rpc("authority", "call_remote", "reliable", 0) func receive_friend_result( request_id: String, accepted: bool, public_capabilities: Dictionary, message: String, ) -> void: _receive_friend_result_local( request_id, accepted, public_capabilities, message ) func kick(peer_id: int, fingerprint: String, revision: int) -> bool: if _session.is_host(): return _kick_on_host(peer_id, fingerprint, revision, true) if not _session.is_local_operator(): moderation_finished.emit(false, "Only the host or an operator can remove players.") return false request_kick.rpc_id(1, peer_id, fingerprint) return true func clear_art(peer_id: int, fingerprint: String, revision: int) -> bool: if _session.is_host(): return _clear_art_on_host( peer_id, fingerprint, revision, true ) if not _session.is_local_operator(): moderation_finished.emit( false, "Only the host or an operator can clear player artwork." ) return false request_clear_art.rpc_id(1, peer_id, fingerprint) return true func ban( peer_id: int, fingerprint: String, display_name: String, revision: int, ) -> bool: if _session.is_host(): return _ban_on_host( peer_id, fingerprint, display_name, revision, true ) if not _session.is_local_operator(): moderation_finished.emit(false, "Only the host or an operator can ban players.") return false request_ban.rpc_id(1, peer_id, fingerprint) return true func unban(fingerprint: String) -> bool: if _session.is_host(): var ok: bool = _bans.unban( _session.get_host_identity_fingerprint(), fingerprint ) moderation_finished.emit( ok, "Player unbanned." if ok else "Ban could not be removed." ) return ok if not _session.is_local_operator(): moderation_finished.emit(false, "Only the host or an operator can remove bans.") return false request_unban.rpc_id(1, fingerprint) return true func set_operator( peer_id: int, fingerprint: String, enabled: bool, revision: int, ) -> bool: if ( not _session.can_manage_operators() or not _valid_moderation_target( peer_id, fingerprint, revision, true, true ) ): moderation_finished.emit(false, "That player is no longer connected.") return false var ok: bool = _session.set_peer_operator(peer_id, fingerprint, enabled) moderation_finished.emit( ok, ("Player is now an operator." if enabled else "Operator access removed.") if ok else "Operator access could not be changed.", ) return ok @rpc("any_peer", "call_remote", "reliable", 0) func request_kick(peer_id: int, fingerprint: String) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if not _valid_operator_sender(sender_id): return var ok: bool = _kick_on_host(peer_id, fingerprint, -1, false) _send_moderation_result( sender_id, ok, "Player removed." if ok else "Player could not be removed." ) @rpc("any_peer", "call_remote", "reliable", 0) func request_clear_art(peer_id: int, fingerprint: String) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if not _valid_operator_sender(sender_id): return var ok: bool = _clear_art_on_host( peer_id, fingerprint, -1, false ) _send_moderation_result( sender_id, ok, "Player artwork cleared." if ok else "Player artwork could not be cleared.", ) @rpc("any_peer", "call_remote", "reliable", 0) func request_ban(peer_id: int, fingerprint: String) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if not _valid_operator_sender(sender_id): return var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) var display_name: String = record.display_name if record != null else "Player" var ok: bool = _ban_on_host( peer_id, fingerprint, display_name, -1, false ) _send_moderation_result( sender_id, ok, "Player banned." if ok else "Player could not be banned." ) _send_ban_snapshot(sender_id) @rpc("any_peer", "call_remote", "reliable", 0) func request_unban(fingerprint: String) -> void: var sender_id: int = multiplayer.get_remote_sender_id() if ( not _valid_operator_sender(sender_id) or not NetworkIdentityCrypto.valid_fingerprint(fingerprint) ): return var ok: bool = _bans.unban( _session.get_host_identity_fingerprint(), fingerprint ) _send_moderation_result( sender_id, ok, "Player unbanned." if ok else "Ban could not be removed." ) _send_ban_snapshot(sender_id) @rpc("any_peer", "call_remote", "reliable", 0) func request_reset_session_artwork() -> void: var sender_id: int = multiplayer.get_remote_sender_id() if not _valid_operator_sender(sender_id): return var ok: bool = _reset_session_artwork_on_host(false) _send_moderation_result( sender_id, ok, "Session artwork cleared." if ok else "Session artwork could not be cleared.", ) @rpc("any_peer", "call_remote", "reliable", 0) func request_ban_snapshot() -> void: var sender_id: int = multiplayer.get_remote_sender_id() if _valid_operator_sender(sender_id): _send_ban_snapshot(sender_id) @rpc("authority", "call_remote", "reliable", 0) func receive_moderation_result(success: bool, message: String) -> void: if not _session.is_joined_client(): return moderation_finished.emit(success, message.left(120)) @rpc("authority", "call_remote", "reliable", 0) func receive_ban_snapshot(records: Array) -> void: if not _session.is_local_operator() or records.size() > HostBanStore.MAX_BANS: return var sanitized: Array[Dictionary] = [] for value: Variant in records: if typeof(value) != TYPE_DICTIONARY: return var record: Dictionary = value var fingerprint: String = str(record.get("target_fingerprint", "")) var display_name: String = str( record.get("last_known_display_name", "Player") ).strip_edges().left(NetworkProtocol.MAX_DISPLAY_NAME_LENGTH) if ( not NetworkIdentityCrypto.valid_fingerprint(fingerprint) or display_name.is_empty() or typeof(record.get("banned_unix")) != TYPE_INT ): return sanitized.append({ "target_fingerprint": fingerprint, "last_known_display_name": display_name, "banned_unix": int(record["banned_unix"]), }) _remote_bans = sanitized _ban_snapshot_requested = false _ban_snapshot_loaded = true _changed() func is_locally_blocked(fingerprint: String) -> bool: return _relationships.is_blocked(fingerprint) func pair_is_blocked(first: String, second: String) -> bool: if first.is_empty() or second.is_empty(): return false return _host_block_pairs.has(_pair_key(first, second)) @rpc("any_peer", "call_remote", "reliable", 0) func submit_block_policy(target_fingerprint: String, blocked: bool) -> void: var sender := multiplayer.get_remote_sender_id() if not _session.is_host() or not _session.is_authenticated_peer(sender): return var record := _session.get_peer_record(sender) if ( record == null or not NetworkIdentityCrypto.valid_fingerprint(target_fingerprint) or record.identity_fingerprint == target_fingerprint ): return _set_host_pair(record.identity_fingerprint, target_fingerprint, blocked) func _on_relationship_changed(fingerprint: String) -> void: var blocked := _relationships.is_blocked(fingerprint) var avatar_peer := _peer_for_fingerprint(fingerprint) if avatar_peer > 0: _spawn.set_peer_presentation_visible(avatar_peer, not blocked) var local_fingerprint := _session.get_local_identity_fingerprint() if _session.is_host(): _set_host_pair(local_fingerprint, fingerprint, blocked) elif _session.is_gameplay_session_active(): submit_block_policy.rpc_id(1, fingerprint, blocked) _chat.refresh_relationship_filters() _mail.refresh_relationship_filters() _changed() func _on_peer_authenticated(peer_id: int, _display_name: String) -> void: var record := _session.get_peer_record(peer_id) if record != null: _peer_fingerprints[peer_id] = record.identity_fingerprint var blocked := _relationships.is_blocked(record.identity_fingerprint) _spawn.set_peer_presentation_visible(peer_id, not blocked) if blocked and peer_id != _session.get_local_peer_id(): var local_fingerprint := _session.get_local_identity_fingerprint() if _session.is_host(): _set_host_pair( local_fingerprint, record.identity_fingerprint, true ) else: submit_block_policy.rpc_id( 1, record.identity_fingerprint, true ) _changed() func _sync_authenticated_peers() -> void: for peer_id: int in _session.get_authenticated_peer_ids(): if not _peer_fingerprints.has(peer_id): _on_peer_authenticated(peer_id, "") _changed() func _on_peer_removed(peer_id: int) -> void: var fingerprint := str(_peer_fingerprints.get(peer_id, "")) _peer_fingerprints.erase(peer_id) if _session.is_host() and not fingerprint.is_empty(): for key: String in _host_block_pairs.keys(): if fingerprint in key.split(":"): _host_block_pairs.erase(key) for request_id: String in _host_friend_routes.keys(): var route: Dictionary = _host_friend_routes[request_id] if int(route.get("target_peer_id", 0)) == peer_id: _send_friend_result_to_peer( int(route.get("requester_peer_id", 0)), request_id, false, {}, "This person needs to be online to do this.", ) if ( int(route.get("target_peer_id", 0)) == peer_id or int(route.get("requester_peer_id", 0)) == peer_id ): _host_friend_routes.erase(request_id) for request_id: String in _pending_outgoing_friends.keys(): var pending: Dictionary = _pending_outgoing_friends[request_id] if str(pending.get("target_fingerprint", "")) == fingerprint: _pending_outgoing_friends.erase(request_id) friend_action_finished.emit( false, "This person needs to be online to do this." ) for request_id: String in _pending_incoming_friends.keys(): var pending: Dictionary = _pending_incoming_friends[request_id] if str(pending.get("requester_fingerprint", "")) == fingerprint: _pending_incoming_friends.erase(request_id) _changed() func _on_session_state_changed(state: NetworkSession.State) -> void: if state in [ NetworkSession.State.INACTIVE, NetworkSession.State.DISCONNECTING, NetworkSession.State.CONNECTION_FAILED, NetworkSession.State.SERVER_LOST, ]: _host_block_pairs.clear() _peer_fingerprints.clear() _remote_bans.clear() _ban_snapshot_requested = false _ban_snapshot_loaded = false _pending_outgoing_friends.clear() _pending_incoming_friends.clear() _host_friend_routes.clear() elif state == NetworkSession.State.JOINED_CLIENT: _request_ban_snapshot() _changed() func _on_operator_status_changed(peer_id: int, enabled: bool) -> void: if peer_id == _session.get_local_peer_id(): if enabled: _request_ban_snapshot() else: _remote_bans.clear() _ban_snapshot_requested = false _ban_snapshot_loaded = false _changed() func _set_host_pair(first: String, second: String, blocked: bool) -> void: var key := _pair_key(first, second) if blocked: _host_block_pairs[key] = true _mail.cancel_unaccepted_between(first, second) else: _host_block_pairs.erase(key) func _pair_key(first: String, second: String) -> String: return "%s:%s" % ([first, second] if first < second else [second, first]) func _peer_for_fingerprint(fingerprint: String) -> int: for peer_id: int in _session.get_authenticated_peer_ids(): var record := _session.get_peer_record(peer_id) if record != null and record.identity_fingerprint == fingerprint: return peer_id return 0 func _valid_friend_target(peer_id: int, fingerprint: String) -> bool: if ( not _session.is_gameplay_session_active() or not _session.supports_server_capability( NetworkProtocol.FRIENDS_CAPABILITY ) or not _session.peer_supports_capability( peer_id, NetworkProtocol.FRIENDS_CAPABILITY ) ): return false var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) return ( record != null and record.identity_authenticated and record.identity_fingerprint == fingerprint ) func _route_friend_request( requester_peer_id: int, target_peer_id: int, request_id: String, public_capabilities: Dictionary, ) -> void: if ( not _session.is_host() or not _valid_friend_request_id(request_id) or not _valid_public_friend_capabilities(public_capabilities) or not _session.is_authenticated_peer(requester_peer_id) ): return var requester: PeerRegistry.PeerRecord = ( _session.get_peer_record(requester_peer_id) ) var target: PeerRegistry.PeerRecord = _session.get_peer_record(target_peer_id) var can_route := ( requester != null and target != null and requester_peer_id != target_peer_id and _valid_friend_target( target_peer_id, target.identity_fingerprint ) and _session.peer_supports_capability( requester_peer_id, NetworkProtocol.FRIENDS_CAPABILITY ) and not pair_is_blocked( requester.identity_fingerprint, target.identity_fingerprint ) and _host_friend_routes.size() < ( MAX_PENDING_FRIEND_REQUESTS * 8 ) ) if not can_route: _send_friend_result_to_peer( requester_peer_id, request_id, false, {}, "This person needs to be online to do this.", ) return var requester_pending_count := 0 for route: Dictionary in _host_friend_routes.values(): if int(route.get("requester_peer_id", 0)) == requester_peer_id: requester_pending_count += 1 if requester_pending_count >= MAX_PENDING_FRIEND_REQUESTS: _send_friend_result_to_peer( requester_peer_id, request_id, false, {}, "Too many friend requests are pending.", ) return _host_friend_routes[request_id] = { "requester_peer_id": requester_peer_id, "target_peer_id": target_peer_id, } if target_peer_id == _session.get_local_peer_id(): _receive_friend_request_local( request_id, requester.identity_fingerprint, requester.display_name, public_capabilities, ) else: receive_friend_request.rpc_id( target_peer_id, request_id, requester.identity_fingerprint, requester.display_name, public_capabilities, ) func _receive_friend_request_local( request_id: String, requester_fingerprint: String, requester_display_name: String, public_capabilities: Dictionary, ) -> void: if ( not _valid_friend_request_id(request_id) or not _valid_public_friend_capabilities(public_capabilities) or not NetworkIdentityCrypto.valid_fingerprint(requester_fingerprint) or not NetworkProfilePreferences.is_valid_display_name( requester_display_name ) or _pending_incoming_friends.size() >= MAX_PENDING_FRIEND_REQUESTS or _relationships.is_blocked(requester_fingerprint) or _relationships.is_friend(requester_fingerprint) ): _decline_friend_request(request_id) return _pending_incoming_friends[request_id] = { "requester_fingerprint": requester_fingerprint, "requester_display_name": requester_display_name, "remote_capabilities": public_capabilities.duplicate(true), } friend_request_received.emit( request_id, requester_fingerprint, requester_display_name ) func _decline_friend_request(request_id: String) -> void: if _session.is_host(): _route_friend_response( _session.get_local_peer_id(), request_id, false, {} ) elif _session.is_joined_client(): respond_friendship.rpc_id(1, request_id, false, {}) func _route_friend_response( responder_peer_id: int, request_id: String, accepted: bool, public_capabilities: Dictionary, ) -> void: if not _session.is_host(): return var route: Dictionary = _host_friend_routes.get(request_id, {}) if ( route.is_empty() or int(route.get("target_peer_id", 0)) != responder_peer_id ): return _host_friend_routes.erase(request_id) var response_accepted := ( accepted and _valid_public_friend_capabilities(public_capabilities) ) _send_friend_result_to_peer( int(route["requester_peer_id"]), request_id, response_accepted, public_capabilities if response_accepted else {}, "Friend request accepted." if response_accepted else "Friend request declined." if not accepted else "Friend request could not be completed.", ) func _send_friend_result_to_peer( peer_id: int, request_id: String, accepted: bool, public_capabilities: Dictionary, message: String, ) -> void: if peer_id == _session.get_local_peer_id(): _receive_friend_result_local( request_id, accepted, public_capabilities, message ) elif _session.is_authenticated_peer(peer_id): receive_friend_result.rpc_id( peer_id, request_id, accepted, public_capabilities, message.left(120), ) func _receive_friend_result_local( request_id: String, accepted: bool, public_capabilities: Dictionary, message: String, ) -> void: var pending: Dictionary = _pending_outgoing_friends.get(request_id, {}) if pending.is_empty(): return _pending_outgoing_friends.erase(request_id) var success := false if accepted and _valid_public_friend_capabilities(public_capabilities): success = _relationships.add_friend( str(pending["target_fingerprint"]), str(pending["target_display_name"]), pending["local_capabilities"], public_capabilities, ) message = "Friend added." if success else "Friend could not be saved." friend_action_finished.emit(success, message.left(120)) func _public_friend_capabilities(capabilities: Dictionary) -> Dictionary: return { "presence_channel": str(capabilities.get("presence_channel", "")), "invite_token": str(capabilities.get("invite_token", "")), } func _valid_public_friend_capabilities(capabilities: Dictionary) -> bool: return ( _valid_friend_hex(str(capabilities.get("presence_channel", ""))) and _valid_friend_hex(str(capabilities.get("invite_token", ""))) ) func _valid_friend_request_id(request_id: String) -> bool: return _valid_friend_hex(request_id, FRIEND_REQUEST_ID_LENGTH) func _valid_friend_hex( value: String, expected_length: int = PlayerRelationshipStore.SOCIAL_TOKEN_LENGTH, ) -> bool: if value.length() != expected_length: return false for character: String in value: if character not in "0123456789abcdef": return false return true func _valid_moderation_target( peer_id: int, fingerprint: String, revision: int, require_revision: bool, can_target_operator: bool, ) -> bool: if ( not _session.is_host() or (require_revision and revision != _revision) or peer_id == 1 or (not can_target_operator and _session.is_peer_operator(peer_id)) ): return false var record := _session.get_peer_record(peer_id) return record != null and record.identity_fingerprint == fingerprint func _valid_operator_sender(peer_id: int) -> bool: return ( _session.is_host() and peer_id > 1 and _session.is_authenticated_peer(peer_id) and _session.is_peer_operator(peer_id) ) func _kick_on_host( peer_id: int, fingerprint: String, revision: int, local_host_request: bool, ) -> bool: if not _valid_moderation_target( peer_id, fingerprint, revision, local_host_request, local_host_request, ): if local_host_request: moderation_finished.emit(false, "That player is no longer connected.") return false var ok: bool = _session.kick_authenticated_peer(peer_id, fingerprint) if local_host_request: moderation_finished.emit( ok, "Player removed." if ok else "Player could not be removed." ) return ok func _ban_on_host( peer_id: int, fingerprint: String, display_name: String, revision: int, local_host_request: bool, ) -> bool: if not _valid_moderation_target( peer_id, fingerprint, revision, local_host_request, local_host_request, ): if local_host_request: moderation_finished.emit(false, "That player is no longer connected.") return false var record: PeerRegistry.PeerRecord = _session.get_peer_record(peer_id) var trusted_display_name: String = ( record.display_name if record != null else display_name ) var host_fingerprint: String = _session.get_host_identity_fingerprint() if not _bans.ban(host_fingerprint, fingerprint, trusted_display_name): if local_host_request: moderation_finished.emit(false, "Ban could not be saved.") return false if _surface_drawing != null: _surface_drawing.clear_artwork_by_fingerprint(fingerprint) var ok: bool = _session.kick_authenticated_peer(peer_id, fingerprint, true) if local_host_request: moderation_finished.emit(ok, "Player banned." if ok else "Ban saved.") return true func _clear_art_on_host( peer_id: int, fingerprint: String, revision: int, local_host_request: bool, ) -> bool: if not _valid_moderation_target( peer_id, fingerprint, revision, local_host_request, local_host_request, ): if local_host_request: moderation_finished.emit( false, "That player is no longer connected." ) return false if _surface_drawing == null: if local_host_request: moderation_finished.emit( false, "Player artwork could not be cleared." ) return false var affected_layers: int = ( _surface_drawing.clear_artwork_by_fingerprint(fingerprint) ) var ok: bool = affected_layers >= 0 if local_host_request: moderation_finished.emit( ok, "Player artwork cleared." if affected_layers > 0 else "No shared artwork from that player was found." if ok else "Player artwork could not be cleared.", ) return ok func _reset_session_artwork_on_host( emit_local_result: bool = true, ) -> bool: if not _session.is_host() or _surface_drawing == null: return false var ok: bool = _surface_drawing.clear_session_artwork() if emit_local_result: moderation_finished.emit( ok, "Session artwork cleared." if ok else "Session artwork could not be cleared.", ) return ok func _send_moderation_result( peer_id: int, success: bool, message: String, ) -> void: receive_moderation_result.rpc_id(peer_id, success, message.left(120)) func _send_ban_snapshot(peer_id: int) -> void: if not _valid_operator_sender(peer_id): return receive_ban_snapshot.rpc_id( peer_id, _bans.get_bans(_session.get_host_identity_fingerprint()), ) func _request_ban_snapshot() -> void: if ( _session.is_local_operator() and not _ban_snapshot_requested and not _ban_snapshot_loaded ): _ban_snapshot_requested = true request_ban_snapshot.rpc_id(1) func _entry_before(a: PlayerListEntry, b: PlayerListEntry) -> bool: if a.is_host != b.is_host: return a.is_host if a.is_operator != b.is_operator: return a.is_operator if a.is_local_player != b.is_local_player: return a.is_local_player var compared := a.display_name.naturalnocasecmp_to(b.display_name) return compared < 0 if compared != 0 else a.full_fingerprint < b.full_fingerprint func _changed() -> void: _revision += 1 entries_changed.emit() func _on_session_artwork_changed( _canvas_count: int, _painted_cell_count: int, ) -> void: _changed()