class_name NetworkPlayerListService extends Node signal entries_changed signal moderation_finished(success: bool, message: String) 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] = {} 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) _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_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.can_kick = _session.is_host() and peer_id != local_id and peer_id != 1 entry.can_ban = entry.can_kick 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_open_host() -> bool: return _session.is_open_host() func get_relationships() -> Array[Dictionary]: return _relationships.get_records() func get_bans() -> Array[Dictionary]: return _bans.get_bans(_session.get_host_identity_fingerprint()) if _session.is_host() else [] 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 not _session.is_host() or _surface_drawing == null: return false var ok: bool = _surface_drawing.clear_session_artwork() moderation_finished.emit( ok, "Session artwork cleared." if ok else "Session artwork could not be cleared.", ) return ok 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 kick(peer_id: int, fingerprint: String, revision: int) -> bool: if not _valid_moderation_target(peer_id, fingerprint, revision): moderation_finished.emit(false, "That player is no longer connected.") return false var ok := _session.kick_authenticated_peer(peer_id, fingerprint) moderation_finished.emit(ok, "Player removed." if ok else "Player could not be removed.") return ok func ban( peer_id: int, fingerprint: String, display_name: String, revision: int, ) -> bool: if not _valid_moderation_target(peer_id, fingerprint, revision): moderation_finished.emit(false, "That player is no longer connected.") return false var host_fingerprint := _session.get_host_identity_fingerprint() if not _bans.ban(host_fingerprint, fingerprint, display_name): moderation_finished.emit(false, "Ban could not be saved.") return false var ok := _session.kick_authenticated_peer(peer_id, fingerprint, true) moderation_finished.emit(ok, "Player banned." if ok else "Ban saved.") return true func unban(fingerprint: String) -> bool: if not _session.is_host(): return false return _bans.unban(_session.get_host_identity_fingerprint(), fingerprint) 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) _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() _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_moderation_target(peer_id: int, fingerprint: String, revision: int) -> bool: if not _session.is_host() or revision != _revision or peer_id == 1: return false var record := _session.get_peer_record(peer_id) return record != null and record.identity_fingerprint == fingerprint func _entry_before(a: PlayerListEntry, b: PlayerListEntry) -> bool: if a.is_host != b.is_host: return a.is_host 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()