feat(network): add selectable privacy-preserving discovery
This commit is contained in:
parent
9cf0c8fc9c
commit
4d31fd1517
26 changed files with 959 additions and 153 deletions
|
|
@ -1,8 +1,8 @@
|
|||
class_name JoinGamePage
|
||||
extends Control
|
||||
|
||||
signal join_requested(endpoint: String)
|
||||
signal join_confirmation_requested(endpoint: String)
|
||||
signal join_requested(route: ConnectionRoute)
|
||||
signal join_confirmation_requested(endpoint: String, relayed: bool)
|
||||
signal back_requested
|
||||
|
||||
enum Mode {
|
||||
|
|
@ -17,14 +17,23 @@ const DISCOVERY_REFRESH_SECONDS: float = 8.0
|
|||
const ControllerFocusNavigationType = preload(
|
||||
"res://ui/controller_focus_navigation.gd"
|
||||
)
|
||||
const ADDRESS_HIDDEN_ICON: Texture2D = preload(
|
||||
"res://ui/icons/address_hidden.svg"
|
||||
)
|
||||
const ADDRESS_VISIBLE_ICON: Texture2D = preload(
|
||||
"res://ui/icons/address_visible.svg"
|
||||
)
|
||||
|
||||
const ADDRESS_FORMAT_HELP: String = (
|
||||
"Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; "
|
||||
+ "include the port shown by a host when it differs."
|
||||
)
|
||||
const DIRECT_WORKFLOW_HELP: String = (
|
||||
"%s\nJoin Now connects once; Save Server stores this address locally."
|
||||
% ADDRESS_FORMAT_HELP
|
||||
"Direct connections expose your public IP address to the remote host. "
|
||||
+ "Only connect to people you trust.\n"
|
||||
+ ADDRESS_FORMAT_HELP
|
||||
+ "\n"
|
||||
+ "Join Now connects once; Save Server stores this address locally."
|
||||
)
|
||||
|
||||
@onready var _main_panel: PanelContainer = %MainPanel
|
||||
|
|
@ -33,6 +42,9 @@ const DIRECT_WORKFLOW_HELP: String = (
|
|||
@onready var _list_content: Control = %ListContent
|
||||
@onready var _list_title: Label = %ListTitle
|
||||
@onready var _online_controls: Control = %OnlineControls
|
||||
@onready var _address_privacy_controls: Control = %AddressPrivacyControls
|
||||
@onready var _address_privacy_label: Label = %AddressPrivacyLabel
|
||||
@onready var _address_visibility_button: Button = %AddressVisibilityButton
|
||||
@onready var _presence_button: Button = %PresenceButton
|
||||
@onready var _room_open_button: Button = %RoomOpenButton
|
||||
@onready var _room_listing_button: Button = %RoomListingButton
|
||||
|
|
@ -84,6 +96,7 @@ var _connection_error_latched: bool = false
|
|||
var _pending_confirmation_endpoint: String = ""
|
||||
var _pending_confirmation_room: Dictionary = {}
|
||||
var _owns_pending_public_join: bool = false
|
||||
var _addresses_visible: bool = false
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
|
|
@ -96,6 +109,9 @@ func _ready() -> void:
|
|||
_presence_button.pressed.connect(_on_presence_pressed)
|
||||
_room_open_button.pressed.connect(_on_room_open_pressed)
|
||||
_room_listing_button.pressed.connect(_on_room_listing_pressed)
|
||||
_address_visibility_button.pressed.connect(
|
||||
_on_address_visibility_pressed
|
||||
)
|
||||
_refresh_button.pressed.connect(_request_discovery_refresh)
|
||||
_join_button.pressed.connect(_request_join)
|
||||
_save_button.pressed.connect(_on_save_pressed)
|
||||
|
|
@ -310,6 +326,7 @@ func setup(
|
|||
|
||||
|
||||
func open_page(preserved_endpoint: String = "") -> void:
|
||||
_addresses_visible = false
|
||||
if not preserved_endpoint.is_empty():
|
||||
_address.text = preserved_endpoint
|
||||
elif _address.text.is_empty():
|
||||
|
|
@ -368,7 +385,11 @@ func confirm_pending_join() -> bool:
|
|||
return false
|
||||
return true
|
||||
_set_status("Connecting…")
|
||||
join_requested.emit(endpoint_text)
|
||||
var endpoint := EndpointParser.parse(endpoint_text)
|
||||
if not endpoint.is_valid():
|
||||
_set_status(endpoint.error_message, true)
|
||||
return false
|
||||
join_requested.emit(ConnectionRoute.direct(endpoint))
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -405,7 +426,7 @@ func _default_mode_status(mode: Mode) -> String:
|
|||
Mode.FRIENDS:
|
||||
return "friend status is live and not stored by discovery"
|
||||
Mode.DIRECT:
|
||||
return "direct connection • default port 7777"
|
||||
return "direct connection • your public IP is visible to the host"
|
||||
Mode.SAVED:
|
||||
return "saved servers are stored on this device"
|
||||
Mode.RECENT:
|
||||
|
|
@ -447,18 +468,26 @@ func _request_join() -> void:
|
|||
if _discovery == null:
|
||||
_set_status("Could not prepare the public connection.", true)
|
||||
return
|
||||
endpoint_text = _discovery.room_endpoint(room)
|
||||
discovery_room = room.duplicate(true)
|
||||
elif _mode != Mode.DIRECT and _selected_entry != null:
|
||||
endpoint_text = _selected_entry.normalized_endpoint
|
||||
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
|
||||
if not endpoint.is_valid():
|
||||
_set_status(endpoint.error_message, true)
|
||||
return
|
||||
var endpoint: ConnectionEndpoint
|
||||
if discovery_room.is_empty():
|
||||
endpoint = EndpointParser.parse(endpoint_text)
|
||||
if not endpoint.is_valid():
|
||||
_set_status(endpoint.error_message, true)
|
||||
return
|
||||
if _gameplay_context:
|
||||
_pending_confirmation_endpoint = endpoint.normalized_display
|
||||
_pending_confirmation_endpoint = (
|
||||
"public room"
|
||||
if not discovery_room.is_empty()
|
||||
else endpoint.normalized_display
|
||||
)
|
||||
_pending_confirmation_room = discovery_room
|
||||
join_confirmation_requested.emit(endpoint.normalized_display)
|
||||
join_confirmation_requested.emit(
|
||||
_pending_confirmation_endpoint,
|
||||
_room_uses_relay(discovery_room),
|
||||
)
|
||||
return
|
||||
if not discovery_room.is_empty():
|
||||
_owns_pending_public_join = true
|
||||
|
|
@ -468,22 +497,20 @@ func _request_join() -> void:
|
|||
return
|
||||
_set_status("Connecting…")
|
||||
_address.text = endpoint.normalized_display
|
||||
join_requested.emit(endpoint.normalized_display)
|
||||
join_requested.emit(ConnectionRoute.direct(endpoint))
|
||||
|
||||
|
||||
func _on_public_join_prepared(endpoint_text: String) -> void:
|
||||
func _on_public_join_prepared(route: ConnectionRoute) -> void:
|
||||
if not _owns_pending_public_join:
|
||||
return
|
||||
_owns_pending_public_join = false
|
||||
if _mode not in [Mode.DISCOVER, Mode.FRIENDS] or not is_visible_in_tree():
|
||||
return
|
||||
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
|
||||
if not endpoint.is_valid():
|
||||
_set_status("Discovery returned an invalid room address.", true)
|
||||
if route == null or not route.is_valid() or not route.is_discovery_join():
|
||||
_set_status("Could not reach this room.", true)
|
||||
return
|
||||
_address.text = endpoint.normalized_display
|
||||
_set_status("Connecting…")
|
||||
join_requested.emit(endpoint.normalized_display)
|
||||
join_requested.emit(route)
|
||||
|
||||
|
||||
func _on_public_join_status_changed(message: String, is_error: bool) -> void:
|
||||
|
|
@ -518,7 +545,7 @@ func _on_save_pressed() -> void:
|
|||
)
|
||||
if existing != null:
|
||||
_set_status(
|
||||
"Already saved locally as “%s”." % existing.display_name,
|
||||
"Already saved locally as “%s”." % _display_entry_name(existing),
|
||||
true,
|
||||
)
|
||||
_set_mode(Mode.SAVED)
|
||||
|
|
@ -530,7 +557,7 @@ func _on_save_pressed() -> void:
|
|||
if _mode == Mode.RECENT
|
||||
and _selected_entry != null
|
||||
and not _selected_entry.last_observed_server_name.is_empty()
|
||||
else endpoint.host
|
||||
else "Saved Server"
|
||||
)
|
||||
_name_entry_active = true
|
||||
_name_edit.show()
|
||||
|
|
@ -559,7 +586,7 @@ func _commit_name_entry() -> void:
|
|||
_set_status(
|
||||
"Server updated."
|
||||
if was_editing
|
||||
else "Saved locally as “%s”." % saved.display_name
|
||||
else "Saved locally as “%s”." % _display_entry_name(saved)
|
||||
)
|
||||
_clear_edit_state()
|
||||
_mode = Mode.SAVED
|
||||
|
|
@ -572,7 +599,17 @@ func _on_edit_pressed() -> void:
|
|||
return
|
||||
_editing_entry_id = _selected_entry.entry_id
|
||||
_address.text = _selected_entry.normalized_endpoint
|
||||
_name_edit.text = _selected_entry.display_name
|
||||
_name_edit.text = (
|
||||
_selected_entry.display_name
|
||||
if (
|
||||
_addresses_visible
|
||||
or not _entry_label_reveals_address(
|
||||
_selected_entry.display_name,
|
||||
_selected_entry,
|
||||
)
|
||||
)
|
||||
else "Saved Server"
|
||||
)
|
||||
_name_entry_active = true
|
||||
_name_edit.show()
|
||||
_save_button.text = "save"
|
||||
|
|
@ -789,8 +826,8 @@ func _refresh_entries() -> void:
|
|||
"%s%s — %s%s"
|
||||
% [
|
||||
prefix,
|
||||
entry.display_name,
|
||||
entry.normalized_endpoint,
|
||||
_display_entry_name(entry),
|
||||
_display_endpoint(entry.normalized_endpoint),
|
||||
count,
|
||||
]
|
||||
)
|
||||
|
|
@ -822,6 +859,7 @@ func _refresh() -> void:
|
|||
var list_content_visible: bool = not direct_content_visible
|
||||
_direct_content.visible = direct_content_visible
|
||||
_list_content.visible = list_content_visible
|
||||
_refresh_address_privacy_controls()
|
||||
_list_title.text = _current_list_title()
|
||||
_refresh_online_controls(discovery_mode, friends_mode, connecting)
|
||||
_address.visible = direct or _name_entry_active
|
||||
|
|
@ -978,8 +1016,10 @@ func _refresh_online_controls(
|
|||
if not local_host:
|
||||
return
|
||||
var room_open: bool = _network_session.is_open_host()
|
||||
var room_listed: bool = (
|
||||
_discovery != null and _discovery.is_discoverable()
|
||||
var host_discovery_mode: String = (
|
||||
_discovery.get_host_discovery_mode()
|
||||
if _discovery != null
|
||||
else DiscoveryClient.HOST_DISCOVERY_PRIVATE
|
||||
)
|
||||
_room_open_button.text = "room: open" if room_open else "room: closed"
|
||||
_room_open_button.disabled = connecting
|
||||
|
|
@ -988,9 +1028,7 @@ func _refresh_online_controls(
|
|||
if room_open
|
||||
else "Open this room so other players can connect."
|
||||
)
|
||||
_room_listing_button.text = (
|
||||
"listing: on" if room_listed else "listing: off"
|
||||
)
|
||||
_room_listing_button.text = "discovery | %s" % host_discovery_mode
|
||||
_room_listing_button.disabled = (
|
||||
connecting
|
||||
or _discovery == null
|
||||
|
|
@ -998,9 +1036,14 @@ func _refresh_online_controls(
|
|||
or not room_open
|
||||
)
|
||||
_room_listing_button.tooltip_text = (
|
||||
"Remove this room from the public room browser."
|
||||
if room_listed
|
||||
else "List this open room in the public room browser."
|
||||
"Stop discovery listing. Existing relay connections remain active "
|
||||
+ "until those players disconnect."
|
||||
if host_discovery_mode == DiscoveryClient.HOST_CONNECTION_RELAY
|
||||
else "Switch this public listing to the privacy relay. Relay hosting "
|
||||
+ "hides peer IPs but may add latency and uses shared server capacity."
|
||||
if host_discovery_mode == DiscoveryClient.HOST_CONNECTION_DIRECT
|
||||
else "List this room through direct discovery. Joining players and the "
|
||||
+ "host can see one another's public IP addresses."
|
||||
if room_open and _discovery != null and _discovery.is_configured()
|
||||
else "Open the room before enabling its public listing."
|
||||
if _discovery != null and _discovery.is_configured()
|
||||
|
|
@ -1025,6 +1068,7 @@ func _configure_controller_navigation() -> void:
|
|||
online_controls.append(control)
|
||||
var content_controls: Array[Control] = []
|
||||
for control: Control in [
|
||||
_address_visibility_button,
|
||||
_address,
|
||||
_name_edit,
|
||||
_server_list,
|
||||
|
|
@ -1056,6 +1100,7 @@ func _configure_controller_navigation() -> void:
|
|||
_presence_button,
|
||||
_room_open_button,
|
||||
_room_listing_button,
|
||||
_address_visibility_button,
|
||||
_address,
|
||||
_name_edit,
|
||||
_server_list,
|
||||
|
|
@ -1219,8 +1264,8 @@ func _recover_controller_focus(
|
|||
func _format_entry_details(entry: SavedServerEntry) -> String:
|
||||
var parts: Array[String] = []
|
||||
if _mode == Mode.SAVED:
|
||||
parts.append("Name: %s" % entry.display_name)
|
||||
parts.append("Address: %s" % entry.normalized_endpoint)
|
||||
parts.append("Name: %s" % _display_entry_name(entry))
|
||||
parts.append("Address: %s" % _display_endpoint(entry.normalized_endpoint))
|
||||
if _server_trust != null:
|
||||
var trust := _server_trust.get_record(entry.get_endpoint())
|
||||
if trust.is_empty():
|
||||
|
|
@ -1239,7 +1284,10 @@ func _format_entry_details(entry: SavedServerEntry) -> String:
|
|||
or entry.last_observed_server_name != entry.display_name
|
||||
)
|
||||
):
|
||||
parts.append("Server name: %s" % entry.last_observed_server_name)
|
||||
parts.append(
|
||||
"Server name: %s"
|
||||
% _display_entry_label(entry.last_observed_server_name, entry)
|
||||
)
|
||||
if entry.last_observed_max_players > 0:
|
||||
parts.append(
|
||||
"Players: %d / %d"
|
||||
|
|
@ -1259,22 +1307,139 @@ func _format_entry_details(entry: SavedServerEntry) -> String:
|
|||
parts.append(
|
||||
"Last result: %s" % _format_result_code(entry.last_result_code)
|
||||
)
|
||||
parts.append("Connection: direct peer-to-peer • not relayed")
|
||||
parts.append("Joining shares your public IP address with the remote host.")
|
||||
return "\n".join(parts)
|
||||
|
||||
|
||||
func _on_address_visibility_pressed() -> void:
|
||||
_addresses_visible = not _addresses_visible
|
||||
if _mode in [Mode.SAVED, Mode.RECENT]:
|
||||
var selected_id := (
|
||||
_selected_entry.entry_id if _selected_entry != null else ""
|
||||
)
|
||||
_refresh_entries()
|
||||
if not selected_id.is_empty():
|
||||
_select_entry_id(selected_id)
|
||||
_refresh()
|
||||
|
||||
|
||||
func _refresh_address_privacy_controls() -> void:
|
||||
var relevant := (
|
||||
_mode in [Mode.DIRECT, Mode.SAVED, Mode.RECENT]
|
||||
or _name_entry_active
|
||||
)
|
||||
_address_privacy_controls.visible = relevant
|
||||
_address.secret = not _addresses_visible
|
||||
_address_privacy_label.text = (
|
||||
"addresses visible"
|
||||
if _addresses_visible
|
||||
else "addresses hidden for streamer safety"
|
||||
)
|
||||
_address_visibility_button.icon = (
|
||||
ADDRESS_VISIBLE_ICON if _addresses_visible else ADDRESS_HIDDEN_ICON
|
||||
)
|
||||
_address_visibility_button.tooltip_text = (
|
||||
"Hide server addresses"
|
||||
if _addresses_visible
|
||||
else "Show server addresses"
|
||||
)
|
||||
_address_visibility_button.accessibility_name = (
|
||||
"hide server addresses"
|
||||
if _addresses_visible
|
||||
else "show server addresses"
|
||||
)
|
||||
|
||||
|
||||
func _display_endpoint(endpoint: String) -> String:
|
||||
return endpoint if _addresses_visible else "*****"
|
||||
|
||||
|
||||
func _display_entry_name(entry: SavedServerEntry) -> String:
|
||||
if entry == null:
|
||||
return "server"
|
||||
if (
|
||||
_addresses_visible
|
||||
or not _entry_label_reveals_address(entry.display_name, entry)
|
||||
):
|
||||
return entry.display_name
|
||||
return (
|
||||
"saved server"
|
||||
if entry.kind == SavedServerEntry.Kind.SAVED
|
||||
else "recent server"
|
||||
)
|
||||
|
||||
|
||||
func _display_entry_label(
|
||||
label: String,
|
||||
entry: SavedServerEntry,
|
||||
) -> String:
|
||||
if _addresses_visible or not _entry_label_reveals_address(label, entry):
|
||||
return label
|
||||
return "*****"
|
||||
|
||||
|
||||
func _entry_label_reveals_address(
|
||||
label: String,
|
||||
entry: SavedServerEntry,
|
||||
) -> bool:
|
||||
if entry == null:
|
||||
return false
|
||||
var normalized_label: String = label.strip_edges().to_lower()
|
||||
if normalized_label.is_empty():
|
||||
return false
|
||||
for candidate: String in [
|
||||
entry.normalized_endpoint,
|
||||
entry.normalized_host,
|
||||
entry.host,
|
||||
]:
|
||||
var normalized_candidate: String = candidate.strip_edges().to_lower()
|
||||
if normalized_candidate.is_empty():
|
||||
continue
|
||||
if normalized_label == normalized_candidate:
|
||||
return true
|
||||
if (
|
||||
normalized_candidate.contains(".")
|
||||
or normalized_candidate.contains(":")
|
||||
) and normalized_label.contains(normalized_candidate):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _format_discovery_details(room: Dictionary) -> String:
|
||||
if room.is_empty():
|
||||
return "Select a public room."
|
||||
var relayed := _room_uses_relay(room)
|
||||
var lines: Array[String] = [
|
||||
"Room: %s" % str(room.get("room_name", "Public room")),
|
||||
"Host: %s" % (
|
||||
"dedicated server operator"
|
||||
if str(room.get("host_kind", "player")) == "dedicated"
|
||||
else "another player"
|
||||
),
|
||||
"Players: %d / %d" % [
|
||||
int(room.get("current_players", 0)),
|
||||
int(room.get("max_players", 0)),
|
||||
],
|
||||
"Connection: %s" % (
|
||||
_discovery.room_endpoint(room) if _discovery != null else "—"
|
||||
(
|
||||
"Connection: privacy relay"
|
||||
if relayed
|
||||
else "Connection: direct peer-to-peer • not relayed"
|
||||
),
|
||||
(
|
||||
"IP privacy: the other player sees the relay address, not your "
|
||||
+ "residential IP. The relay service still handles both addresses."
|
||||
if relayed
|
||||
else "IP privacy: joining shares your public IP address with the host."
|
||||
),
|
||||
(
|
||||
"Relay failure stops the join; straywild will not silently fall back "
|
||||
+ "to a direct connection. The extra network hop may increase latency."
|
||||
if relayed
|
||||
else "The room address is disclosed to your game only after you press "
|
||||
+ "Join; it is not displayed or saved. Use a VPN if you need stronger "
|
||||
+ "privacy."
|
||||
),
|
||||
"Direct UDP connection • ping is measured after joining",
|
||||
]
|
||||
if _discovery != null and _discovery.is_own_room(room):
|
||||
lines.append("This is the room you are currently hosting.")
|
||||
|
|
@ -1292,11 +1457,28 @@ func _format_friend_details(friend: Dictionary) -> String:
|
|||
),
|
||||
]
|
||||
if not room.is_empty():
|
||||
var relayed := _room_uses_relay(room)
|
||||
lines.append("Room: %s" % str(room.get("room_name", "Public room")))
|
||||
lines.append("Host: %s" % (
|
||||
"dedicated server operator"
|
||||
if str(room.get("host_kind", "player")) == "dedicated"
|
||||
else "another player"
|
||||
))
|
||||
lines.append("Players: %d / %d" % [
|
||||
int(room.get("current_players", 0)),
|
||||
int(room.get("max_players", 0)),
|
||||
])
|
||||
lines.append(
|
||||
"Connection: privacy relay"
|
||||
if relayed
|
||||
else "Connection: direct peer-to-peer • not relayed"
|
||||
)
|
||||
lines.append(
|
||||
"The other player sees the relay address; the relay service still "
|
||||
+ "handles both network addresses. The extra hop may add latency."
|
||||
if relayed
|
||||
else "Joining shares your public IP address with the host."
|
||||
)
|
||||
elif bool(friend.get("online", false)):
|
||||
lines.append("This friend is not in a joinable public room.")
|
||||
else:
|
||||
|
|
@ -1338,6 +1520,10 @@ func _discovery_room_is_full(room: Dictionary) -> bool:
|
|||
return int(room.get("current_players", 0)) >= int(room.get("max_players", 0))
|
||||
|
||||
|
||||
func _room_uses_relay(room: Dictionary) -> bool:
|
||||
return str(room.get("connection_mode", "")) == "relay"
|
||||
|
||||
|
||||
func _request_discovery_refresh() -> void:
|
||||
if (
|
||||
_discovery == null
|
||||
|
|
@ -1435,14 +1621,15 @@ func _on_room_open_pressed() -> void:
|
|||
func _on_room_listing_pressed() -> void:
|
||||
if _discovery == null:
|
||||
return
|
||||
var enabling: bool = not _discovery.is_discoverable()
|
||||
if not _discovery.set_discoverable(enabling):
|
||||
if not _discovery.cycle_host_discovery_mode():
|
||||
return
|
||||
_set_status(
|
||||
"The room is being listed publicly."
|
||||
if enabling
|
||||
else "The room is no longer listed publicly."
|
||||
)
|
||||
match _discovery.get_host_discovery_mode():
|
||||
DiscoveryClient.HOST_CONNECTION_DIRECT:
|
||||
_set_status("Discovery is listing this room as a direct connection.")
|
||||
DiscoveryClient.HOST_CONNECTION_RELAY:
|
||||
_set_status("Discovery is listing this room through the privacy relay.")
|
||||
_:
|
||||
_set_status("Discovery listing is private.")
|
||||
_refresh()
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
[gd_scene load_steps=5 format=3]
|
||||
[gd_scene load_steps=7 format=3]
|
||||
|
||||
[ext_resource type="Script" path="res://ui/network/join_game_page.gd" id="1_script"]
|
||||
[ext_resource type="Theme" path="res://ui/game_theme.tres" id="2_theme"]
|
||||
[ext_resource type="Script" path="res://ui/components/organizer_tab.gd" id="3_tab"]
|
||||
[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="4_confirmation"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/address_hidden.svg" id="5_address_hidden"]
|
||||
[ext_resource type="Texture2D" path="res://ui/icons/address_visible.svg" id="6_address_visible"]
|
||||
|
||||
[node name="JoinGamePage" type="Control"]
|
||||
visible = false
|
||||
|
|
@ -117,6 +119,33 @@ theme_override_constants/margin_bottom = 20
|
|||
layout_mode = 2
|
||||
theme_override_constants/separation = 12
|
||||
|
||||
[node name="AddressPrivacyControls" type="HBoxContainer" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout"]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
custom_minimum_size = Vector2(0, 40)
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="AddressPrivacyLabel" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/AddressPrivacyControls"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
size_flags_horizontal = 3
|
||||
theme_override_font_sizes/font_size = 14
|
||||
text = "addresses hidden for streamer safety"
|
||||
horizontal_alignment = 2
|
||||
vertical_alignment = 1
|
||||
|
||||
[node name="AddressVisibilityButton" type="Button" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/AddressPrivacyControls"]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(44, 40)
|
||||
layout_mode = 2
|
||||
focus_mode = 2
|
||||
tooltip_text = "Show server addresses"
|
||||
icon = ExtResource("5_address_hidden")
|
||||
expand_icon = true
|
||||
icon_max_width = 27
|
||||
accessibility_name = "show server addresses"
|
||||
|
||||
[node name="PageStack" type="Control" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout"]
|
||||
layout_mode = 2
|
||||
size_flags_vertical = 3
|
||||
|
|
@ -214,6 +243,8 @@ theme_override_font_sizes/font_size = 19
|
|||
placeholder_text = "example.net or 192.168.1.50:7777"
|
||||
alignment = 1
|
||||
max_length = 300
|
||||
secret = true
|
||||
secret_character = "*"
|
||||
|
||||
[node name="AddressHelper" type="Label" parent="MainPanel/OuterMargin/Layout/ContentPanel/ContentMargin/ContentLayout/PageStack/DirectContent"]
|
||||
unique_name_in_owner = true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue