class_name PlayersPage extends Control var _service: NetworkPlayerListService var _count_label: 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) UtilityPageStyle.apply_page(self) _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() UtilityPageStyle.animate_in(self) _focus_first() func deactivate() -> void: _status.text = "" func _build() -> void: var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self) var root := VBoxContainer.new() root.add_theme_constant_override("separation", 9) margin.add_child(root) var tab_row := HBoxContainer.new() tab_row.add_theme_constant_override("separation", 16) root.add_child(tab_row) _tabs = HBoxContainer.new() _tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL _tabs.add_theme_constant_override("separation", 10) tab_row.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)) UtilityPageStyle.apply_ocean_button(button) _tabs.add_child(button) _count_label = Label.new() _count_label.add_theme_font_size_override("font_size", 17) _count_label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) _count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER tab_row.add_child(_count_label) var scroll := ScrollContainer.new() scroll.custom_minimum_size = Vector2(0, 310) scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED root.add_child(scroll) _list = VBoxContainer.new() _list.custom_minimum_size = Vector2(1032, 0) _list.add_theme_constant_override("separation", 7) scroll.add_child(_list) _status = Label.new() _status.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) 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 _count_label.text = "%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: if _service.is_local_host(): _build_session_artwork_controls() 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 ) identity.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) 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 "—" ) ping.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) 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)) UtilityPageStyle.apply_ocean_button(mute) row.add_child(mute) var block := Button.new() block.text = "block" block.disabled = entry.is_local_player block.pressed.connect(_confirm_block.bind(entry)) UtilityPageStyle.apply_ocean_button(block) row.add_child(block) var kick := Button.new() kick.text = "kick" kick.disabled = not entry.can_kick kick.pressed.connect(_confirm_kick.bind(entry)) UtilityPageStyle.apply_ocean_button(kick) row.add_child(kick) var ban := Button.new() ban.text = "ban" ban.disabled = not entry.can_ban ban.pressed.connect(_confirm_ban.bind(entry)) UtilityPageStyle.apply_ocean_button(ban) row.add_child(ban) func _build_session_artwork_controls() -> void: var counts: Vector2i = _service.get_session_artwork_counts() var row := _make_row() var label := Label.new() label.custom_minimum_size.x = 830 label.text = "session artwork · %d layers · %d painted pixels" % [ counts.x, counts.y, ] label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) row.add_child(label) var reset := Button.new() reset.text = "reset paint" reset.disabled = counts.x == 0 reset.tooltip_text = "Clears all shared artwork from this session." reset.pressed.connect(func() -> void: _confirm( "Clear all shared paint from this session?\nThis cannot be undone.", _service.reset_session_artwork, ) ) UtilityPageStyle.apply_ocean_button(reset) row.add_child(reset) 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) label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) 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) ) UtilityPageStyle.apply_ocean_button(unblock) 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) ) UtilityPageStyle.apply_ocean_button(unmute) row.add_child(unmute) 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) label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY ) row.add_child(label) var unban := Button.new() unban.text = "unban" unban.pressed.connect(_confirm_unban.bind(fingerprint)) UtilityPageStyle.apply_ocean_button(unban) row.add_child(unban) func _make_row() -> HBoxContainer: var panel := PanelContainer.new() panel.add_theme_stylebox_override( "panel", UtilityPageStyle.row_style(false) ) _list.add_child(panel) var row := HBoxContainer.new() row.custom_minimum_size.y = 58 row.add_theme_constant_override("separation", 8) panel.add_child(row) 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 label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY ) _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: var candidates: Array[Control] = [] _collect_focusable_player_controls(self, candidates) if candidates.is_empty(): return candidates.sort_custom(func(first: Control, second: Control) -> bool: if not is_equal_approx(first.global_position.y, second.global_position.y): return first.global_position.y < second.global_position.y return first.global_position.x < second.global_position.x ) candidates[0].grab_focus() func _collect_focusable_player_controls( root: Node, output: Array[Control], ) -> void: for child: Node in root.get_children(): if child == _tabs: continue var control := child as Control if control != null and not control.is_visible_in_tree(): continue if ( control != null and control.focus_mode != Control.FOCUS_NONE and not control is ScrollBar ): output.append(control) _collect_focusable_player_controls(child, output)