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
4
ui/icons/address_hidden.svg
Normal file
4
ui/icons/address_hidden.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path d="M4 16c3.2-5 7.2-7.5 12-7.5 4.8 0 8.8 2.5 12 7.5-3.2 5-7.2 7.5-12 7.5C11.2 23.5 7.2 21 4 16Z" fill="none" stroke="#062735" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<path d="M7 5 25 27" fill="none" stroke="#062735" stroke-width="3" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 363 B |
43
ui/icons/address_hidden.svg.import
Normal file
43
ui/icons/address_hidden.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bot2k4h3wnroh"
|
||||
path="res://.godot/imported/address_hidden.svg-fd3f3bc53a766848efcd26f62867a980.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://ui/icons/address_hidden.svg"
|
||||
dest_files=["res://.godot/imported/address_hidden.svg-fd3f3bc53a766848efcd26f62867a980.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
4
ui/icons/address_visible.svg
Normal file
4
ui/icons/address_visible.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
|
||||
<path d="M3 16c3.4-5.3 7.8-8 13-8s9.6 2.7 13 8c-3.4 5.3-7.8 8-13 8S6.4 21.3 3 16Z" fill="none" stroke="#062735" stroke-width="2.5" stroke-linejoin="round"/>
|
||||
<circle cx="16" cy="16" r="4.5" fill="none" stroke="#062735" stroke-width="2.5"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
43
ui/icons/address_visible.svg.import
Normal file
43
ui/icons/address_visible.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dbmqtwisapw4n"
|
||||
path="res://.godot/imported/address_visible.svg-da2e41f3b965e45cfc3839b82d107ccf.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://ui/icons/address_visible.svg"
|
||||
dest_files=["res://.godot/imported/address_visible.svg-da2e41f3b965e45cfc3839b82d107ccf.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ signal return_to_title_requested
|
|||
signal reset_progress_requested
|
||||
signal quit_requested
|
||||
signal menu_visibility_changed(is_open: bool)
|
||||
signal join_game_requested(endpoint: String)
|
||||
signal join_game_requested(route: ConnectionRoute)
|
||||
|
||||
enum ConfirmationAction {
|
||||
NONE,
|
||||
|
|
@ -240,18 +240,29 @@ func _close_join_game() -> void:
|
|||
_begin_root_entry(true)
|
||||
|
||||
|
||||
func _on_join_confirmation_requested(_endpoint: String) -> void:
|
||||
func _on_join_confirmation_requested(
|
||||
_endpoint: String,
|
||||
relayed: bool,
|
||||
) -> void:
|
||||
if _action_in_progress or _root_transition_active:
|
||||
_join_game_page.cancel_pending_join_confirmation()
|
||||
return
|
||||
_confirmation_action = ConfirmationAction.JOIN_ANOTHER
|
||||
_confirmation_returns_to_join_game = true
|
||||
var privacy_message := (
|
||||
"this join uses a privacy relay. the other player sees the relay "
|
||||
+ "address instead of your residential IP; the relay service still "
|
||||
+ "handles both addresses and will not silently fall back to direct."
|
||||
if relayed
|
||||
else "this is a direct peer-to-peer connection, not a relay. the "
|
||||
+ "remote host can see your public IP address. discovery room "
|
||||
+ "addresses are not displayed or saved."
|
||||
)
|
||||
_confirmation_page.configure(
|
||||
(
|
||||
"join another game?\n\n"
|
||||
+ "your progression will be saved first. current players will be "
|
||||
+ "disconnected if you are hosting."
|
||||
),
|
||||
"join another game?\n\n"
|
||||
+ "your progression will be saved first. current players will be "
|
||||
+ "disconnected if you are hosting.\n\n"
|
||||
+ privacy_message,
|
||||
"save and join",
|
||||
"cancel",
|
||||
BubbleConfirmationPageType.InitialFocus.CONFIRM,
|
||||
|
|
@ -261,8 +272,8 @@ func _on_join_confirmation_requested(_endpoint: String) -> void:
|
|||
_show_confirmation()
|
||||
|
||||
|
||||
func _on_confirmed_join_requested(endpoint: String) -> void:
|
||||
join_game_requested.emit(endpoint)
|
||||
func _on_confirmed_join_requested(route: ConnectionRoute) -> void:
|
||||
join_game_requested.emit(route)
|
||||
|
||||
|
||||
func report_network_error(message: String) -> void:
|
||||
|
|
|
|||
|
|
@ -401,7 +401,10 @@ func _refresh_host_settings() -> void:
|
|||
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()
|
||||
var discovery_mode: String = _discovery.get_host_discovery_mode()
|
||||
var is_discoverable: bool = (
|
||||
discovery_mode != DiscoveryClient.HOST_DISCOVERY_PRIVATE
|
||||
)
|
||||
_set_host_toggle_state(
|
||||
_online_toggle,
|
||||
_online_state_label,
|
||||
|
|
@ -411,7 +414,7 @@ func _refresh_host_settings() -> void:
|
|||
_set_host_toggle_state(
|
||||
_discoverable_toggle,
|
||||
_discoverable_state_label,
|
||||
"ON" if is_discoverable else "OFF",
|
||||
discovery_mode.to_upper(),
|
||||
is_discoverable,
|
||||
)
|
||||
_online_toggle.disabled = not _service.is_local_host()
|
||||
|
|
@ -427,9 +430,11 @@ func _refresh_host_settings() -> void:
|
|||
)
|
||||
_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."
|
||||
"Stop discovery listing. Existing relayed players remain connected."
|
||||
if discovery_mode == DiscoveryClient.HOST_CONNECTION_RELAY
|
||||
else "Switch discovery from direct hosting to the privacy relay."
|
||||
if discovery_mode == DiscoveryClient.HOST_CONNECTION_DIRECT
|
||||
else "List this open game through direct discovery."
|
||||
if can_publish
|
||||
else "Open the game before enabling discovery."
|
||||
if _discovery.is_configured()
|
||||
|
|
@ -460,7 +465,7 @@ func _on_online_pressed() -> void:
|
|||
func _on_discoverable_pressed() -> void:
|
||||
if _discovery == null:
|
||||
return
|
||||
_discovery.set_discoverable(not _discovery.is_discoverable())
|
||||
_discovery.cycle_host_discovery_mode()
|
||||
_refresh_host_settings()
|
||||
|
||||
|
||||
|
|
@ -474,7 +479,7 @@ func _on_host_settings_changed(
|
|||
_set_host_toggle_state(
|
||||
_discoverable_toggle,
|
||||
_discoverable_state_label,
|
||||
"ON" if discoverable else "OFF",
|
||||
_discovery.get_host_discovery_mode().to_upper(),
|
||||
discoverable,
|
||||
)
|
||||
_refresh_host_settings()
|
||||
|
|
|
|||
|
|
@ -67,7 +67,7 @@ signal new_slot_requested(
|
|||
duplicate_source_slot_id: String,
|
||||
)
|
||||
signal quit_requested
|
||||
signal join_game_requested(endpoint: String)
|
||||
signal join_game_requested(route: ConnectionRoute)
|
||||
|
||||
enum ConfirmationAction {
|
||||
NONE,
|
||||
|
|
|
|||
|
|
@ -213,7 +213,7 @@ unique_name_in_owner = true
|
|||
layout_mode = 2
|
||||
theme_override_colors/font_color = Color(0.682, 0.733, 0.761, 1)
|
||||
theme_override_font_sizes/font_size = 22
|
||||
text = "v0.20.2-alpha"
|
||||
text = "v0.20.3-alpha"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="Spacer" type="Control" parent="ResponsiveTitleStage/TitlePresentationScaleRoot/Center/MainContent"]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue