Add players page and host moderation
This commit is contained in:
parent
fa996a6735
commit
141b54261d
23 changed files with 1166 additions and 31 deletions
|
|
@ -70,6 +70,10 @@ func cleanup() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _process(_delta: float) -> void:
|
func _process(_delta: float) -> void:
|
||||||
|
if _owner != null and not _owner.is_remote_presentation_visible():
|
||||||
|
_bobber.visible = false
|
||||||
|
_line.visible = false
|
||||||
|
return
|
||||||
if _active:
|
if _active:
|
||||||
_redraw_line()
|
_redraw_line()
|
||||||
|
|
||||||
|
|
|
||||||
16
main/main.gd
16
main/main.gd
|
|
@ -104,6 +104,8 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
@onready var _host_identity: HostIdentityStore = %HostIdentityStore
|
@onready var _host_identity: HostIdentityStore = %HostIdentityStore
|
||||||
@onready var _known_players: KnownPlayerStore = %KnownPlayerStore
|
@onready var _known_players: KnownPlayerStore = %KnownPlayerStore
|
||||||
@onready var _server_trust: ServerTrustStore = %ServerTrustStore
|
@onready var _server_trust: ServerTrustStore = %ServerTrustStore
|
||||||
|
@onready var _relationships: PlayerRelationshipStore = %PlayerRelationshipStore
|
||||||
|
@onready var _host_bans: HostBanStore = %HostBanStore
|
||||||
@onready var _saved_servers: SavedServerStoreType = %SavedServerStore
|
@onready var _saved_servers: SavedServerStoreType = %SavedServerStore
|
||||||
@onready var _player_spawn_service: PlayerSpawnServiceType = (
|
@onready var _player_spawn_service: PlayerSpawnServiceType = (
|
||||||
%PlayerSpawnService
|
%PlayerSpawnService
|
||||||
|
|
@ -118,6 +120,7 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
|
||||||
)
|
)
|
||||||
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
|
@onready var _network_chat: NetworkChatServiceType = %NetworkChatService
|
||||||
@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
|
@onready var _network_mail: NetworkMailServiceType = %NetworkMailService
|
||||||
|
@onready var _network_player_list: NetworkPlayerListService = %NetworkPlayerListService
|
||||||
@onready var _asset_reservations: PlayerAssetReservationServiceType = (
|
@onready var _asset_reservations: PlayerAssetReservationServiceType = (
|
||||||
%PlayerAssetReservationService
|
%PlayerAssetReservationService
|
||||||
)
|
)
|
||||||
|
|
@ -157,6 +160,7 @@ func _ready() -> void:
|
||||||
_host_identity,
|
_host_identity,
|
||||||
_known_players,
|
_known_players,
|
||||||
_server_trust,
|
_server_trust,
|
||||||
|
_host_bans,
|
||||||
)
|
)
|
||||||
_network_profile_service.setup(
|
_network_profile_service.setup(
|
||||||
_network_session,
|
_network_session,
|
||||||
|
|
@ -164,6 +168,15 @@ func _ready() -> void:
|
||||||
_appearance_store,
|
_appearance_store,
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
)
|
)
|
||||||
|
_network_player_list.setup(
|
||||||
|
_network_session,
|
||||||
|
_relationships,
|
||||||
|
_host_bans,
|
||||||
|
_known_players,
|
||||||
|
_player_spawn_service,
|
||||||
|
_network_chat,
|
||||||
|
_network_mail,
|
||||||
|
)
|
||||||
_network_session.set_local_appearance_snapshot(
|
_network_session.set_local_appearance_snapshot(
|
||||||
_appearance_store.get_snapshot()
|
_appearance_store.get_snapshot()
|
||||||
)
|
)
|
||||||
|
|
@ -310,7 +323,8 @@ func _ready() -> void:
|
||||||
_player_spawn_service,
|
_player_spawn_service,
|
||||||
_network_mail,
|
_network_mail,
|
||||||
_asset_reservations,
|
_asset_reservations,
|
||||||
_network_profile_service
|
_network_profile_service,
|
||||||
|
_network_player_list,
|
||||||
)
|
)
|
||||||
_water_recovery.setup(
|
_water_recovery.setup(
|
||||||
_player,
|
_player,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=34 format=3]
|
[gd_scene load_steps=37 format=3]
|
||||||
|
|
||||||
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
|
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
|
||||||
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
|
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
|
||||||
|
|
@ -33,6 +33,9 @@
|
||||||
[ext_resource type="Script" path="res://network/host_identity_store.gd" id="31_host_identity"]
|
[ext_resource type="Script" path="res://network/host_identity_store.gd" id="31_host_identity"]
|
||||||
[ext_resource type="Script" path="res://network/known_player_store.gd" id="32_known_players"]
|
[ext_resource type="Script" path="res://network/known_player_store.gd" id="32_known_players"]
|
||||||
[ext_resource type="Script" path="res://network/server_trust_store.gd" id="33_server_trust"]
|
[ext_resource type="Script" path="res://network/server_trust_store.gd" id="33_server_trust"]
|
||||||
|
[ext_resource type="Script" path="res://network/player_relationship_store.gd" id="34_relationships"]
|
||||||
|
[ext_resource type="Script" path="res://network/host_ban_store.gd" id="35_bans"]
|
||||||
|
[ext_resource type="Script" path="res://network/network_player_list_service.gd" id="36_player_list"]
|
||||||
|
|
||||||
[node name="Main" type="Node3D"]
|
[node name="Main" type="Node3D"]
|
||||||
script = ExtResource("3_main")
|
script = ExtResource("3_main")
|
||||||
|
|
@ -65,6 +68,14 @@ script = ExtResource("32_known_players")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("33_server_trust")
|
script = ExtResource("33_server_trust")
|
||||||
|
|
||||||
|
[node name="PlayerRelationshipStore" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("34_relationships")
|
||||||
|
|
||||||
|
[node name="HostBanStore" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("35_bans")
|
||||||
|
|
||||||
[node name="PlayerAppearanceStore" type="Node" parent="."]
|
[node name="PlayerAppearanceStore" type="Node" parent="."]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("28_appearance")
|
script = ExtResource("28_appearance")
|
||||||
|
|
@ -109,6 +120,10 @@ script = ExtResource("27_reservations")
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
script = ExtResource("26_network_mail")
|
script = ExtResource("26_network_mail")
|
||||||
|
|
||||||
|
[node name="NetworkPlayerListService" type="Node" parent="."]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
script = ExtResource("36_player_list")
|
||||||
|
|
||||||
[node name="TestWorld" parent="." instance=ExtResource("1_world")]
|
[node name="TestWorld" parent="." instance=ExtResource("1_world")]
|
||||||
|
|
||||||
[node name="Players" type="Node3D" parent="."]
|
[node name="Players" type="Node3D" parent="."]
|
||||||
|
|
|
||||||
132
network/host_ban_store.gd
Normal file
132
network/host_ban_store.gd
Normal file
|
|
@ -0,0 +1,132 @@
|
||||||
|
class_name HostBanStore
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal bans_changed
|
||||||
|
|
||||||
|
const FORMAT_VERSION := 1
|
||||||
|
const STORE_PATH := "user://host_bans.json"
|
||||||
|
const TEMP_PATH := STORE_PATH + ".tmp"
|
||||||
|
const MAX_BANS := 500
|
||||||
|
|
||||||
|
var _namespaces: Dictionary = {}
|
||||||
|
var _loaded := false
|
||||||
|
var _write_blocked := false
|
||||||
|
|
||||||
|
|
||||||
|
func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
|
||||||
|
_ensure_loaded()
|
||||||
|
return Dictionary(_namespaces.get(host_fingerprint, {})).has(target_fingerprint)
|
||||||
|
|
||||||
|
|
||||||
|
func ban(host_fingerprint: String, target_fingerprint: String, name: String) -> bool:
|
||||||
|
if (
|
||||||
|
not NetworkIdentityCrypto.valid_fingerprint(host_fingerprint)
|
||||||
|
or not NetworkIdentityCrypto.valid_fingerprint(target_fingerprint)
|
||||||
|
or not NetworkProfilePreferences.is_valid_display_name(name)
|
||||||
|
):
|
||||||
|
return false
|
||||||
|
_ensure_loaded()
|
||||||
|
if _write_blocked:
|
||||||
|
return false
|
||||||
|
var previous_namespace: Dictionary = Dictionary(
|
||||||
|
_namespaces.get(host_fingerprint, {})
|
||||||
|
).duplicate(true)
|
||||||
|
var records: Dictionary = _namespaces.get(host_fingerprint, {})
|
||||||
|
records[target_fingerprint] = {
|
||||||
|
"host_fingerprint": host_fingerprint,
|
||||||
|
"target_fingerprint": target_fingerprint,
|
||||||
|
"last_known_display_name": name,
|
||||||
|
"banned_unix": int(Time.get_unix_time_from_system()),
|
||||||
|
"reason": "host_ban",
|
||||||
|
}
|
||||||
|
while records.size() > MAX_BANS:
|
||||||
|
records.erase(records.keys().front())
|
||||||
|
_namespaces[host_fingerprint] = records
|
||||||
|
if not _save():
|
||||||
|
_namespaces[host_fingerprint] = previous_namespace
|
||||||
|
return false
|
||||||
|
bans_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func unban(host_fingerprint: String, target_fingerprint: String) -> bool:
|
||||||
|
_ensure_loaded()
|
||||||
|
if _write_blocked:
|
||||||
|
return false
|
||||||
|
var previous_namespace: Dictionary = Dictionary(
|
||||||
|
_namespaces.get(host_fingerprint, {})
|
||||||
|
).duplicate(true)
|
||||||
|
var records: Dictionary = _namespaces.get(host_fingerprint, {})
|
||||||
|
records.erase(target_fingerprint)
|
||||||
|
_namespaces[host_fingerprint] = records
|
||||||
|
if not _save():
|
||||||
|
_namespaces[host_fingerprint] = previous_namespace
|
||||||
|
return false
|
||||||
|
bans_changed.emit()
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func get_bans(host_fingerprint: String) -> Array[Dictionary]:
|
||||||
|
_ensure_loaded()
|
||||||
|
var result: Array[Dictionary] = []
|
||||||
|
for value: Dictionary in Dictionary(_namespaces.get(host_fingerprint, {})).values():
|
||||||
|
result.append(value.duplicate(true))
|
||||||
|
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return str(a["last_known_display_name"]).naturalnocasecmp_to(
|
||||||
|
str(b["last_known_display_name"])
|
||||||
|
) < 0
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_loaded() -> void:
|
||||||
|
if _loaded:
|
||||||
|
return
|
||||||
|
_loaded = true
|
||||||
|
if not FileAccess.file_exists(STORE_PATH):
|
||||||
|
return
|
||||||
|
var file := FileAccess.open(STORE_PATH, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
var json := JSON.new()
|
||||||
|
if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var data: Dictionary = json.data
|
||||||
|
if data.get("format_version") != FORMAT_VERSION:
|
||||||
|
_write_blocked = true
|
||||||
|
return
|
||||||
|
for value: Variant in data.get("records", []):
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var record: Dictionary = value
|
||||||
|
var host := str(record.get("host_fingerprint", ""))
|
||||||
|
var target := str(record.get("target_fingerprint", ""))
|
||||||
|
if not NetworkIdentityCrypto.valid_fingerprint(host) or not NetworkIdentityCrypto.valid_fingerprint(target):
|
||||||
|
continue
|
||||||
|
var records: Dictionary = _namespaces.get(host, {})
|
||||||
|
records[target] = record.duplicate(true)
|
||||||
|
_namespaces[host] = records
|
||||||
|
|
||||||
|
|
||||||
|
func _save() -> bool:
|
||||||
|
var values: Array = []
|
||||||
|
for records: Dictionary in _namespaces.values():
|
||||||
|
values.append_array(records.values())
|
||||||
|
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
return false
|
||||||
|
file.store_string(JSON.stringify({
|
||||||
|
"format_version": FORMAT_VERSION,
|
||||||
|
"records": values,
|
||||||
|
}, "\t"))
|
||||||
|
file.flush()
|
||||||
|
var ok := file.get_error() == OK
|
||||||
|
file.close()
|
||||||
|
if not ok:
|
||||||
|
return false
|
||||||
|
if FileAccess.file_exists(STORE_PATH):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(STORE_PATH))
|
||||||
|
return DirAccess.rename_absolute(
|
||||||
|
ProjectSettings.globalize_path(TEMP_PATH),
|
||||||
|
ProjectSettings.globalize_path(STORE_PATH),
|
||||||
|
) == OK
|
||||||
1
network/host_ban_store.gd.uid
Normal file
1
network/host_ban_store.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c71hostbans000001
|
||||||
|
|
@ -16,6 +16,7 @@ var _request_ledgers: Dictionary[int, Dictionary] = {}
|
||||||
var _rate_times: Dictionary[int, Array] = {}
|
var _rate_times: Dictionary[int, Array] = {}
|
||||||
var _sequence: int = 0
|
var _sequence: int = 0
|
||||||
var _peer_names: Dictionary[int, String] = {}
|
var _peer_names: Dictionary[int, String] = {}
|
||||||
|
var _relationships: PlayerRelationshipStore
|
||||||
|
|
||||||
|
|
||||||
func setup(session: NetworkSession) -> void:
|
func setup(session: NetworkSession) -> void:
|
||||||
|
|
@ -29,6 +30,21 @@ func setup(session: NetworkSession) -> void:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func set_relationship_store(store: PlayerRelationshipStore) -> void:
|
||||||
|
_relationships = store
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_relationship_filters() -> void:
|
||||||
|
history_replaced.emit(get_history())
|
||||||
|
|
||||||
|
|
||||||
|
func is_sender_filtered(fingerprint: String) -> bool:
|
||||||
|
return (
|
||||||
|
_relationships != null
|
||||||
|
and _relationships.is_muted(fingerprint)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func send_local_message(body: String) -> bool:
|
func send_local_message(body: String) -> bool:
|
||||||
if (
|
if (
|
||||||
_session == null
|
_session == null
|
||||||
|
|
@ -63,7 +79,11 @@ func send_local_message(body: String) -> bool:
|
||||||
|
|
||||||
|
|
||||||
func get_history() -> Array[Dictionary]:
|
func get_history() -> Array[Dictionary]:
|
||||||
return _history.duplicate(true)
|
var result: Array[Dictionary] = []
|
||||||
|
for message: Dictionary in _history:
|
||||||
|
if _message_is_visible(message):
|
||||||
|
result.append(message.duplicate(true))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
@rpc("any_peer", "call_remote", "reliable", NetworkChatProtocol.RELIABLE_CHANNEL)
|
||||||
|
|
@ -193,6 +213,7 @@ func _apply_message(data: Dictionary) -> void:
|
||||||
_history.append(data.duplicate(true))
|
_history.append(data.duplicate(true))
|
||||||
while _history.size() > NetworkChatProtocol.MAX_HISTORY:
|
while _history.size() > NetworkChatProtocol.MAX_HISTORY:
|
||||||
_history.pop_front()
|
_history.pop_front()
|
||||||
|
if _message_is_visible(data):
|
||||||
message_received.emit(data.duplicate(true))
|
message_received.emit(data.duplicate(true))
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -240,7 +261,17 @@ func receive_chat_history(values: Array) -> void:
|
||||||
for value: Variant in values:
|
for value: Variant in values:
|
||||||
if NetworkChatProtocol.validate_message(value):
|
if NetworkChatProtocol.validate_message(value):
|
||||||
_apply_message(value)
|
_apply_message(value)
|
||||||
history_replaced.emit(_history.duplicate(true))
|
history_replaced.emit(get_history())
|
||||||
|
|
||||||
|
|
||||||
|
func _message_is_visible(message: Dictionary) -> bool:
|
||||||
|
if int(message.get("kind", -1)) == NetworkChatProtocol.Kind.SYSTEM:
|
||||||
|
return true
|
||||||
|
if _relationships == null:
|
||||||
|
return true
|
||||||
|
return not _relationships.is_muted(
|
||||||
|
str(message.get("sender_fingerprint", ""))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _consume_rate(peer_id: int) -> bool:
|
func _consume_rate(peer_id: int) -> bool:
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ var _pending_local_send: Dictionary = {}
|
||||||
var _pending_transfers: Dictionary[String, Dictionary] = {}
|
var _pending_transfers: Dictionary[String, Dictionary] = {}
|
||||||
var _local_removal_snapshots: Dictionary[String, Dictionary] = {}
|
var _local_removal_snapshots: Dictionary[String, Dictionary] = {}
|
||||||
var _received_awards: Dictionary[String, bool] = {}
|
var _received_awards: Dictionary[String, bool] = {}
|
||||||
|
var _relationship_policy: NetworkPlayerListService
|
||||||
|
|
||||||
|
|
||||||
func setup(
|
func setup(
|
||||||
|
|
@ -62,10 +63,22 @@ func setup(
|
||||||
_session.state_changed.connect(_on_session_state_changed)
|
_session.state_changed.connect(_on_session_state_changed)
|
||||||
|
|
||||||
|
|
||||||
|
func set_relationship_policy(policy: NetworkPlayerListService) -> void:
|
||||||
|
_relationship_policy = policy
|
||||||
|
|
||||||
|
|
||||||
|
func refresh_relationship_filters() -> void:
|
||||||
|
_emit_mailbox()
|
||||||
|
peers_changed.emit()
|
||||||
|
|
||||||
|
|
||||||
func get_local_letters() -> Array[Dictionary]:
|
func get_local_letters() -> Array[Dictionary]:
|
||||||
var values: Array[Dictionary] = []
|
var values: Array[Dictionary] = []
|
||||||
for letter: Dictionary in _local_letters.values():
|
for letter: Dictionary in _local_letters.values():
|
||||||
if int(letter["recipient_peer_id"]) == _session.get_local_peer_id():
|
if (
|
||||||
|
int(letter["recipient_peer_id"]) == _session.get_local_peer_id()
|
||||||
|
and not _letter_is_locally_blocked(letter)
|
||||||
|
):
|
||||||
values.append(letter.duplicate(true))
|
values.append(letter.duplicate(true))
|
||||||
values.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
values.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
var a_unread := int(a["state"]) == NetworkMailProtocol.State.SENT_UNREAD
|
var a_unread := int(a["state"]) == NetworkMailProtocol.State.SENT_UNREAD
|
||||||
|
|
@ -87,6 +100,7 @@ func get_unread_count() -> int:
|
||||||
if (
|
if (
|
||||||
int(letter["recipient_peer_id"]) == _session.get_local_peer_id()
|
int(letter["recipient_peer_id"]) == _session.get_local_peer_id()
|
||||||
and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD
|
and int(letter["state"]) == NetworkMailProtocol.State.SENT_UNREAD
|
||||||
|
and not _letter_is_locally_blocked(letter)
|
||||||
):
|
):
|
||||||
count += 1
|
count += 1
|
||||||
return count
|
return count
|
||||||
|
|
@ -111,7 +125,12 @@ func get_recipient_choices() -> Array[Dictionary]:
|
||||||
if peer_id == local_id:
|
if peer_id == local_id:
|
||||||
continue
|
continue
|
||||||
var record := _session.get_peer_record(peer_id)
|
var record := _session.get_peer_record(peer_id)
|
||||||
if record != null:
|
if record != null and (
|
||||||
|
_relationship_policy == null
|
||||||
|
or not _relationship_policy.is_locally_blocked(
|
||||||
|
record.identity_fingerprint
|
||||||
|
)
|
||||||
|
):
|
||||||
choices.append({
|
choices.append({
|
||||||
"peer_id": peer_id,
|
"peer_id": peer_id,
|
||||||
"name": record.display_name,
|
"name": record.display_name,
|
||||||
|
|
@ -148,6 +167,12 @@ func send_letter(
|
||||||
or not _session.is_gameplay_session_active()
|
or not _session.is_gameplay_session_active()
|
||||||
or recipient_peer_id == _session.get_local_peer_id()
|
or recipient_peer_id == _session.get_local_peer_id()
|
||||||
or not _session.is_authenticated_peer(recipient_peer_id)
|
or not _session.is_authenticated_peer(recipient_peer_id)
|
||||||
|
or (
|
||||||
|
_relationship_policy != null
|
||||||
|
and _relationship_policy.is_locally_blocked(
|
||||||
|
_session.get_peer_record(recipient_peer_id).identity_fingerprint
|
||||||
|
)
|
||||||
|
)
|
||||||
or (
|
or (
|
||||||
not _session.is_host()
|
not _session.is_host()
|
||||||
and not _session.supports_server_capability(
|
and not _session.supports_server_capability(
|
||||||
|
|
@ -235,6 +260,15 @@ func _handle_send(sender: int, data: Dictionary) -> void:
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
error = "Letter identity could not be verified."
|
error = "Letter identity could not be verified."
|
||||||
|
if (
|
||||||
|
error.is_empty()
|
||||||
|
and _relationship_policy != null
|
||||||
|
and _relationship_policy.pair_is_blocked(
|
||||||
|
sender_record.identity_fingerprint,
|
||||||
|
recipient_record.identity_fingerprint,
|
||||||
|
)
|
||||||
|
):
|
||||||
|
error = "That player is unavailable."
|
||||||
if error.is_empty() and _letters.size() >= NetworkMailProtocol.MAX_SESSION_LETTERS:
|
if error.is_empty() and _letters.size() >= NetworkMailProtocol.MAX_SESSION_LETTERS:
|
||||||
error = "The session mailbox is full."
|
error = "The session mailbox is full."
|
||||||
var recipient_count := 0
|
var recipient_count := 0
|
||||||
|
|
@ -327,6 +361,27 @@ func _receive_letter(letter: Dictionary) -> void:
|
||||||
_emit_mailbox()
|
_emit_mailbox()
|
||||||
|
|
||||||
|
|
||||||
|
func cancel_unaccepted_between(first: String, second: String) -> void:
|
||||||
|
if not _session.is_host():
|
||||||
|
return
|
||||||
|
for letter: Dictionary in _letters.values():
|
||||||
|
if not (
|
||||||
|
str(letter.get("sender_fingerprint", "")) in [first, second]
|
||||||
|
and str(letter.get("recipient_fingerprint", "")) in [first, second]
|
||||||
|
):
|
||||||
|
continue
|
||||||
|
if int(letter.get("state", -1)) not in [
|
||||||
|
NetworkMailProtocol.State.SENT_UNREAD,
|
||||||
|
NetworkMailProtocol.State.READ,
|
||||||
|
]:
|
||||||
|
continue
|
||||||
|
letter["state"] = NetworkMailProtocol.State.CANCELLED
|
||||||
|
_request_release(
|
||||||
|
int(letter["sender_peer_id"]), str(letter.get("reservation_id", ""))
|
||||||
|
)
|
||||||
|
_update_participants(letter)
|
||||||
|
|
||||||
|
|
||||||
func _deliver_send_result(peer_id: int, result: Dictionary) -> void:
|
func _deliver_send_result(peer_id: int, result: Dictionary) -> void:
|
||||||
if peer_id == _session.get_local_peer_id():
|
if peer_id == _session.get_local_peer_id():
|
||||||
_receive_send_result(result)
|
_receive_send_result(result)
|
||||||
|
|
@ -872,6 +927,18 @@ func _emit_mailbox() -> void:
|
||||||
unread_count_changed.emit(get_unread_count())
|
unread_count_changed.emit(get_unread_count())
|
||||||
|
|
||||||
|
|
||||||
|
func _letter_is_locally_blocked(letter: Dictionary) -> bool:
|
||||||
|
if _relationship_policy == null:
|
||||||
|
return false
|
||||||
|
var local_id := _session.get_local_peer_id()
|
||||||
|
var other_fingerprint := (
|
||||||
|
str(letter.get("sender_fingerprint", ""))
|
||||||
|
if int(letter.get("sender_peer_id", 0)) != local_id
|
||||||
|
else str(letter.get("recipient_fingerprint", ""))
|
||||||
|
)
|
||||||
|
return _relationship_policy.is_locally_blocked(other_fingerprint)
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_removed(peer_id: int) -> void:
|
func _on_peer_removed(peer_id: int) -> void:
|
||||||
if not _session.is_host():
|
if not _session.is_host():
|
||||||
_clear_local_mail_for_peer(peer_id)
|
_clear_local_mail_for_peer(peer_id)
|
||||||
|
|
|
||||||
266
network/network_player_list_service.gd
Normal file
266
network/network_player_list_service.gd
Normal file
|
|
@ -0,0 +1,266 @@
|
||||||
|
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 _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 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_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, 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, 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()
|
||||||
1
network/network_player_list_service.gd.uid
Normal file
1
network/network_player_list_service.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c71playerlist0001
|
||||||
|
|
@ -30,6 +30,7 @@ enum RejectionCode {
|
||||||
AUTHENTICATION_TIMEOUT,
|
AUTHENTICATION_TIMEOUT,
|
||||||
SERVER_SHUTTING_DOWN,
|
SERVER_SHUTTING_DOWN,
|
||||||
UNSUPPORTED_CLIENT,
|
UNSUPPORTED_CLIENT,
|
||||||
|
BANNED,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -212,5 +213,7 @@ static func rejection_text(code: int) -> String:
|
||||||
return "The server is shutting down."
|
return "The server is shutting down."
|
||||||
RejectionCode.UNSUPPORTED_CLIENT:
|
RejectionCode.UNSUPPORTED_CLIENT:
|
||||||
return "This game build is not supported by the server."
|
return "This game build is not supported by the server."
|
||||||
|
RejectionCode.BANNED:
|
||||||
|
return "You are not permitted to join this server."
|
||||||
_:
|
_:
|
||||||
return "The server rejected the connection."
|
return "The server rejected the connection."
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ var _player_identity: PlayerIdentityStore
|
||||||
var _host_identity: HostIdentityStore
|
var _host_identity: HostIdentityStore
|
||||||
var _known_players: KnownPlayerStore
|
var _known_players: KnownPlayerStore
|
||||||
var _server_trust: ServerTrustStore
|
var _server_trust: ServerTrustStore
|
||||||
|
var _host_bans: HostBanStore
|
||||||
var _pending_identity_challenges: Dictionary[int, Dictionary] = {}
|
var _pending_identity_challenges: Dictionary[int, Dictionary] = {}
|
||||||
var _authenticated_identity_cache: Dictionary[int, Dictionary] = {}
|
var _authenticated_identity_cache: Dictionary[int, Dictionary] = {}
|
||||||
var _client_identity_attempt: Dictionary = {}
|
var _client_identity_attempt: Dictionary = {}
|
||||||
|
|
@ -89,6 +90,7 @@ var _session_identity_keys: Dictionary[String, String] = {}
|
||||||
var _local_appearance_snapshot: Dictionary = (
|
var _local_appearance_snapshot: Dictionary = (
|
||||||
CharacterCustomizationCatalog.default_snapshot()
|
CharacterCustomizationCatalog.default_snapshot()
|
||||||
)
|
)
|
||||||
|
var _moderation_disconnect_message := ""
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
|
|
@ -107,6 +109,7 @@ func setup(
|
||||||
host_identity: HostIdentityStore,
|
host_identity: HostIdentityStore,
|
||||||
known_players: KnownPlayerStore,
|
known_players: KnownPlayerStore,
|
||||||
server_trust: ServerTrustStore,
|
server_trust: ServerTrustStore,
|
||||||
|
host_bans: HostBanStore,
|
||||||
) -> void:
|
) -> void:
|
||||||
_profile = profile
|
_profile = profile
|
||||||
_saved_servers = saved_servers
|
_saved_servers = saved_servers
|
||||||
|
|
@ -115,6 +118,7 @@ func setup(
|
||||||
_host_identity = host_identity
|
_host_identity = host_identity
|
||||||
_known_players = known_players
|
_known_players = known_players
|
||||||
_server_trust = server_trust
|
_server_trust = server_trust
|
||||||
|
_host_bans = host_bans
|
||||||
if _profile != null:
|
if _profile != null:
|
||||||
_profile_ready = _profile.load_or_create()
|
_profile_ready = _profile.load_or_create()
|
||||||
if _player_identity != null:
|
if _player_identity != null:
|
||||||
|
|
@ -293,7 +297,7 @@ func get_player_count() -> int:
|
||||||
|
|
||||||
|
|
||||||
func get_session_max_players() -> int:
|
func get_session_max_players() -> int:
|
||||||
return session_max_players
|
return _last_server_max_players if is_joined_client() else session_max_players
|
||||||
|
|
||||||
|
|
||||||
func get_current_route_display() -> String:
|
func get_current_route_display() -> String:
|
||||||
|
|
@ -362,6 +366,50 @@ func get_authenticated_peer_ids() -> Array[int]:
|
||||||
return _registry.get_peer_ids()
|
return _registry.get_peer_ids()
|
||||||
|
|
||||||
|
|
||||||
|
func get_peer_rtt_ms(peer_id: int) -> int:
|
||||||
|
if peer_id == 1:
|
||||||
|
return 0
|
||||||
|
var enet := multiplayer.multiplayer_peer as ENetMultiplayerPeer
|
||||||
|
if (
|
||||||
|
enet == null
|
||||||
|
or enet.get_connection_status()
|
||||||
|
!= MultiplayerPeer.CONNECTION_CONNECTED
|
||||||
|
):
|
||||||
|
return -1
|
||||||
|
var measurable := is_host() or (
|
||||||
|
is_joined_client() and peer_id == get_local_peer_id()
|
||||||
|
)
|
||||||
|
if not measurable:
|
||||||
|
return -1
|
||||||
|
if is_joined_client() and not _registry.has_peer(1):
|
||||||
|
return -1
|
||||||
|
var packet_peer: ENetPacketPeer = enet.get_peer(peer_id if is_host() else 1)
|
||||||
|
if packet_peer == null:
|
||||||
|
return -1
|
||||||
|
return int(packet_peer.get_statistic(ENetPacketPeer.PEER_ROUND_TRIP_TIME))
|
||||||
|
|
||||||
|
|
||||||
|
func kick_authenticated_peer(
|
||||||
|
peer_id: int,
|
||||||
|
fingerprint: String,
|
||||||
|
banned: bool = false,
|
||||||
|
) -> bool:
|
||||||
|
if not is_host() or peer_id <= 1:
|
||||||
|
return false
|
||||||
|
var record := _registry.get_peer(peer_id)
|
||||||
|
if record == null or record.identity_fingerprint != fingerprint:
|
||||||
|
return false
|
||||||
|
receive_moderation_disconnect.rpc_id(
|
||||||
|
peer_id,
|
||||||
|
(
|
||||||
|
"You are not permitted to join this server."
|
||||||
|
if banned else "You were removed by the host."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
call_deferred("_disconnect_rejected_peer", peer_id)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
func get_local_identity_fingerprint() -> String:
|
func get_local_identity_fingerprint() -> String:
|
||||||
return _player_identity.fingerprint if _player_identity != null else ""
|
return _player_identity.fingerprint if _player_identity != null else ""
|
||||||
|
|
||||||
|
|
@ -601,9 +649,15 @@ func _on_server_disconnected() -> void:
|
||||||
]:
|
]:
|
||||||
_fail("The server rejected or ended authentication.")
|
_fail("The server rejected or ended authentication.")
|
||||||
return
|
return
|
||||||
_set_state(State.SERVER_LOST, "The server connection was lost.")
|
var message := (
|
||||||
|
_moderation_disconnect_message
|
||||||
|
if not _moderation_disconnect_message.is_empty()
|
||||||
|
else "The server connection was lost."
|
||||||
|
)
|
||||||
|
_moderation_disconnect_message = ""
|
||||||
|
_set_state(State.SERVER_LOST, message)
|
||||||
server_lost.emit()
|
server_lost.emit()
|
||||||
connection_error.emit("The server connection was lost.")
|
connection_error.emit(message)
|
||||||
|
|
||||||
|
|
||||||
func _on_peer_disconnected(peer_id: int) -> void:
|
func _on_peer_disconnected(peer_id: int) -> void:
|
||||||
|
|
@ -782,9 +836,24 @@ func submit_client_identity_proof(data: Dictionary) -> void:
|
||||||
"fingerprint": challenge["client_fingerprint"],
|
"fingerprint": challenge["client_fingerprint"],
|
||||||
"public_key": challenge["client_public_key"],
|
"public_key": challenge["client_public_key"],
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
_host_bans != null
|
||||||
|
and _host_bans.is_banned(
|
||||||
|
_host_identity.fingerprint,
|
||||||
|
str(challenge["client_fingerprint"]),
|
||||||
|
)
|
||||||
|
):
|
||||||
|
_reject_peer(sender_id, NetworkProtocol.RejectionCode.BANNED)
|
||||||
|
return
|
||||||
request_client_profile.rpc_id(sender_id)
|
request_client_profile.rpc_id(sender_id)
|
||||||
|
|
||||||
|
|
||||||
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
|
func receive_moderation_disconnect(message: String) -> void:
|
||||||
|
_moderation_disconnect_message = message.left(120)
|
||||||
|
connection_error.emit(_moderation_disconnect_message)
|
||||||
|
|
||||||
|
|
||||||
@rpc("authority", "call_remote", "reliable", 0)
|
@rpc("authority", "call_remote", "reliable", 0)
|
||||||
func request_client_profile() -> void:
|
func request_client_profile() -> void:
|
||||||
if state != State.AUTHENTICATING:
|
if state != State.AUTHENTICATING:
|
||||||
|
|
@ -913,11 +982,13 @@ func _reject_peer(
|
||||||
|
|
||||||
|
|
||||||
func _disconnect_rejected_peer(peer_id: int) -> void:
|
func _disconnect_rejected_peer(peer_id: int) -> void:
|
||||||
|
await get_tree().create_timer(0.15).timeout
|
||||||
if (
|
if (
|
||||||
is_host()
|
is_host()
|
||||||
and multiplayer.multiplayer_peer != null
|
and multiplayer.multiplayer_peer != null
|
||||||
and multiplayer.multiplayer_peer.get_connection_status()
|
and multiplayer.multiplayer_peer.get_connection_status()
|
||||||
== MultiplayerPeer.CONNECTION_CONNECTED
|
== MultiplayerPeer.CONNECTION_CONNECTED
|
||||||
|
and peer_id in multiplayer.get_peers()
|
||||||
):
|
):
|
||||||
multiplayer.multiplayer_peer.disconnect_peer(peer_id)
|
multiplayer.multiplayer_peer.disconnect_peer(peer_id)
|
||||||
|
|
||||||
|
|
|
||||||
16
network/player_list_entry.gd
Normal file
16
network/player_list_entry.gd
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
class_name PlayerListEntry
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
var peer_id := 0
|
||||||
|
var full_fingerprint := ""
|
||||||
|
var compact_fingerprint := ""
|
||||||
|
var display_name := ""
|
||||||
|
var is_host := false
|
||||||
|
var is_local_player := false
|
||||||
|
var continuity_state := ""
|
||||||
|
var ping_to_host_ms := -1
|
||||||
|
var muted := false
|
||||||
|
var blocked := false
|
||||||
|
var can_kick := false
|
||||||
|
var can_ban := false
|
||||||
|
var revision := 0
|
||||||
1
network/player_list_entry.gd.uid
Normal file
1
network/player_list_entry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c71playerentry001
|
||||||
143
network/player_relationship_store.gd
Normal file
143
network/player_relationship_store.gd
Normal file
|
|
@ -0,0 +1,143 @@
|
||||||
|
class_name PlayerRelationshipStore
|
||||||
|
extends Node
|
||||||
|
|
||||||
|
signal relationship_changed(fingerprint: String)
|
||||||
|
|
||||||
|
const FORMAT_VERSION := 1
|
||||||
|
const STORE_PATH := "user://player_relationships.json"
|
||||||
|
const TEMP_PATH := STORE_PATH + ".tmp"
|
||||||
|
const MAX_RECORDS := 500
|
||||||
|
|
||||||
|
var _records: Dictionary = {}
|
||||||
|
var _loaded := false
|
||||||
|
var _write_blocked := false
|
||||||
|
|
||||||
|
|
||||||
|
func is_muted(fingerprint: String) -> bool:
|
||||||
|
_ensure_loaded()
|
||||||
|
return bool(_records.get(fingerprint, {}).get("muted", false))
|
||||||
|
|
||||||
|
|
||||||
|
func is_blocked(fingerprint: String) -> bool:
|
||||||
|
_ensure_loaded()
|
||||||
|
return bool(_records.get(fingerprint, {}).get("blocked", false))
|
||||||
|
|
||||||
|
|
||||||
|
func set_muted(fingerprint: String, display_name: String, value: bool) -> bool:
|
||||||
|
if not _valid_target(fingerprint, display_name):
|
||||||
|
return false
|
||||||
|
_ensure_loaded()
|
||||||
|
var record := _record(fingerprint, display_name)
|
||||||
|
record["muted"] = value or bool(record.get("blocked", false))
|
||||||
|
return _commit(fingerprint, record)
|
||||||
|
|
||||||
|
|
||||||
|
func set_blocked(fingerprint: String, display_name: String, value: bool) -> bool:
|
||||||
|
if not _valid_target(fingerprint, display_name):
|
||||||
|
return false
|
||||||
|
_ensure_loaded()
|
||||||
|
var record := _record(fingerprint, display_name)
|
||||||
|
record["blocked"] = value
|
||||||
|
if value:
|
||||||
|
record["muted"] = true
|
||||||
|
return _commit(fingerprint, record)
|
||||||
|
|
||||||
|
|
||||||
|
func get_records() -> Array[Dictionary]:
|
||||||
|
_ensure_loaded()
|
||||||
|
var result: Array[Dictionary] = []
|
||||||
|
for value: Dictionary in _records.values():
|
||||||
|
if bool(value.get("muted", false)) or bool(value.get("blocked", false)):
|
||||||
|
result.append(value.duplicate(true))
|
||||||
|
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
||||||
|
return str(a.get("last_known_display_name", "")).naturalnocasecmp_to(
|
||||||
|
str(b.get("last_known_display_name", ""))
|
||||||
|
) < 0
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
func _record(fingerprint: String, display_name: String) -> Dictionary:
|
||||||
|
var now := int(Time.get_unix_time_from_system())
|
||||||
|
var record: Dictionary = _records.get(fingerprint, {
|
||||||
|
"fingerprint": fingerprint,
|
||||||
|
"created_unix": now,
|
||||||
|
"muted": false,
|
||||||
|
"blocked": false,
|
||||||
|
})
|
||||||
|
record["last_known_display_name"] = display_name
|
||||||
|
record["updated_unix"] = now
|
||||||
|
return record
|
||||||
|
|
||||||
|
|
||||||
|
func _commit(fingerprint: String, record: Dictionary) -> bool:
|
||||||
|
if _write_blocked:
|
||||||
|
return false
|
||||||
|
var previous: Dictionary = _records.duplicate(true)
|
||||||
|
if not bool(record["muted"]) and not bool(record["blocked"]):
|
||||||
|
_records.erase(fingerprint)
|
||||||
|
else:
|
||||||
|
_records[fingerprint] = record
|
||||||
|
while _records.size() > MAX_RECORDS:
|
||||||
|
_records.erase(_records.keys().front())
|
||||||
|
if not _save():
|
||||||
|
_records = previous
|
||||||
|
return false
|
||||||
|
relationship_changed.emit(fingerprint)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _valid_target(fingerprint: String, display_name: String) -> bool:
|
||||||
|
return (
|
||||||
|
NetworkIdentityCrypto.valid_fingerprint(fingerprint)
|
||||||
|
and NetworkProfilePreferences.is_valid_display_name(display_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _ensure_loaded() -> void:
|
||||||
|
if _loaded:
|
||||||
|
return
|
||||||
|
_loaded = true
|
||||||
|
if not FileAccess.file_exists(STORE_PATH):
|
||||||
|
return
|
||||||
|
var file := FileAccess.open(STORE_PATH, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
return
|
||||||
|
var json := JSON.new()
|
||||||
|
if json.parse(file.get_as_text()) != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||||
|
return
|
||||||
|
var data: Dictionary = json.data
|
||||||
|
if data.get("format_version") != FORMAT_VERSION:
|
||||||
|
_write_blocked = true
|
||||||
|
return
|
||||||
|
for value: Variant in data.get("records", []):
|
||||||
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
|
continue
|
||||||
|
var record: Dictionary = value
|
||||||
|
var fingerprint := str(record.get("fingerprint", ""))
|
||||||
|
if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
|
||||||
|
continue
|
||||||
|
record["blocked"] = bool(record.get("blocked", false))
|
||||||
|
record["muted"] = bool(record.get("muted", false)) or record["blocked"]
|
||||||
|
_records[fingerprint] = record.duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
func _save() -> bool:
|
||||||
|
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
||||||
|
if file == null:
|
||||||
|
return false
|
||||||
|
file.store_string(JSON.stringify({
|
||||||
|
"format_version": FORMAT_VERSION,
|
||||||
|
"records": _records.values(),
|
||||||
|
}, "\t"))
|
||||||
|
file.flush()
|
||||||
|
var ok := file.get_error() == OK
|
||||||
|
file.close()
|
||||||
|
if not ok:
|
||||||
|
return false
|
||||||
|
if FileAccess.file_exists(STORE_PATH):
|
||||||
|
DirAccess.remove_absolute(ProjectSettings.globalize_path(STORE_PATH))
|
||||||
|
return DirAccess.rename_absolute(
|
||||||
|
ProjectSettings.globalize_path(TEMP_PATH),
|
||||||
|
ProjectSettings.globalize_path(STORE_PATH),
|
||||||
|
) == OK
|
||||||
1
network/player_relationship_store.gd.uid
Normal file
1
network/player_relationship_store.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c71relationships01
|
||||||
|
|
@ -78,6 +78,12 @@ func get_avatar(peer_id: int) -> Player:
|
||||||
return _avatars.get(peer_id)
|
return _avatars.get(peer_id)
|
||||||
|
|
||||||
|
|
||||||
|
func set_peer_presentation_visible(peer_id: int, visible: bool) -> void:
|
||||||
|
var avatar: Player = _avatars.get(peer_id)
|
||||||
|
if avatar != null and is_instance_valid(avatar):
|
||||||
|
avatar.set_remote_presentation_visible(visible)
|
||||||
|
|
||||||
|
|
||||||
func get_peer_ids() -> Array[int]:
|
func get_peer_ids() -> Array[int]:
|
||||||
var result: Array[int] = []
|
var result: Array[int] = []
|
||||||
for peer_id: int in _avatars:
|
for peer_id: int in _avatars:
|
||||||
|
|
|
||||||
|
|
@ -108,6 +108,7 @@ var _movement_enabled: bool = true
|
||||||
var _water_recovery_active: bool = false
|
var _water_recovery_active: bool = false
|
||||||
var _target_zoom: float = 5.0
|
var _target_zoom: float = 5.0
|
||||||
var _showcase_rod_visibility: bool = true
|
var _showcase_rod_visibility: bool = true
|
||||||
|
var _remote_presentation_visible := true
|
||||||
var _showcase_rod_state_stored: bool = false
|
var _showcase_rod_state_stored: bool = false
|
||||||
var _showcase_visual_rotation: Vector3
|
var _showcase_visual_rotation: Vector3
|
||||||
var _showcase_visual_rotation_stored: bool = false
|
var _showcase_visual_rotation_stored: bool = false
|
||||||
|
|
@ -551,6 +552,17 @@ func get_gameplay_camera() -> Camera3D:
|
||||||
return _camera
|
return _camera
|
||||||
|
|
||||||
|
|
||||||
|
func set_remote_presentation_visible(value: bool) -> void:
|
||||||
|
if local_control_enabled:
|
||||||
|
return
|
||||||
|
_remote_presentation_visible = value
|
||||||
|
_visuals.visible = value
|
||||||
|
|
||||||
|
|
||||||
|
func is_remote_presentation_visible() -> bool:
|
||||||
|
return _remote_presentation_visible
|
||||||
|
|
||||||
|
|
||||||
func get_fishing_rod_tip() -> Marker3D:
|
func get_fishing_rod_tip() -> Marker3D:
|
||||||
return _fishing_rod_tip
|
return _fishing_rod_tip
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,12 +187,19 @@ func _on_message(message: Dictionary) -> void:
|
||||||
_speech[peer_id] = {
|
_speech[peer_id] = {
|
||||||
"label": label,
|
"label": label,
|
||||||
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
|
"expires": Time.get_ticks_msec() / 1000.0 + SPEECH_SECONDS,
|
||||||
|
"fingerprint": str(message.get("sender_fingerprint", "")),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func _on_history(_messages: Array) -> void:
|
func _on_history(_messages: Array) -> void:
|
||||||
if _messages.is_empty():
|
|
||||||
for peer_id: int in _speech.keys():
|
for peer_id: int in _speech.keys():
|
||||||
|
var state: Dictionary = _speech[peer_id]
|
||||||
|
if (
|
||||||
|
_messages.is_empty()
|
||||||
|
or _service.is_sender_filtered(
|
||||||
|
str(state.get("fingerprint", ""))
|
||||||
|
)
|
||||||
|
):
|
||||||
_on_peer_removed(peer_id)
|
_on_peer_removed(peer_id)
|
||||||
_refresh_history()
|
_refresh_history()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -119,6 +119,7 @@ func setup(
|
||||||
network_mail_service: NetworkMailService,
|
network_mail_service: NetworkMailService,
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
network_profile_service: NetworkProfileService,
|
network_profile_service: NetworkProfileService,
|
||||||
|
network_player_list: NetworkPlayerListService,
|
||||||
) -> void:
|
) -> void:
|
||||||
_fishing_spot = fishing_spot
|
_fishing_spot = fishing_spot
|
||||||
_item_effects = item_effects
|
_item_effects = item_effects
|
||||||
|
|
@ -155,7 +156,8 @@ func setup(
|
||||||
network_sale_service,
|
network_sale_service,
|
||||||
network_mail_service,
|
network_mail_service,
|
||||||
reservations,
|
reservations,
|
||||||
network_profile_service
|
network_profile_service,
|
||||||
|
network_player_list,
|
||||||
)
|
)
|
||||||
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot)
|
_hotbar_ui.setup(hotbar, bag, item_catalog, fishing_spot)
|
||||||
_fishing_shop.setup(
|
_fishing_shop.setup(
|
||||||
|
|
|
||||||
|
|
@ -88,6 +88,7 @@ enum Section {
|
||||||
LOGBOOK,
|
LOGBOOK,
|
||||||
MAIL,
|
MAIL,
|
||||||
PROFILE,
|
PROFILE,
|
||||||
|
PLAYERS,
|
||||||
}
|
}
|
||||||
|
|
||||||
enum SortMode {
|
enum SortMode {
|
||||||
|
|
@ -158,6 +159,7 @@ enum CloseReason {
|
||||||
@onready var _logbook_page: Control = %LogbookPage
|
@onready var _logbook_page: Control = %LogbookPage
|
||||||
@onready var _mail_page: MailPage = %MailPage
|
@onready var _mail_page: MailPage = %MailPage
|
||||||
@onready var _profile_page: ProfilePage = %ProfilePage
|
@onready var _profile_page: ProfilePage = %ProfilePage
|
||||||
|
@onready var _players_page: PlayersPage = %PlayersPage
|
||||||
@onready var _book_backing: PanelContainer = %BookBacking
|
@onready var _book_backing: PanelContainer = %BookBacking
|
||||||
@onready var _book_spread: BoxContainer = %BookSpread
|
@onready var _book_spread: BoxContainer = %BookSpread
|
||||||
@onready var _left_page: PanelContainer = %LeftPage
|
@onready var _left_page: PanelContainer = %LeftPage
|
||||||
|
|
@ -176,6 +178,7 @@ enum CloseReason {
|
||||||
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
|
@onready var _logbook_tab: BubbleButtonType = %LogbookTab
|
||||||
@onready var _mail_tab: BubbleButtonType = %MailTab
|
@onready var _mail_tab: BubbleButtonType = %MailTab
|
||||||
@onready var _profile_tab: BubbleButtonType = %ProfileTab
|
@onready var _profile_tab: BubbleButtonType = %ProfileTab
|
||||||
|
@onready var _players_tab: BubbleButtonType = %PlayersTab
|
||||||
@onready var _mail_unread_badge: Label = %MailUnreadBadge
|
@onready var _mail_unread_badge: Label = %MailUnreadBadge
|
||||||
@onready var _close_button: BubbleButtonType = %CloseButton
|
@onready var _close_button: BubbleButtonType = %CloseButton
|
||||||
@onready var _content_shell: BubbleContentShellType = %MenuPanel
|
@onready var _content_shell: BubbleContentShellType = %MenuPanel
|
||||||
|
|
@ -237,6 +240,7 @@ var _network_session: NetworkSessionType
|
||||||
var _network_sale_service: NetworkSaleService
|
var _network_sale_service: NetworkSaleService
|
||||||
var _network_mail_service: NetworkMailService
|
var _network_mail_service: NetworkMailService
|
||||||
var _network_profile_service: NetworkProfileService
|
var _network_profile_service: NetworkProfileService
|
||||||
|
var _network_player_list: NetworkPlayerListService
|
||||||
var _default_buyer: FishBuyerProfileType
|
var _default_buyer: FishBuyerProfileType
|
||||||
var _catalog: FishPoolType
|
var _catalog: FishPoolType
|
||||||
var _fishing_spot: FishingSpotType
|
var _fishing_spot: FishingSpotType
|
||||||
|
|
@ -300,6 +304,7 @@ func _ready() -> void:
|
||||||
)
|
)
|
||||||
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
|
_mail_tab.pressed.connect(_show_section.bind(Section.MAIL))
|
||||||
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
|
_profile_tab.pressed.connect(_show_section.bind(Section.PROFILE))
|
||||||
|
_players_tab.pressed.connect(_show_section.bind(Section.PLAYERS))
|
||||||
_close_button.pressed.connect(close_menu)
|
_close_button.pressed.connect(close_menu)
|
||||||
_sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option))
|
_sort_option.item_selected.connect(_on_sort_selected.bind(_sort_option))
|
||||||
_sort_direction.pressed.connect(_on_sort_direction_pressed)
|
_sort_direction.pressed.connect(_on_sort_direction_pressed)
|
||||||
|
|
@ -327,6 +332,7 @@ func _ready() -> void:
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
|
_players_tab,
|
||||||
_close_button,
|
_close_button,
|
||||||
])
|
])
|
||||||
_configure_navigation_focus()
|
_configure_navigation_focus()
|
||||||
|
|
@ -376,6 +382,7 @@ func setup(
|
||||||
network_mail_service: NetworkMailService,
|
network_mail_service: NetworkMailService,
|
||||||
reservations: PlayerAssetReservationService,
|
reservations: PlayerAssetReservationService,
|
||||||
network_profile_service: NetworkProfileService,
|
network_profile_service: NetworkProfileService,
|
||||||
|
network_player_list: NetworkPlayerListService,
|
||||||
) -> void:
|
) -> void:
|
||||||
_player = player
|
_player = player
|
||||||
_inventory = inventory
|
_inventory = inventory
|
||||||
|
|
@ -393,10 +400,12 @@ func setup(
|
||||||
_network_sale_service = network_sale_service
|
_network_sale_service = network_sale_service
|
||||||
_network_mail_service = network_mail_service
|
_network_mail_service = network_mail_service
|
||||||
_network_profile_service = network_profile_service
|
_network_profile_service = network_profile_service
|
||||||
|
_network_player_list = network_player_list
|
||||||
_mail_page.setup(
|
_mail_page.setup(
|
||||||
network_mail_service, reservations, inventory, wallet, bag, item_catalog
|
network_mail_service, reservations, inventory, wallet, bag, item_catalog
|
||||||
)
|
)
|
||||||
_profile_page.setup(network_profile_service)
|
_profile_page.setup(network_profile_service)
|
||||||
|
_players_page.setup(network_player_list)
|
||||||
_network_mail_service.unread_count_changed.connect(
|
_network_mail_service.unread_count_changed.connect(
|
||||||
_on_mail_unread_count_changed
|
_on_mail_unread_count_changed
|
||||||
)
|
)
|
||||||
|
|
@ -608,6 +617,7 @@ func _show_section_immediate(section: Section) -> void:
|
||||||
_logbook_page.visible = section == Section.LOGBOOK
|
_logbook_page.visible = section == Section.LOGBOOK
|
||||||
_mail_page.visible = section == Section.MAIL
|
_mail_page.visible = section == Section.MAIL
|
||||||
_profile_page.visible = section == Section.PROFILE
|
_profile_page.visible = section == Section.PROFILE
|
||||||
|
_players_page.visible = section == Section.PLAYERS
|
||||||
_content_shell.visible = false
|
_content_shell.visible = false
|
||||||
_inventory_section.visible = false
|
_inventory_section.visible = false
|
||||||
_bag_section.visible = false
|
_bag_section.visible = false
|
||||||
|
|
@ -634,11 +644,16 @@ func _show_section_immediate(section: Section) -> void:
|
||||||
_profile_page.activate()
|
_profile_page.activate()
|
||||||
else:
|
else:
|
||||||
_profile_page.deactivate()
|
_profile_page.deactivate()
|
||||||
|
if section == Section.PLAYERS:
|
||||||
|
_players_page.activate()
|
||||||
|
else:
|
||||||
|
_players_page.deactivate()
|
||||||
_inventory_tab.button_pressed = section == Section.COOLER
|
_inventory_tab.button_pressed = section == Section.COOLER
|
||||||
_bag_tab.button_pressed = section == Section.BAG
|
_bag_tab.button_pressed = section == Section.BAG
|
||||||
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
||||||
_mail_tab.button_pressed = section == Section.MAIL
|
_mail_tab.button_pressed = section == Section.MAIL
|
||||||
_profile_tab.button_pressed = section == Section.PROFILE
|
_profile_tab.button_pressed = section == Section.PROFILE
|
||||||
|
_players_tab.button_pressed = section == Section.PLAYERS
|
||||||
_update_navigation_selection()
|
_update_navigation_selection()
|
||||||
_configure_active_page_focus()
|
_configure_active_page_focus()
|
||||||
|
|
||||||
|
|
@ -652,6 +667,8 @@ func _focus_current_section() -> void:
|
||||||
_logbook_tab.grab_focus()
|
_logbook_tab.grab_focus()
|
||||||
elif _current_section == Section.MAIL:
|
elif _current_section == Section.MAIL:
|
||||||
_mail_tab.grab_focus()
|
_mail_tab.grab_focus()
|
||||||
|
elif _current_section == Section.PLAYERS:
|
||||||
|
_players_tab.grab_focus()
|
||||||
else:
|
else:
|
||||||
_profile_tab.grab_focus()
|
_profile_tab.grab_focus()
|
||||||
|
|
||||||
|
|
@ -682,6 +699,7 @@ func _configure_navigation_focus() -> void:
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
|
_players_tab,
|
||||||
_close_button,
|
_close_button,
|
||||||
]
|
]
|
||||||
for index: int in navigation.size():
|
for index: int in navigation.size():
|
||||||
|
|
@ -835,6 +853,7 @@ func _apply_navigation_styles() -> void:
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
|
_players_tab,
|
||||||
_close_button,
|
_close_button,
|
||||||
]:
|
]:
|
||||||
bubble.add_theme_stylebox_override("normal", normal)
|
bubble.add_theme_stylebox_override("normal", normal)
|
||||||
|
|
@ -866,6 +885,7 @@ func _apply_navigation_selection_presentation() -> void:
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
|
_players_tab,
|
||||||
]:
|
]:
|
||||||
if not bubble.button_pressed:
|
if not bubble.button_pressed:
|
||||||
continue
|
continue
|
||||||
|
|
@ -975,6 +995,7 @@ func _set_navigation_target(section: Section) -> void:
|
||||||
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
_logbook_tab.button_pressed = section == Section.LOGBOOK
|
||||||
_mail_tab.button_pressed = section == Section.MAIL
|
_mail_tab.button_pressed = section == Section.MAIL
|
||||||
_profile_tab.button_pressed = section == Section.PROFILE
|
_profile_tab.button_pressed = section == Section.PROFILE
|
||||||
|
_players_tab.button_pressed = section == Section.PLAYERS
|
||||||
|
|
||||||
|
|
||||||
func _update_shell_layout() -> void:
|
func _update_shell_layout() -> void:
|
||||||
|
|
@ -1591,6 +1612,7 @@ func _set_shell_interactive(interactive: bool) -> void:
|
||||||
_logbook_tab,
|
_logbook_tab,
|
||||||
_mail_tab,
|
_mail_tab,
|
||||||
_profile_tab,
|
_profile_tab,
|
||||||
|
_players_tab,
|
||||||
_close_button,
|
_close_button,
|
||||||
]:
|
]:
|
||||||
bubble.focus_mode = Control.FOCUS_ALL if interactive else Control.FOCUS_NONE
|
bubble.focus_mode = Control.FOCUS_ALL if interactive else Control.FOCUS_NONE
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
[gd_scene load_steps=21 format=3]
|
[gd_scene load_steps=22 format=3]
|
||||||
|
|
||||||
[ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"]
|
[ext_resource type="Script" path="res://ui/player_menu.gd" id="1_menu"]
|
||||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
[ext_resource type="Texture2D" path="res://ui/assets/title/bubbles/bubble3.png" id="17_bubble3"]
|
[ext_resource type="Texture2D" path="res://ui/assets/title/bubbles/bubble3.png" id="17_bubble3"]
|
||||||
[ext_resource type="Script" path="res://ui/mail_page.gd" id="18_mail_page"]
|
[ext_resource type="Script" path="res://ui/mail_page.gd" id="18_mail_page"]
|
||||||
[ext_resource type="PackedScene" path="res://ui/profile_page.tscn" id="19_profile_page"]
|
[ext_resource type="PackedScene" path="res://ui/profile_page.tscn" id="19_profile_page"]
|
||||||
|
[ext_resource type="Script" path="res://ui/players_page.gd" id="20_players_page"]
|
||||||
|
|
||||||
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"]
|
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_collection"]
|
||||||
|
|
||||||
|
|
@ -592,6 +593,15 @@ offset_top = 112.0
|
||||||
offset_right = 1280.0
|
offset_right = 1280.0
|
||||||
offset_bottom = 710.0
|
offset_bottom = 710.0
|
||||||
|
|
||||||
|
[node name="PlayersPage" type="Control" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot"]
|
||||||
|
visible = false
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
offset_right = 1280.0
|
||||||
|
offset_bottom = 720.0
|
||||||
|
mouse_filter = 1
|
||||||
|
script = ExtResource("20_players_page")
|
||||||
|
|
||||||
[node name="BookBacking" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
|
[node name="BookBacking" type="PanelContainer" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/LogbookPage"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
|
|
@ -1232,9 +1242,9 @@ toggle_mode = true
|
||||||
text = "cooler"
|
text = "cooler"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(124, 120)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(92, 60)
|
desktop_anchor = Vector2(70, 60)
|
||||||
compact_anchor = Vector2(92, 73.3333)
|
compact_anchor = Vector2(70, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 25
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1252,9 +1262,9 @@ toggle_mode = true
|
||||||
text = "bag"
|
text = "bag"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(124, 120)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(220, 60)
|
desktop_anchor = Vector2(182, 60)
|
||||||
compact_anchor = Vector2(220, 73.3333)
|
compact_anchor = Vector2(182, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 25
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1272,9 +1282,9 @@ toggle_mode = true
|
||||||
text = "logbook"
|
text = "logbook"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(124, 120)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(348, 60)
|
desktop_anchor = Vector2(294, 60)
|
||||||
compact_anchor = Vector2(348, 73.3333)
|
compact_anchor = Vector2(294, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 25
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1292,9 +1302,9 @@ toggle_mode = true
|
||||||
text = "mail"
|
text = "mail"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(124, 120)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(476, 60)
|
desktop_anchor = Vector2(406, 60)
|
||||||
compact_anchor = Vector2(476, 73.3333)
|
compact_anchor = Vector2(406, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 25
|
minimum_font_size = 25
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1328,9 +1338,9 @@ toggle_mode = true
|
||||||
text = "profile"
|
text = "profile"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(124, 120)
|
neutral_size = Vector2(112, 108)
|
||||||
desktop_anchor = Vector2(604, 60)
|
desktop_anchor = Vector2(518, 60)
|
||||||
compact_anchor = Vector2(604, 73.3333)
|
compact_anchor = Vector2(518, 73.3333)
|
||||||
compact_minimum_size = Vector2(84, 82)
|
compact_minimum_size = Vector2(84, 82)
|
||||||
minimum_font_size = 23
|
minimum_font_size = 23
|
||||||
maximum_font_size = 25
|
maximum_font_size = 25
|
||||||
|
|
@ -1341,6 +1351,26 @@ motion_phase = 4.05
|
||||||
deformation_amplitude = 0.011
|
deformation_amplitude = 0.011
|
||||||
deformation_period = 6.5
|
deformation_period = 6.5
|
||||||
|
|
||||||
|
[node name="PlayersTab" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
|
||||||
|
unique_name_in_owner = true
|
||||||
|
layout_mode = 0
|
||||||
|
toggle_mode = true
|
||||||
|
text = "players"
|
||||||
|
script = ExtResource("6_bubble")
|
||||||
|
profile = ExtResource("4_profile")
|
||||||
|
neutral_size = Vector2(112, 108)
|
||||||
|
desktop_anchor = Vector2(630, 60)
|
||||||
|
compact_anchor = Vector2(630, 73.3333)
|
||||||
|
compact_minimum_size = Vector2(84, 82)
|
||||||
|
minimum_font_size = 22
|
||||||
|
maximum_font_size = 24
|
||||||
|
horizontal_amplitude = 1.1
|
||||||
|
vertical_amplitude = 2.2
|
||||||
|
motion_period = 5.8
|
||||||
|
motion_phase = 4.35
|
||||||
|
deformation_amplitude = 0.011
|
||||||
|
deformation_period = 6.4
|
||||||
|
|
||||||
[node name="CloseButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
|
[node name="CloseButton" type="Button" parent="ResponsivePlayerMenuStage/PlayerMenuPresentationScaleRoot/NavigationCluster"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
|
|
@ -1348,8 +1378,8 @@ text = "close"
|
||||||
script = ExtResource("6_bubble")
|
script = ExtResource("6_bubble")
|
||||||
profile = ExtResource("4_profile")
|
profile = ExtResource("4_profile")
|
||||||
neutral_size = Vector2(96, 94)
|
neutral_size = Vector2(96, 94)
|
||||||
desktop_anchor = Vector2(744, 57)
|
desktop_anchor = Vector2(770, 57)
|
||||||
compact_anchor = Vector2(744, 73.3333)
|
compact_anchor = Vector2(770, 73.3333)
|
||||||
compact_minimum_size = Vector2(76, 74)
|
compact_minimum_size = Vector2(76, 74)
|
||||||
minimum_font_size = 23
|
minimum_font_size = 23
|
||||||
maximum_font_size = 23
|
maximum_font_size = 23
|
||||||
|
|
|
||||||
289
ui/players_page.gd
Normal file
289
ui/players_page.gd
Normal file
|
|
@ -0,0 +1,289 @@
|
||||||
|
class_name PlayersPage
|
||||||
|
extends Control
|
||||||
|
|
||||||
|
var _service: NetworkPlayerListService
|
||||||
|
var _header: Label
|
||||||
|
var _tabs: HBoxContainer
|
||||||
|
var _list: VBoxContainer
|
||||||
|
var _status: Label
|
||||||
|
var _current_tab := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _ready() -> void:
|
||||||
|
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
|
||||||
|
_build()
|
||||||
|
|
||||||
|
|
||||||
|
func setup(service: NetworkPlayerListService) -> void:
|
||||||
|
_service = service
|
||||||
|
_service.entries_changed.connect(_refresh)
|
||||||
|
_service.moderation_finished.connect(func(_ok: bool, message: String) -> void:
|
||||||
|
_status.text = message
|
||||||
|
_refresh()
|
||||||
|
)
|
||||||
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
func activate() -> void:
|
||||||
|
_refresh()
|
||||||
|
_focus_first()
|
||||||
|
|
||||||
|
|
||||||
|
func deactivate() -> void:
|
||||||
|
_status.text = ""
|
||||||
|
|
||||||
|
|
||||||
|
func _build() -> void:
|
||||||
|
var paper := PanelContainer.new()
|
||||||
|
paper.position = Vector2(58, 128)
|
||||||
|
paper.size = Vector2(1164, 476)
|
||||||
|
add_child(paper)
|
||||||
|
var style := StyleBoxFlat.new()
|
||||||
|
style.bg_color = Color("eee2bd")
|
||||||
|
style.border_color = Color("473d2e")
|
||||||
|
style.set_border_width_all(4)
|
||||||
|
style.set_corner_radius_all(16)
|
||||||
|
style.content_margin_left = 26
|
||||||
|
style.content_margin_right = 26
|
||||||
|
style.content_margin_top = 20
|
||||||
|
style.content_margin_bottom = 20
|
||||||
|
paper.add_theme_stylebox_override("panel", style)
|
||||||
|
var root := VBoxContainer.new()
|
||||||
|
root.add_theme_constant_override("separation", 10)
|
||||||
|
paper.add_child(root)
|
||||||
|
_header = Label.new()
|
||||||
|
_header.add_theme_font_size_override("font_size", 27)
|
||||||
|
_header.add_theme_color_override("font_color", Color("28251f"))
|
||||||
|
root.add_child(_header)
|
||||||
|
_tabs = HBoxContainer.new()
|
||||||
|
_tabs.add_theme_constant_override("separation", 10)
|
||||||
|
root.add_child(_tabs)
|
||||||
|
for index: int in 3:
|
||||||
|
var button := Button.new()
|
||||||
|
button.text = ["players", "relationships", "banned"][index]
|
||||||
|
button.toggle_mode = true
|
||||||
|
button.pressed.connect(_select_tab.bind(index))
|
||||||
|
_tabs.add_child(button)
|
||||||
|
var scroll := ScrollContainer.new()
|
||||||
|
scroll.custom_minimum_size = Vector2(0, 330)
|
||||||
|
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
|
||||||
|
root.add_child(scroll)
|
||||||
|
_list = VBoxContainer.new()
|
||||||
|
_list.custom_minimum_size = Vector2(1080, 0)
|
||||||
|
_list.add_theme_constant_override("separation", 7)
|
||||||
|
scroll.add_child(_list)
|
||||||
|
_status = Label.new()
|
||||||
|
_status.add_theme_color_override("font_color", Color("514535"))
|
||||||
|
root.add_child(_status)
|
||||||
|
|
||||||
|
|
||||||
|
func _select_tab(index: int) -> void:
|
||||||
|
if index == 2 and (_service == null or not _service.is_local_host()):
|
||||||
|
return
|
||||||
|
_current_tab = index
|
||||||
|
_refresh()
|
||||||
|
_focus_first()
|
||||||
|
|
||||||
|
|
||||||
|
func _refresh() -> void:
|
||||||
|
if _service == null or _list == null:
|
||||||
|
return
|
||||||
|
_header.text = "players %d / %d connected" % [
|
||||||
|
_service.get_connected_count(), _service.get_max_players(),
|
||||||
|
]
|
||||||
|
for index: int in _tabs.get_child_count():
|
||||||
|
var button := _tabs.get_child(index) as Button
|
||||||
|
button.button_pressed = index == _current_tab
|
||||||
|
button.visible = index != 2 or _service.is_local_host()
|
||||||
|
for child: Node in _list.get_children():
|
||||||
|
child.queue_free()
|
||||||
|
match _current_tab:
|
||||||
|
0:
|
||||||
|
_build_active_rows()
|
||||||
|
1:
|
||||||
|
_build_relationship_rows()
|
||||||
|
2:
|
||||||
|
_build_ban_rows()
|
||||||
|
|
||||||
|
|
||||||
|
func _build_active_rows() -> void:
|
||||||
|
var entries := _service.get_entries()
|
||||||
|
if entries.is_empty():
|
||||||
|
_add_empty("No authenticated players.")
|
||||||
|
return
|
||||||
|
for entry: PlayerListEntry in entries:
|
||||||
|
var row := _make_row()
|
||||||
|
var identity := Label.new()
|
||||||
|
identity.custom_minimum_size.x = 515
|
||||||
|
var markers: Array[String] = []
|
||||||
|
if entry.is_host:
|
||||||
|
markers.append("host")
|
||||||
|
if entry.is_local_player:
|
||||||
|
markers.append("You")
|
||||||
|
identity.text = "%s%s · %s\n%s" % [
|
||||||
|
entry.display_name,
|
||||||
|
" [%s]" % ", ".join(markers) if not markers.is_empty() else "",
|
||||||
|
entry.compact_fingerprint,
|
||||||
|
entry.continuity_state,
|
||||||
|
]
|
||||||
|
identity.tooltip_text = NetworkIdentityCrypto.format_fingerprint(
|
||||||
|
entry.full_fingerprint
|
||||||
|
)
|
||||||
|
row.add_child(identity)
|
||||||
|
var ping := Label.new()
|
||||||
|
ping.custom_minimum_size.x = 80
|
||||||
|
ping.text = (
|
||||||
|
"Local" if entry.is_host
|
||||||
|
else "%d ms" % entry.ping_to_host_ms
|
||||||
|
if entry.ping_to_host_ms >= 0 else "—"
|
||||||
|
)
|
||||||
|
row.add_child(ping)
|
||||||
|
var mute := Button.new()
|
||||||
|
mute.text = "unmute" if entry.muted else "mute"
|
||||||
|
mute.disabled = entry.is_local_player
|
||||||
|
mute.pressed.connect(_toggle_mute.bind(entry))
|
||||||
|
row.add_child(mute)
|
||||||
|
var block := Button.new()
|
||||||
|
block.text = "block"
|
||||||
|
block.disabled = entry.is_local_player
|
||||||
|
block.pressed.connect(_confirm_block.bind(entry))
|
||||||
|
row.add_child(block)
|
||||||
|
var kick := Button.new()
|
||||||
|
kick.text = "kick"
|
||||||
|
kick.disabled = not entry.can_kick
|
||||||
|
kick.pressed.connect(_confirm_kick.bind(entry))
|
||||||
|
row.add_child(kick)
|
||||||
|
var ban := Button.new()
|
||||||
|
ban.text = "ban"
|
||||||
|
ban.disabled = not entry.can_ban
|
||||||
|
ban.pressed.connect(_confirm_ban.bind(entry))
|
||||||
|
row.add_child(ban)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_relationship_rows() -> void:
|
||||||
|
var records := _service.get_relationships()
|
||||||
|
if records.is_empty():
|
||||||
|
_add_empty("No muted or blocked identities.")
|
||||||
|
return
|
||||||
|
for record: Dictionary in records:
|
||||||
|
var row := _make_row()
|
||||||
|
var label := Label.new()
|
||||||
|
label.custom_minimum_size.x = 690
|
||||||
|
var fingerprint := str(record["fingerprint"])
|
||||||
|
label.text = "%s · %s %s" % [
|
||||||
|
str(record.get("last_known_display_name", "Player")),
|
||||||
|
NetworkIdentityCrypto.compact_suffix(fingerprint),
|
||||||
|
"Blocked" if bool(record.get("blocked", false)) else "Muted",
|
||||||
|
]
|
||||||
|
label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
|
||||||
|
row.add_child(label)
|
||||||
|
if bool(record.get("blocked", false)):
|
||||||
|
var unblock := Button.new()
|
||||||
|
unblock.text = "unblock"
|
||||||
|
unblock.pressed.connect(func() -> void:
|
||||||
|
_service.set_blocked(fingerprint, str(record["last_known_display_name"]), false)
|
||||||
|
)
|
||||||
|
row.add_child(unblock)
|
||||||
|
var unmute := Button.new()
|
||||||
|
unmute.text = "unmute"
|
||||||
|
unmute.disabled = bool(record.get("blocked", false))
|
||||||
|
unmute.pressed.connect(func() -> void:
|
||||||
|
_service.set_muted(fingerprint, str(record["last_known_display_name"]), false)
|
||||||
|
)
|
||||||
|
row.add_child(unmute)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _build_ban_rows() -> void:
|
||||||
|
var records := _service.get_bans()
|
||||||
|
if records.is_empty():
|
||||||
|
_add_empty("No banned identities.")
|
||||||
|
return
|
||||||
|
for record: Dictionary in records:
|
||||||
|
var row := _make_row()
|
||||||
|
var fingerprint := str(record["target_fingerprint"])
|
||||||
|
var label := Label.new()
|
||||||
|
label.custom_minimum_size.x = 830
|
||||||
|
label.text = "%s · %s banned %s" % [
|
||||||
|
str(record.get("last_known_display_name", "Player")),
|
||||||
|
NetworkIdentityCrypto.compact_suffix(fingerprint),
|
||||||
|
Time.get_date_string_from_unix_time(int(record.get("banned_unix", 0))),
|
||||||
|
]
|
||||||
|
label.tooltip_text = NetworkIdentityCrypto.format_fingerprint(fingerprint)
|
||||||
|
row.add_child(label)
|
||||||
|
var unban := Button.new()
|
||||||
|
unban.text = "unban"
|
||||||
|
unban.pressed.connect(_confirm_unban.bind(fingerprint))
|
||||||
|
row.add_child(unban)
|
||||||
|
_list.add_child(row)
|
||||||
|
|
||||||
|
|
||||||
|
func _make_row() -> HBoxContainer:
|
||||||
|
var row := HBoxContainer.new()
|
||||||
|
row.custom_minimum_size.y = 58
|
||||||
|
row.add_theme_constant_override("separation", 8)
|
||||||
|
return row
|
||||||
|
|
||||||
|
|
||||||
|
func _add_empty(text: String) -> void:
|
||||||
|
var label := Label.new()
|
||||||
|
label.text = text
|
||||||
|
label.custom_minimum_size.y = 100
|
||||||
|
label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||||
|
_list.add_child(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _toggle_mute(entry: PlayerListEntry) -> void:
|
||||||
|
_service.set_muted(entry.full_fingerprint, entry.display_name, not entry.muted)
|
||||||
|
|
||||||
|
|
||||||
|
func _confirm_block(entry: PlayerListEntry) -> void:
|
||||||
|
_confirm(
|
||||||
|
"Block %s?\nTheir local social and visual presentation will be hidden."
|
||||||
|
% entry.display_name,
|
||||||
|
func() -> void:
|
||||||
|
_service.set_blocked(entry.full_fingerprint, entry.display_name, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _confirm_kick(entry: PlayerListEntry) -> void:
|
||||||
|
_confirm("Remove %s from this session?" % entry.display_name, func() -> void:
|
||||||
|
_service.kick(entry.peer_id, entry.full_fingerprint, entry.revision)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _confirm_ban(entry: PlayerListEntry) -> void:
|
||||||
|
_confirm("Ban %s · %s from this server?" % [
|
||||||
|
entry.display_name, entry.compact_fingerprint,
|
||||||
|
], func() -> void:
|
||||||
|
_service.ban(
|
||||||
|
entry.peer_id, entry.full_fingerprint, entry.display_name, entry.revision
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _confirm_unban(fingerprint: String) -> void:
|
||||||
|
_confirm("Unban %s?" % NetworkIdentityCrypto.compact_suffix(fingerprint), func() -> void:
|
||||||
|
_service.unban(fingerprint)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _confirm(text: String, action: Callable) -> void:
|
||||||
|
var dialog := ConfirmationDialog.new()
|
||||||
|
dialog.dialog_text = text
|
||||||
|
dialog.ok_button_text = "confirm"
|
||||||
|
dialog.canceled.connect(dialog.queue_free)
|
||||||
|
dialog.confirmed.connect(func() -> void:
|
||||||
|
action.call()
|
||||||
|
dialog.queue_free()
|
||||||
|
)
|
||||||
|
add_child(dialog)
|
||||||
|
dialog.popup_centered(Vector2i(520, 220))
|
||||||
|
|
||||||
|
|
||||||
|
func _focus_first() -> void:
|
||||||
|
for child: Node in _tabs.get_children():
|
||||||
|
if child is Button and child.visible and not child.disabled:
|
||||||
|
child.grab_focus()
|
||||||
|
return
|
||||||
1
ui/players_page.gd.uid
Normal file
1
ui/players_page.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
uid://c71playerspage0001
|
||||||
Loading…
Add table
Add a link
Reference in a new issue