netfishing/ui/players_page.gd

619 lines
18 KiB
GDScript3
Raw Permalink Normal View History

2026-07-29 23:10:51 -04:00
class_name PlayersPage
extends Control
const TOGGLE_STATE_COLOR := Color("c3dfe6")
2026-07-29 23:10:51 -04:00
var _service: NetworkPlayerListService
2026-08-10 10:04:05 -04:00
var _discovery: DiscoveryClient
var _count_label: Label
2026-07-29 23:10:51 -04:00
var _tabs: HBoxContainer
var _list: VBoxContainer
var _status: Label
2026-08-10 10:04:05 -04:00
var _host_settings_panel: PanelContainer
var _room_name_edit: LineEdit
var _online_toggle: Button
var _online_state_label: Label
var _discoverable_toggle: Button
var _discoverable_state_label: Label
2026-08-10 10:04:05 -04:00
var _host_discovery_status: Label
2026-07-29 23:10:51 -04:00
var _current_tab := 0
func _ready() -> void:
set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
UtilityPageStyle.apply_page(self)
2026-07-29 23:10:51 -04:00
_build()
2026-08-10 10:04:05 -04:00
func setup(
service: NetworkPlayerListService,
discovery: DiscoveryClient,
) -> void:
2026-07-29 23:10:51 -04:00
_service = service
2026-08-10 10:04:05 -04:00
_discovery = discovery
2026-07-29 23:10:51 -04:00
_service.entries_changed.connect(_refresh)
_service.moderation_finished.connect(func(_ok: bool, message: String) -> void:
_status.text = message
_refresh()
)
2026-08-10 10:04:05 -04:00
if _discovery != null:
_discovery.host_settings_changed.connect(
_on_host_settings_changed
)
_discovery.host_status_changed.connect(_on_host_status_changed)
2026-07-29 23:10:51 -04:00
_refresh()
func activate() -> void:
_refresh()
UtilityPageStyle.animate_in(self)
2026-07-29 23:10:51 -04:00
_focus_first()
func deactivate() -> void:
_status.text = ""
func _build() -> void:
var margin: MarginContainer = UtilityPageStyle.build_laptop_screen(self)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
_tabs = HBoxContainer.new()
_tabs.size_flags_horizontal = Control.SIZE_EXPAND_FILL
2026-07-29 23:10:51 -04:00
_tabs.add_theme_constant_override("separation", 10)
tab_row.add_child(_tabs)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
_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)
2026-08-10 10:04:05 -04:00
_build_host_settings(root)
2026-07-29 23:10:51 -04:00
var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(0, 310)
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
2026-07-29 23:10:51 -04:00
scroll.horizontal_scroll_mode = ScrollContainer.SCROLL_MODE_DISABLED
root.add_child(scroll)
_list = VBoxContainer.new()
_list.custom_minimum_size = Vector2(1032, 0)
2026-07-29 23:10:51 -04:00
_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
)
2026-07-29 23:10:51 -04:00
root.add_child(_status)
2026-08-10 10:04:05 -04:00
func _build_host_settings(root: VBoxContainer) -> void:
_host_settings_panel = PanelContainer.new()
_host_settings_panel.add_theme_stylebox_override(
"panel", UtilityPageStyle.row_style(false)
)
root.add_child(_host_settings_panel)
var row := HBoxContainer.new()
row.custom_minimum_size.y = 58.0
row.add_theme_constant_override("separation", 10)
_host_settings_panel.add_child(row)
var label := Label.new()
label.text = "room name"
label.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
row.add_child(label)
_room_name_edit = LineEdit.new()
_room_name_edit.custom_minimum_size = Vector2(300.0, 42.0)
_room_name_edit.max_length = DiscoveryClient.MAX_ROOM_NAME_LENGTH
_room_name_edit.placeholder_text = "Player Name's Server"
2026-08-10 10:04:05 -04:00
UtilityPageStyle.apply_ocean_line_edit(_room_name_edit)
_room_name_edit.text_submitted.connect(
func(_value: String) -> void: _commit_room_name()
)
_room_name_edit.focus_exited.connect(_commit_room_name)
row.add_child(_room_name_edit)
_online_toggle = _build_host_toggle("online", row)
_online_state_label = _online_toggle.get_node("StateBadge/State") as Label
_online_toggle.pressed.connect(_on_online_pressed)
_discoverable_toggle = _build_host_toggle("discovery", row)
_discoverable_state_label = (
_discoverable_toggle.get_node("StateBadge/State") as Label
)
_discoverable_toggle.pressed.connect(_on_discoverable_pressed)
2026-08-10 10:04:05 -04:00
_host_discovery_status = Label.new()
_host_discovery_status.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_host_discovery_status.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
_host_discovery_status.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_host_discovery_status.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
2026-08-10 10:04:05 -04:00
_host_discovery_status.add_theme_font_size_override("font_size", 14)
_host_discovery_status.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY
)
row.add_child(_host_discovery_status)
func _build_host_toggle(label_text: String, row: HBoxContainer) -> Button:
var button := Button.new()
button.custom_minimum_size = Vector2(150.0, 46.0)
button.text = label_text
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.clip_contents = false
UtilityPageStyle.apply_ocean_button(button)
row.add_child(button)
var badge := PanelContainer.new()
badge.name = "StateBadge"
badge.mouse_filter = Control.MOUSE_FILTER_IGNORE
badge.z_index = 2
button.add_child(badge)
badge.set_anchors_preset(Control.PRESET_CENTER_BOTTOM)
badge.offset_left = -39.0
badge.offset_top = -12.0
badge.offset_right = 39.0
badge.offset_bottom = 12.0
var state_label := Label.new()
state_label.name = "State"
state_label.mouse_filter = Control.MOUSE_FILTER_IGNORE
state_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
state_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
state_label.add_theme_font_override("font", UtilityPageStyle.TuffyFont)
state_label.add_theme_font_size_override("font_size", 15)
state_label.add_theme_color_override("font_color", TOGGLE_STATE_COLOR)
badge.add_child(state_label)
return button
func _set_host_toggle_state(
button: Button,
state_label: Label,
state_text: String,
enabled: bool,
) -> void:
state_label.text = state_text
var badge := state_label.get_parent() as PanelContainer
badge.add_theme_stylebox_override(
"panel",
UtilityPageStyle.rounded_style(
UtilityPageStyle.OCEAN_SELECTED
if enabled
else UtilityPageStyle.OCEAN_FIELD,
12,
),
)
button.queue_redraw()
2026-07-29 23:10:51 -04:00
func _select_tab(index: int) -> void:
2026-08-11 22:58:33 -04:00
if index == 2 and (_service == null or not _service.is_local_moderator()):
2026-07-29 23:10:51 -04:00
return
_current_tab = index
_refresh()
_focus_first()
func _refresh() -> void:
if _service == null or _list == null:
return
_count_label.text = "%d / %d connected" % [
2026-07-29 23:10:51 -04:00
_service.get_connected_count(), _service.get_max_players(),
]
2026-08-11 22:58:33 -04:00
if _current_tab == 2 and not _service.is_local_moderator():
_current_tab = 0
2026-07-29 23:10:51 -04:00
for index: int in _tabs.get_child_count():
var button := _tabs.get_child(index) as Button
button.button_pressed = index == _current_tab
2026-08-11 22:58:33 -04:00
button.visible = index != 2 or _service.is_local_moderator()
2026-08-10 10:04:05 -04:00
_refresh_host_settings()
2026-07-29 23:10:51 -04:00
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()
2026-08-10 10:04:05 -04:00
func _refresh_host_settings() -> void:
if _host_settings_panel == null:
return
var host_visible: bool = _service.is_local_host() and _current_tab == 0
_host_settings_panel.visible = host_visible
if not host_visible or _discovery == null:
return
if not _room_name_edit.has_focus():
_room_name_edit.text = _discovery.get_room_name()
var is_open: bool = _service.is_open_host()
var is_discoverable: bool = _discovery.is_discoverable()
_set_host_toggle_state(
_online_toggle,
_online_state_label,
"OPEN" if is_open else "CLOSED",
is_open,
)
_set_host_toggle_state(
_discoverable_toggle,
_discoverable_state_label,
"ON" if is_discoverable else "OFF",
is_discoverable,
)
_online_toggle.disabled = not _service.is_local_host()
_online_toggle.tooltip_text = (
"Close this game to new connections."
if is_open
else "Open this game so other players can connect."
)
2026-08-10 10:04:05 -04:00
var can_publish: bool = (
_service.is_local_host()
and _discovery.is_configured()
and is_open
2026-08-10 10:04:05 -04:00
)
_discoverable_toggle.disabled = not can_publish
_discoverable_toggle.tooltip_text = (
"Stop advertising this game in the public room browser."
if is_discoverable
else "Advertise this open game in the public room browser."
2026-08-10 10:04:05 -04:00
if can_publish
else "Open the game before enabling discovery."
2026-08-10 10:04:05 -04:00
if _discovery.is_configured()
else "Room discovery is not configured in this build."
)
_on_host_status_changed(
_discovery.get_host_status_message(),
_discovery.host_status_is_error(),
)
func _commit_room_name() -> void:
if _discovery == null:
return
if not _discovery.set_room_name(_room_name_edit.text):
_room_name_edit.text = _discovery.get_room_name()
else:
_room_name_edit.text = _discovery.get_room_name()
func _on_online_pressed() -> void:
if _service == null or not _service.is_local_host():
return
_service.set_host_open(not _service.is_open_host())
_refresh_host_settings()
func _on_discoverable_pressed() -> void:
2026-08-10 10:04:05 -04:00
if _discovery == null:
return
_discovery.set_discoverable(not _discovery.is_discoverable())
_refresh_host_settings()
2026-08-10 10:04:05 -04:00
func _on_host_settings_changed(
room_name: String,
discoverable: bool,
) -> void:
if _room_name_edit != null and not _room_name_edit.has_focus():
_room_name_edit.text = room_name
if _discoverable_state_label != null:
_set_host_toggle_state(
_discoverable_toggle,
_discoverable_state_label,
"ON" if discoverable else "OFF",
discoverable,
)
2026-08-10 10:04:05 -04:00
_refresh_host_settings()
func _on_host_status_changed(message: String, is_error: bool) -> void:
if _host_discovery_status == null:
return
var tooltip_only: bool = message in [
"Open the game before listing it publicly.",
"Open the game before enabling discovery.",
]
_host_discovery_status.text = "" if tooltip_only else message
_host_discovery_status.visible = not _host_discovery_status.text.is_empty()
2026-08-10 10:04:05 -04:00
_host_discovery_status.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_DANGER
if is_error
else UtilityPageStyle.OCEAN_TEXT_SECONDARY,
)
2026-07-29 23:10:51 -04:00
func _build_active_rows() -> void:
2026-08-11 22:58:33 -04:00
if _service.is_local_moderator():
2026-08-01 22:55:39 -04:00
_build_session_artwork_controls()
2026-07-29 23:10:51 -04:00
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()
2026-08-11 22:58:33 -04:00
identity.custom_minimum_size.x = 430
2026-07-29 23:10:51 -04:00
var markers: Array[String] = []
if entry.is_host:
markers.append("host")
2026-08-11 22:58:33 -04:00
if entry.is_operator:
markers.append("operator")
2026-07-29 23:10:51 -04:00
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,
]
2026-08-11 22:58:33 -04:00
identity.tooltip_text = "Full identity fingerprint:\n%s" % (
2026-07-29 23:10:51 -04:00
entry.full_fingerprint
)
identity.add_theme_color_override(
"font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY
)
2026-07-29 23:10:51 -04:00
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
)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
row.add_child(block)
2026-08-11 22:58:33 -04:00
if entry.can_manage_operator:
var operator := Button.new()
operator.text = "deop" if entry.is_operator else "op"
operator.pressed.connect(_confirm_operator.bind(entry))
UtilityPageStyle.apply_ocean_button(operator)
row.add_child(operator)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
row.add_child(ban)
2026-08-01 22:55:39 -04:00
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)
2026-07-29 23:10:51 -04:00
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
)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
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
)
2026-07-29 23:10:51 -04:00
row.add_child(label)
var unban := Button.new()
unban.text = "unban"
unban.pressed.connect(_confirm_unban.bind(fingerprint))
UtilityPageStyle.apply_ocean_button(unban)
2026-07-29 23:10:51 -04:00
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)
2026-07-29 23:10:51 -04:00
var row := HBoxContainer.new()
row.custom_minimum_size.y = 58
row.add_theme_constant_override("separation", 8)
panel.add_child(row)
2026-07-29 23:10:51 -04:00
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
)
2026-07-29 23:10:51 -04:00
_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
)
)
2026-08-11 22:58:33 -04:00
func _confirm_operator(entry: PlayerListEntry) -> void:
var enabled: bool = not entry.is_operator
_confirm(
(
"Grant operator access to %s for this room session?"
if enabled
else "Remove operator access from %s?"
) % entry.display_name,
func() -> void:
_service.set_operator(
entry.peer_id,
entry.full_fingerprint,
enabled,
entry.revision,
)
)
2026-07-29 23:10:51 -04:00
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)