straywild/network/network_player_list_service.gd
Voyager 3b84bfe3a0 feat: expand progression and multiplayer systems
Add named save slots, progression import/export, and a unified play flow. Add live friend requests, presence, invitations, and relationship controls without durable discovery-server social storage. Advance the network protocol with isolated channels, movement reconciliation, late-join recovery, fishing replication, and animation synchronization. Preserve per-species catch totals, refine generated-world startup and water recovery, and complete the related input and interface improvements.
2026-08-23 20:49:54 -04:00

1147 lines
32 KiB
GDScript

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()