2026-07-29 23:10:51 -04:00
|
|
|
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
|
2026-08-01 22:55:39 -04:00
|
|
|
var _surface_drawing: NetworkSurfaceDrawingService
|
2026-07-29 23:10:51 -04:00
|
|
|
var _revision := 0
|
|
|
|
|
var _host_block_pairs: Dictionary[String, bool] = {}
|
|
|
|
|
var _peer_fingerprints: Dictionary[int, String] = {}
|
2026-08-11 22:58:33 -04:00
|
|
|
var _remote_bans: Array[Dictionary] = []
|
|
|
|
|
var _ban_snapshot_requested: bool = false
|
|
|
|
|
var _ban_snapshot_loaded: bool = false
|
2026-07-29 23:10:51 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
2026-08-11 22:58:33 -04:00
|
|
|
_session.operator_status_changed.connect(_on_operator_status_changed)
|
2026-07-29 23:10:51 -04:00
|
|
|
_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
|
2026-08-11 22:58:33 -04:00
|
|
|
entry.is_operator = _session.is_peer_operator(peer_id)
|
2026-07-29 23:10:51 -04:00
|
|
|
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
|
2026-08-11 22:58:33 -04:00
|
|
|
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)
|
|
|
|
|
)
|
2026-07-29 23:10:51 -04:00
|
|
|
entry.can_ban = entry.can_kick
|
2026-08-11 22:58:33 -04:00
|
|
|
entry.can_manage_operator = (
|
|
|
|
|
_session.can_manage_operators()
|
|
|
|
|
and peer_id != local_id
|
|
|
|
|
and peer_id != 1
|
|
|
|
|
)
|
2026-07-29 23:10:51 -04:00
|
|
|
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()
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 22:58:33 -04:00
|
|
|
func is_local_moderator() -> bool:
|
|
|
|
|
return _session.can_local_moderate()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func can_manage_operators() -> bool:
|
|
|
|
|
return _session.can_manage_operators()
|
|
|
|
|
|
|
|
|
|
|
2026-08-10 10:04:05 -04:00
|
|
|
func is_open_host() -> bool:
|
|
|
|
|
return _session.is_open_host()
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 16:35:33 -04:00
|
|
|
func set_host_open(is_open: bool) -> bool:
|
|
|
|
|
return _session.set_host_open(is_open)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 23:10:51 -04:00
|
|
|
func get_relationships() -> Array[Dictionary]:
|
|
|
|
|
return _relationships.get_records()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func get_bans() -> Array[Dictionary]:
|
2026-08-11 22:58:33 -04:00
|
|
|
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)
|
2026-07-29 23:10:51 -04:00
|
|
|
|
|
|
|
|
|
2026-08-01 22:55:39 -04:00
|
|
|
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:
|
2026-08-11 22:58:33 -04:00
|
|
|
if _session.is_host():
|
|
|
|
|
return _reset_session_artwork_on_host()
|
|
|
|
|
if not _session.is_local_operator():
|
2026-08-01 22:55:39 -04:00
|
|
|
return false
|
2026-08-11 22:58:33 -04:00
|
|
|
request_reset_session_artwork.rpc_id(1)
|
|
|
|
|
return true
|
2026-08-01 22:55:39 -04:00
|
|
|
|
|
|
|
|
|
2026-07-29 23:10:51 -04:00
|
|
|
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:
|
2026-08-11 22:58:33 -04:00
|
|
|
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.")
|
2026-07-29 23:10:51 -04:00
|
|
|
return false
|
2026-08-11 22:58:33 -04:00
|
|
|
request_kick.rpc_id(1, peer_id, fingerprint)
|
|
|
|
|
return true
|
2026-07-29 23:10:51 -04:00
|
|
|
|
|
|
|
|
|
2026-07-30 00:54:53 -04:00
|
|
|
func ban(
|
|
|
|
|
peer_id: int,
|
|
|
|
|
fingerprint: String,
|
|
|
|
|
display_name: String,
|
|
|
|
|
revision: int,
|
|
|
|
|
) -> bool:
|
2026-08-11 22:58:33 -04:00
|
|
|
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.")
|
2026-07-29 23:10:51 -04:00
|
|
|
return false
|
2026-08-11 22:58:33 -04:00
|
|
|
request_ban.rpc_id(1, peer_id, fingerprint)
|
2026-07-29 23:10:51 -04:00
|
|
|
return true
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func unban(fingerprint: String) -> bool:
|
2026-08-11 22:58:33 -04:00
|
|
|
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.")
|
2026-07-29 23:10:51 -04:00
|
|
|
return false
|
2026-08-11 22:58:33 -04:00
|
|
|
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_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()
|
2026-07-29 23:10:51 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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()
|
2026-08-11 22:58:33 -04:00
|
|
|
_remote_bans.clear()
|
|
|
|
|
_ban_snapshot_requested = false
|
|
|
|
|
_ban_snapshot_loaded = false
|
|
|
|
|
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
|
2026-07-29 23:10:51 -04:00
|
|
|
_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
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 22:58:33 -04:00
|
|
|
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))
|
|
|
|
|
):
|
2026-07-29 23:10:51 -04:00
|
|
|
return false
|
|
|
|
|
var record := _session.get_peer_record(peer_id)
|
|
|
|
|
return record != null and record.identity_fingerprint == fingerprint
|
|
|
|
|
|
|
|
|
|
|
2026-08-11 22:58:33 -04:00
|
|
|
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
|
|
|
|
|
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 _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)
|
|
|
|
|
|
|
|
|
|
|
2026-07-29 23:10:51 -04:00
|
|
|
func _entry_before(a: PlayerListEntry, b: PlayerListEntry) -> bool:
|
|
|
|
|
if a.is_host != b.is_host:
|
|
|
|
|
return a.is_host
|
2026-08-11 22:58:33 -04:00
|
|
|
if a.is_operator != b.is_operator:
|
|
|
|
|
return a.is_operator
|
2026-07-29 23:10:51 -04:00
|
|
|
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()
|
2026-08-01 22:55:39 -04:00
|
|
|
|
|
|
|
|
|
|
|
|
|
func _on_session_artwork_changed(
|
|
|
|
|
_canvas_count: int,
|
|
|
|
|
_painted_cell_count: int,
|
|
|
|
|
) -> void:
|
|
|
|
|
_changed()
|