Add public room discovery

This commit is contained in:
Alexander Sellite 2026-08-10 10:04:05 -04:00
parent 59ef173ea7
commit e204279b09
16 changed files with 857 additions and 22 deletions

View file

@ -33,6 +33,14 @@ node names, connect persistent and networked state to that data.
network services validate and replicate bounded domains such as fishing, network services validate and replicate bounded domains such as fishing,
sales, shops, item use, profiles, jobs, mail, chat, drawings, time, and weather. sales, shops, item use, profiles, jobs, mail, chat, drawings, time, and weather.
`DiscoveryClient` is an optional directory layer beside `NetworkSession`. An
open host may publish a short-lived room lease, and the shared Join Game page
may browse compatible leases before handing the selected address back to the
existing direct ENet connection flow. The directory does not carry gameplay
traffic or become a gameplay authority. Its base URL comes from
`network/discovery/base_url`, with `NETFISHING_DISCOVERY_URL` available as a
development/deployment override.
The host is authoritative. Clients submit requests or evidence; the host The host is authoritative. Clients submit requests or evidence; the host
derives trusted context from registered peers, authoritative regions, and derives trusted context from registered peers, authoritative regions, and
server-owned state before mutating inventory, wallet, progression, or shared server-owned state before mutating inventory, wallet, progression, or shared

View file

@ -38,6 +38,7 @@ const WorldPixelationPostprocessType = preload(
"res://main/world_pixelation_postprocess.gd" "res://main/world_pixelation_postprocess.gd"
) )
const NetworkSessionType = preload("res://network/network_session.gd") const NetworkSessionType = preload("res://network/network_session.gd")
const DiscoveryClientType = preload("res://network/discovery_client.gd")
const NetworkProfilePreferencesType = preload( const NetworkProfilePreferencesType = preload(
"res://network/network_profile_preferences.gd" "res://network/network_profile_preferences.gd"
) )
@ -131,6 +132,7 @@ const SHOP_PATTERN_SCALE: float = 1.75
%WorldPixelationPostprocess %WorldPixelationPostprocess
) )
@onready var _network_session: NetworkSessionType = %NetworkSession @onready var _network_session: NetworkSessionType = %NetworkSession
@onready var _discovery: DiscoveryClientType = %DiscoveryClient
@onready var _world_time: WorldTimeServiceType = %WorldTimeService @onready var _world_time: WorldTimeServiceType = %WorldTimeService
@onready var _world_time_visuals: WorldTimeVisualControllerType = ( @onready var _world_time_visuals: WorldTimeVisualControllerType = (
%WorldTimeVisualController %WorldTimeVisualController
@ -313,6 +315,7 @@ func _initialize_after_data_root() -> void:
_server_trust, _server_trust,
_host_bans, _host_bans,
) )
_discovery.setup(_network_session)
_world_time_visuals.setup( _world_time_visuals.setup(
_world_time, _world_time,
_test_world.get_world_environment(), _test_world.get_world_environment(),
@ -546,6 +549,7 @@ func _initialize_after_data_root() -> void:
_asset_reservations, _asset_reservations,
_network_profile_service, _network_profile_service,
_network_player_list, _network_player_list,
_discovery,
_settings_manager, _settings_manager,
_network_surface_drawing, _network_surface_drawing,
_player.art_unlocks, _player.art_unlocks,
@ -623,6 +627,7 @@ func _initialize_after_data_root() -> void:
_network_session, _network_session,
_saved_servers, _saved_servers,
_server_trust, _server_trust,
_discovery,
) )
var pause_menu: PauseMenuType = _game_ui.get_pause_menu() var pause_menu: PauseMenuType = _game_ui.get_pause_menu()
pause_menu.setup( pause_menu.setup(
@ -633,6 +638,7 @@ func _initialize_after_data_root() -> void:
_network_session, _network_session,
_saved_servers, _saved_servers,
_server_trust, _server_trust,
_discovery,
) )
title_screen.join_game_requested.connect(_on_title_join_game_requested) title_screen.join_game_requested.connect(_on_title_join_game_requested)
pause_menu.join_game_requested.connect(_on_pause_join_game_requested) pause_menu.join_game_requested.connect(_on_pause_join_game_requested)

View file

@ -56,6 +56,7 @@
[ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"] [ext_resource type="Script" uid="uid://b0c3o2vy76fg3" path="res://world/shoreline_ambience.gd" id="54_shoreline_ambience"]
[ext_resource type="AudioStream" uid="uid://dck3tf8qkadea" path="res://audio/ambience/waves.wav" id="55_waves"] [ext_resource type="AudioStream" uid="uid://dck3tf8qkadea" path="res://audio/ambience/waves.wav" id="55_waves"]
[ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"] [ext_resource type="Script" uid="uid://c80x8jkdnx0xy" path="res://settings/controller_mapping_manager.gd" id="56_controller_mapping"]
[ext_resource type="Script" uid="uid://d0jv8n2nfqia0" path="res://network/discovery_client.gd" id="57_discovery"]
[sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"] [sub_resource type="ShaderMaterial" id="ShaderMaterial_title_water_native"]
shader = ExtResource("40_title_water") shader = ExtResource("40_title_water")
@ -214,6 +215,10 @@ script = ExtResource("38_identity_backup")
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("17_network_session") script = ExtResource("17_network_session")
[node name="DiscoveryClient" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("57_discovery")
[node name="NetworkProfilePreferences" type="Node" parent="." unique_id=2119445545] [node name="NetworkProfilePreferences" type="Node" parent="." unique_id=2119445545]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("18_network_profile") script = ExtResource("18_network_profile")

478
network/discovery_client.gd Normal file
View file

@ -0,0 +1,478 @@
class_name DiscoveryClient
extends Node
signal rooms_updated(rooms: Array[Dictionary])
signal browse_status_changed(message: String, is_error: bool)
signal host_settings_changed(room_name: String, discoverable: bool)
signal host_status_changed(message: String, is_error: bool)
const BASE_URL_SETTING: String = "network/discovery/base_url"
const BASE_URL_ENVIRONMENT: String = "NETFISHING_DISCOVERY_URL"
const SETTINGS_PATH: String = "user://network_discovery.cfg"
const DEFAULT_ROOM_NAME: String = "NETfishing Room"
const MAX_ROOM_NAME_LENGTH: int = 48
const HEARTBEAT_INTERVAL_SECONDS: float = 15.0
const REQUEST_TIMEOUT_SECONDS: float = 8.0
enum HostRequestKind {
NONE,
CREATE,
UPDATE,
DELETE,
}
var _session: NetworkSession
var _base_url: String = ""
var _room_name: String = DEFAULT_ROOM_NAME
var _discoverable: bool = false
var _host_status_message: String = ""
var _host_status_is_error: bool = false
var _lease_room_id: String = ""
var _lease_token: String = ""
var _host_request_kind: HostRequestKind = HostRequestKind.NONE
var _host_request_in_flight: bool = false
var _host_sync_queued: bool = false
var _browse_request_in_flight: bool = false
var _host_request: HTTPRequest
var _browse_request: HTTPRequest
var _heartbeat: Timer
func _ready() -> void:
_host_request = HTTPRequest.new()
_host_request.name = "HostLeaseRequest"
_host_request.timeout = REQUEST_TIMEOUT_SECONDS
add_child(_host_request)
_host_request.request_completed.connect(_on_host_request_completed)
_browse_request = HTTPRequest.new()
_browse_request.name = "RoomBrowseRequest"
_browse_request.timeout = REQUEST_TIMEOUT_SECONDS
add_child(_browse_request)
_browse_request.request_completed.connect(_on_browse_request_completed)
_heartbeat = Timer.new()
_heartbeat.name = "HostLeaseHeartbeat"
_heartbeat.wait_time = HEARTBEAT_INTERVAL_SECONDS
_heartbeat.one_shot = false
add_child(_heartbeat)
_heartbeat.timeout.connect(_synchronize_host_lease)
_load_settings()
_base_url = _configured_base_url()
func setup(session: NetworkSession) -> void:
_session = session
_session.set_session_display_name(_room_name)
if not _session.state_changed.is_connected(_on_session_state_changed):
_session.state_changed.connect(_on_session_state_changed)
if not _session.peer_count_changed.is_connected(_on_peer_count_changed):
_session.peer_count_changed.connect(_on_peer_count_changed)
if not _session.host_openness_changed.is_connected(_on_host_openness_changed):
_session.host_openness_changed.connect(_on_host_openness_changed)
host_settings_changed.emit(_room_name, _discoverable)
if not is_configured():
_set_host_status("Room discovery is not configured in this build.", true)
elif _session.is_open_host():
_set_host_status("Room is open but unlisted.", false)
else:
_set_host_status("Open the game before listing it publicly.", false)
func is_configured() -> bool:
return not _base_url.is_empty()
func get_base_url() -> String:
return _base_url
func get_room_name() -> String:
return _room_name
func is_discoverable() -> bool:
return _discoverable
func get_host_status_message() -> String:
return _host_status_message
func host_status_is_error() -> bool:
return _host_status_is_error
func set_room_name(value: String) -> bool:
var cleaned: String = _sanitize_room_name(value)
if cleaned.is_empty():
_set_host_status("Room name cannot be empty.", true)
return false
if cleaned == _room_name:
return true
_room_name = cleaned
_save_settings()
if _session != null:
_session.set_session_display_name(_room_name)
host_settings_changed.emit(_room_name, _discoverable)
if _discoverable:
_synchronize_host_lease()
return true
func set_discoverable(enabled: bool) -> bool:
if enabled and (
_session == null
or not _session.is_open_host()
or not is_configured()
):
_set_host_status(
"Open the game before enabling discovery."
if is_configured()
else "Room discovery is not configured in this build.",
true,
)
return false
if _discoverable == enabled:
return true
_discoverable = enabled
host_settings_changed.emit(_room_name, _discoverable)
if enabled:
_heartbeat.start()
_set_host_status("Publishing room…", false)
_synchronize_host_lease()
else:
_heartbeat.stop()
_remove_host_lease()
_set_host_status("Room is open but unlisted.", false)
return true
func request_rooms() -> bool:
if not is_configured():
rooms_updated.emit([])
browse_status_changed.emit(
"Public room discovery is not configured in this build.", true
)
return false
if _browse_request_in_flight:
return true
var game_version: String = str(
ProjectSettings.get_setting("application/config/version", "unknown")
)
var url: String = "%s/v1/rooms?game_version=%s&protocol_version=%d" % [
_base_url,
game_version.uri_encode(),
NetworkProtocol.PROTOCOL_VERSION,
]
var error: Error = _browse_request.request(url)
if error != OK:
rooms_updated.emit([])
browse_status_changed.emit("Could not request public rooms.", true)
return false
_browse_request_in_flight = true
browse_status_changed.emit("Looking for public rooms…", false)
return true
func room_endpoint(room: Dictionary) -> String:
var address: String = str(room.get("address", "")).strip_edges()
var port: int = int(room.get("port", 0))
if address.is_empty() or port < 1 or port > 65535:
return ""
if ":" in address and not address.begins_with("["):
address = "[%s]" % address
return "%s:%d" % [address, port]
func _configured_base_url() -> String:
var value: String = OS.get_environment(BASE_URL_ENVIRONMENT).strip_edges()
if value.is_empty():
value = str(ProjectSettings.get_setting(BASE_URL_SETTING, "")).strip_edges()
while value.ends_with("/"):
value = value.left(value.length() - 1)
if not value.is_empty() and not (
value.begins_with("https://") or value.begins_with("http://")
):
push_warning("Ignored invalid NETfishing discovery URL.")
return ""
return value
func _synchronize_host_lease() -> void:
if not _should_advertise():
if not _lease_room_id.is_empty():
_remove_host_lease()
return
if _host_request_in_flight:
_host_sync_queued = true
return
var method: HTTPClient.Method = HTTPClient.METHOD_POST
var url: String = "%s/v1/rooms" % _base_url
var headers := PackedStringArray(["Content-Type: application/json"])
_host_request_kind = HostRequestKind.CREATE
if not _lease_room_id.is_empty():
method = HTTPClient.METHOD_PUT
url = "%s/v1/rooms/%s" % [_base_url, _lease_room_id.uri_encode()]
headers.append("Authorization: Bearer %s" % _lease_token)
_host_request_kind = HostRequestKind.UPDATE
_start_host_request(url, headers, method, JSON.stringify(_host_payload()))
func _remove_host_lease() -> void:
if _lease_room_id.is_empty():
return
if _host_request_in_flight:
_host_sync_queued = true
return
var url: String = "%s/v1/rooms/%s" % [
_base_url, _lease_room_id.uri_encode()
]
var headers := PackedStringArray([
"Authorization: Bearer %s" % _lease_token,
])
_host_request_kind = HostRequestKind.DELETE
_start_host_request(url, headers, HTTPClient.METHOD_DELETE, "")
func _start_host_request(
url: String,
headers: PackedStringArray,
method: HTTPClient.Method,
body: String,
) -> void:
var error: Error = _host_request.request(url, headers, method, body)
if error != OK:
_host_request_kind = HostRequestKind.NONE
_set_host_status("Could not contact room discovery.", true)
return
_host_request_in_flight = true
func _host_payload() -> Dictionary:
return {
"room_name": _room_name,
"port": _session.get_host_port(),
"current_players": _session.get_player_count(),
"max_players": _session.get_session_max_players(),
"game_version": str(
ProjectSettings.get_setting("application/config/version", "unknown")
),
"protocol_version": NetworkProtocol.PROTOCOL_VERSION,
}
func _should_advertise() -> bool:
return (
_discoverable
and is_configured()
and _session != null
and _session.is_open_host()
)
func _on_host_request_completed(
result: int,
response_code: int,
_headers: PackedStringArray,
body: PackedByteArray,
) -> void:
var completed_kind: HostRequestKind = _host_request_kind
_host_request_kind = HostRequestKind.NONE
_host_request_in_flight = false
var response: Dictionary = _parse_response_dictionary(body)
var transport_ok: bool = result == HTTPRequest.RESULT_SUCCESS
match completed_kind:
HostRequestKind.CREATE:
if transport_ok and response_code == HTTPClient.RESPONSE_CREATED:
var room: Dictionary = response.get("room", {})
_lease_room_id = str(room.get("room_id", ""))
_lease_token = str(response.get("lease_token", ""))
if _lease_room_id.is_empty() or _lease_token.is_empty():
_clear_lease()
_set_host_status("Discovery returned an invalid lease.", true)
else:
_set_host_status("Room is listed publicly.", false)
else:
_set_host_status(_request_failure(response), true)
HostRequestKind.UPDATE:
if transport_ok and response_code == HTTPClient.RESPONSE_OK:
_set_host_status("Room is listed publicly.", false)
elif response_code in [
HTTPClient.RESPONSE_UNAUTHORIZED,
HTTPClient.RESPONSE_NOT_FOUND,
]:
_clear_lease()
_host_sync_queued = true
else:
_set_host_status(_request_failure(response), true)
HostRequestKind.DELETE:
_clear_lease()
if response_code not in [
HTTPClient.RESPONSE_NO_CONTENT,
HTTPClient.RESPONSE_NOT_FOUND,
] and transport_ok:
_set_host_status(_request_failure(response), true)
_:
pass
if not _should_advertise() and not _lease_room_id.is_empty():
_host_sync_queued = true
if _host_sync_queued:
_host_sync_queued = false
call_deferred("_synchronize_host_lease")
func _on_browse_request_completed(
result: int,
response_code: int,
_headers: PackedStringArray,
body: PackedByteArray,
) -> void:
_browse_request_in_flight = false
var response: Dictionary = _parse_response_dictionary(body)
if result != HTTPRequest.RESULT_SUCCESS or response_code != HTTPClient.RESPONSE_OK:
rooms_updated.emit([])
browse_status_changed.emit(_request_failure(response), true)
return
var raw_rooms: Variant = response.get("rooms", [])
if typeof(raw_rooms) != TYPE_ARRAY:
rooms_updated.emit([])
browse_status_changed.emit("Discovery returned an invalid room list.", true)
return
var rooms: Array[Dictionary] = []
for value: Variant in raw_rooms:
if typeof(value) != TYPE_DICTIONARY:
continue
var room: Dictionary = value
if _valid_public_room(room):
rooms.append(room.duplicate(true))
rooms_updated.emit(rooms)
browse_status_changed.emit(
"No public rooms are available."
if rooms.is_empty()
else "%d public room%s found." % [rooms.size(), "" if rooms.size() == 1 else "s"],
false,
)
func _valid_public_room(room: Dictionary) -> bool:
var expected_version: String = str(
ProjectSettings.get_setting("application/config/version", "unknown")
)
return (
typeof(room.get("room_id")) == TYPE_STRING
and typeof(room.get("room_name")) == TYPE_STRING
and typeof(room.get("address")) == TYPE_STRING
and typeof(room.get("game_version")) == TYPE_STRING
and str(room.get("game_version")) == expected_version
and _valid_json_integer(room.get("port"), 1, 65535)
and _valid_json_integer(room.get("current_players"), 1, 128)
and _valid_json_integer(room.get("max_players"), 1, 128)
and int(room["current_players"]) <= int(room["max_players"])
and _valid_json_integer(
room.get("protocol_version"),
NetworkProtocol.PROTOCOL_VERSION,
NetworkProtocol.PROTOCOL_VERSION,
)
)
func _valid_json_integer(value: Variant, minimum: int, maximum: int) -> bool:
if typeof(value) == TYPE_INT:
return int(value) >= minimum and int(value) <= maximum
if typeof(value) != TYPE_FLOAT:
return false
var number: float = float(value)
return (
is_finite(number)
and number == floor(number)
and number >= float(minimum)
and number <= float(maximum)
)
func _parse_response_dictionary(body: PackedByteArray) -> Dictionary:
if body.is_empty():
return {}
var parsed: Variant = JSON.parse_string(body.get_string_from_utf8())
return parsed if typeof(parsed) == TYPE_DICTIONARY else {}
func _request_failure(response: Dictionary) -> String:
var error: Variant = response.get("error", {})
if typeof(error) == TYPE_DICTIONARY:
var message: String = str((error as Dictionary).get("message", "")).strip_edges()
if not message.is_empty():
return message
return "Room discovery is temporarily unavailable."
func _on_session_state_changed(state: NetworkSession.State) -> void:
if state == NetworkSession.State.OPEN_HOST:
if _discoverable:
_heartbeat.start()
_synchronize_host_lease()
else:
_set_host_status("Room is open but unlisted.", false)
return
if state in [
NetworkSession.State.PRIVATE_HOST,
NetworkSession.State.INACTIVE,
NetworkSession.State.DISCONNECTING,
NetworkSession.State.CONNECTION_FAILED,
NetworkSession.State.SERVER_LOST,
]:
if _discoverable:
_discoverable = false
host_settings_changed.emit(_room_name, false)
_heartbeat.stop()
_remove_host_lease()
if state == NetworkSession.State.PRIVATE_HOST:
_set_host_status("Open the game before listing it publicly.", false)
func _on_host_openness_changed(is_open: bool) -> void:
if not is_open and _discoverable:
set_discoverable(false)
func _on_peer_count_changed(_player_count: int, _max_players: int) -> void:
if _discoverable:
_synchronize_host_lease()
func _clear_lease() -> void:
_lease_room_id = ""
_lease_token = ""
func _set_host_status(message: String, is_error: bool) -> void:
_host_status_message = message
_host_status_is_error = is_error
host_status_changed.emit(message, is_error)
func _sanitize_room_name(value: String) -> String:
var cleaned: String = value.strip_edges().replace("\n", " ").replace("\r", " ")
cleaned = cleaned.replace("\t", " ")
while " " in cleaned:
cleaned = cleaned.replace(" ", " ")
return cleaned.left(MAX_ROOM_NAME_LENGTH)
func _load_settings() -> void:
var config := ConfigFile.new()
if config.load(SETTINGS_PATH) == OK:
var saved_name: String = _sanitize_room_name(
str(config.get_value("host", "room_name", DEFAULT_ROOM_NAME))
)
if not saved_name.is_empty():
_room_name = saved_name
func _save_settings() -> void:
var config := ConfigFile.new()
config.set_value("host", "room_name", _room_name)
var error: Error = config.save(SETTINGS_PATH)
if error != OK:
push_warning("Could not save the local NETfishing room name.")

View file

@ -0,0 +1 @@
uid://d0jv8n2nfqia0

View file

@ -89,6 +89,10 @@ func is_local_host() -> bool:
return _session.is_host() return _session.is_host()
func is_open_host() -> bool:
return _session.is_open_host()
func get_relationships() -> Array[Dictionary]: func get_relationships() -> Array[Dictionary]:
return _relationships.get_records() return _relationships.get_records()

View file

@ -198,6 +198,7 @@ static func make_server_hello(
assigned_peer_id: int, assigned_peer_id: int,
player_count: int, player_count: int,
max_players: int, max_players: int,
server_display_name: String = "NETfishing",
) -> Dictionary: ) -> Dictionary:
return { return {
"accepted": accepted, "accepted": accepted,
@ -205,7 +206,7 @@ static func make_server_hello(
"protocol_version": PROTOCOL_VERSION, "protocol_version": PROTOCOL_VERSION,
"session_id": session_id, "session_id": session_id,
"assigned_peer_id": assigned_peer_id, "assigned_peer_id": assigned_peer_id,
"server_display_name": "NETfishing", "server_display_name": server_display_name,
"player_count": player_count, "player_count": player_count,
"max_players": max_players, "max_players": max_players,
"capability_flags": PackedStringArray([ "capability_flags": PackedStringArray([

View file

@ -16,6 +16,7 @@ signal connection_error(message: String)
signal peer_authenticated(peer_id: int, display_name: String) signal peer_authenticated(peer_id: int, display_name: String)
signal peer_removed(peer_id: int) signal peer_removed(peer_id: int)
signal host_openness_changed(is_open: bool) signal host_openness_changed(is_open: bool)
signal session_display_name_changed(display_name: String)
signal peer_count_changed(player_count: int, max_players: int) signal peer_count_changed(player_count: int, max_players: int)
signal peer_display_name_changed(peer_id: int, display_name: String) signal peer_display_name_changed(peer_id: int, display_name: String)
signal peer_profile_changed( signal peer_profile_changed(
@ -99,6 +100,7 @@ var _local_appearance_snapshot: Dictionary = (
) )
var _moderation_disconnect_message := "" var _moderation_disconnect_message := ""
var _host_port: int = 0 var _host_port: int = 0
var _session_display_name: String = "NETfishing Room"
func _ready() -> void: func _ready() -> void:
@ -228,6 +230,20 @@ func get_host_port() -> int:
return _host_port if is_host() else 0 return _host_port if is_host() else 0
func set_session_display_name(value: String) -> void:
var cleaned: String = value.strip_edges().left(48)
if cleaned.is_empty():
cleaned = "NETfishing Room"
if cleaned == _session_display_name:
return
_session_display_name = cleaned
session_display_name_changed.emit(_session_display_name)
func get_session_display_name() -> String:
return _session_display_name
static func _can_bind_udp_port(port: int) -> bool: static func _can_bind_udp_port(port: int) -> bool:
var probe := PacketPeerUDP.new() var probe := PacketPeerUDP.new()
var error: Error = probe.bind(port) var error: Error = probe.bind(port)
@ -1038,7 +1054,8 @@ func submit_client_hello(data: Dictionary) -> void:
_session_id, _session_id,
sender_id, sender_id,
_registry.size(), _registry.size(),
session_max_players session_max_players,
_session_display_name,
) )
) )
receive_spawn_list.rpc_id(sender_id, _build_spawn_list()) receive_spawn_list.rpc_id(sender_id, _build_spawn_list())

View file

@ -241,6 +241,10 @@ alternate_fishing={
] ]
} }
[network]
discovery/base_url="https://discovery.netfishing.org"
[physics] [physics]
3d/physics_engine="Jolt Physics" 3d/physics_engine="Jolt Physics"

View file

@ -277,6 +277,7 @@ func setup(
reservations: PlayerAssetReservationService, reservations: PlayerAssetReservationService,
network_profile_service: NetworkProfileService, network_profile_service: NetworkProfileService,
network_player_list: NetworkPlayerListService, network_player_list: NetworkPlayerListService,
discovery: DiscoveryClient,
settings_manager: PlayerSettingsManagerType, settings_manager: PlayerSettingsManagerType,
surface_drawing: NetworkSurfaceDrawingService, surface_drawing: NetworkSurfaceDrawingService,
art_unlocks: PlayerArtUnlocks, art_unlocks: PlayerArtUnlocks,
@ -372,6 +373,7 @@ func setup(
reservations, reservations,
network_profile_service, network_profile_service,
network_player_list, network_player_list,
discovery,
player_jobs, player_jobs,
world_time, world_time,
world_environment, world_environment,

View file

@ -5,11 +5,14 @@ signal join_requested(endpoint: String)
signal back_requested signal back_requested
enum Mode { enum Mode {
DISCOVER,
DIRECT, DIRECT,
SAVED, SAVED,
RECENT, RECENT,
} }
const DISCOVERY_REFRESH_SECONDS: float = 8.0
const ADDRESS_FORMAT_HELP: String = ( const ADDRESS_FORMAT_HELP: String = (
"Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; " "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted; "
+ "include the port shown by a host when it differs." + "include the port shown by a host when it differs."
@ -27,10 +30,12 @@ const DIRECT_WORKFLOW_HELP: String = (
@onready var _name_helper: Label = %NameHelper @onready var _name_helper: Label = %NameHelper
@onready var _server_list: ItemList = %ServerList @onready var _server_list: ItemList = %ServerList
@onready var _details: Label = %Details @onready var _details: Label = %Details
@onready var _discover_button: Button = %DiscoverButton
@onready var _direct_button: Button = %DirectButton @onready var _direct_button: Button = %DirectButton
@onready var _saved_button: Button = %SavedButton @onready var _saved_button: Button = %SavedButton
@onready var _recent_button: Button = %RecentButton @onready var _recent_button: Button = %RecentButton
@onready var _join_button: Button = %JoinButton @onready var _join_button: Button = %JoinButton
@onready var _refresh_button: Button = %RefreshButton
@onready var _save_button: Button = %SaveButton @onready var _save_button: Button = %SaveButton
@onready var _edit_button: Button = %EditButton @onready var _edit_button: Button = %EditButton
@onready var _favorite_button: Button = %FavoriteButton @onready var _favorite_button: Button = %FavoriteButton
@ -47,10 +52,14 @@ const DIRECT_WORKFLOW_HELP: String = (
var _network_session: NetworkSession var _network_session: NetworkSession
var _saved_servers: SavedServerStore var _saved_servers: SavedServerStore
var _server_trust: ServerTrustStore var _server_trust: ServerTrustStore
var _discovery: DiscoveryClient
var _gameplay_context: bool = false var _gameplay_context: bool = false
var _mode: Mode = Mode.DIRECT var _mode: Mode = Mode.DISCOVER
var _visible_entries: Array[SavedServerEntry] = [] var _visible_entries: Array[SavedServerEntry] = []
var _selected_entry: SavedServerEntry var _selected_entry: SavedServerEntry
var _discovery_rooms: Array[Dictionary] = []
var _selected_discovery_index: int = -1
var _discovery_refresh_timer: Timer
var _editing_entry_id: String = "" var _editing_entry_id: String = ""
var _name_entry_active: bool = false var _name_entry_active: bool = false
var _delete_armed: bool = false var _delete_armed: bool = false
@ -63,16 +72,19 @@ func _ready() -> void:
"panel", UtilityPageStyle.panel_style() "panel", UtilityPageStyle.panel_style()
) )
for button: BaseButton in [ for button: BaseButton in [
_direct_button, _saved_button, _recent_button, _join_button, _discover_button, _direct_button, _saved_button, _recent_button,
_refresh_button, _join_button,
_save_button, _edit_button, _favorite_button, _delete_button, _save_button, _edit_button, _favorite_button, _delete_button,
_cancel_button, _back_button, _open_close_button, _cancel_button, _back_button, _open_close_button,
]: ]:
UtilityPageStyle.apply_ocean_button(button) UtilityPageStyle.apply_ocean_button(button)
UtilityPageStyle.apply_ocean_line_edit(_address) UtilityPageStyle.apply_ocean_line_edit(_address)
UtilityPageStyle.apply_ocean_line_edit(_name_edit) UtilityPageStyle.apply_ocean_line_edit(_name_edit)
_discover_button.pressed.connect(_set_mode.bind(Mode.DISCOVER))
_direct_button.pressed.connect(_set_mode.bind(Mode.DIRECT)) _direct_button.pressed.connect(_set_mode.bind(Mode.DIRECT))
_saved_button.pressed.connect(_set_mode.bind(Mode.SAVED)) _saved_button.pressed.connect(_set_mode.bind(Mode.SAVED))
_recent_button.pressed.connect(_set_mode.bind(Mode.RECENT)) _recent_button.pressed.connect(_set_mode.bind(Mode.RECENT))
_refresh_button.pressed.connect(_request_discovery_refresh)
_join_button.pressed.connect(_request_join) _join_button.pressed.connect(_request_join)
_save_button.pressed.connect(_on_save_pressed) _save_button.pressed.connect(_on_save_pressed)
_edit_button.pressed.connect(_on_edit_pressed) _edit_button.pressed.connect(_on_edit_pressed)
@ -97,6 +109,11 @@ func _ready() -> void:
) )
_delete_confirmation.confirmed.connect(_confirm_delete) _delete_confirmation.confirmed.connect(_confirm_delete)
_delete_confirmation.cancelled.connect(_cancel_delete) _delete_confirmation.cancelled.connect(_cancel_delete)
_discovery_refresh_timer = Timer.new()
_discovery_refresh_timer.wait_time = DISCOVERY_REFRESH_SECONDS
_discovery_refresh_timer.one_shot = false
_discovery_refresh_timer.timeout.connect(_request_discovery_refresh)
add_child(_discovery_refresh_timer)
hide() hide()
@ -105,11 +122,13 @@ func setup(
saved_servers: SavedServerStore, saved_servers: SavedServerStore,
gameplay_context: bool, gameplay_context: bool,
server_trust: ServerTrustStore = null, server_trust: ServerTrustStore = null,
discovery: DiscoveryClient = null,
) -> void: ) -> void:
_network_session = network_session _network_session = network_session
_saved_servers = saved_servers _saved_servers = saved_servers
_gameplay_context = gameplay_context _gameplay_context = gameplay_context
_server_trust = server_trust _server_trust = server_trust
_discovery = discovery
if not _network_session.state_changed.is_connected(_on_state_changed): if not _network_session.state_changed.is_connected(_on_state_changed):
_network_session.state_changed.connect(_on_state_changed) _network_session.state_changed.connect(_on_state_changed)
if not _network_session.status_message_changed.is_connected( if not _network_session.status_message_changed.is_connected(
@ -131,6 +150,17 @@ func setup(
and not _saved_servers.data_changed.is_connected(_on_store_changed) and not _saved_servers.data_changed.is_connected(_on_store_changed)
): ):
_saved_servers.data_changed.connect(_on_store_changed) _saved_servers.data_changed.connect(_on_store_changed)
if _discovery != null:
if not _discovery.rooms_updated.is_connected(
_on_discovery_rooms_updated
):
_discovery.rooms_updated.connect(_on_discovery_rooms_updated)
if not _discovery.browse_status_changed.is_connected(
_on_discovery_status_changed
):
_discovery.browse_status_changed.connect(
_on_discovery_status_changed
)
_refresh() _refresh()
@ -149,6 +179,7 @@ func open_page(preserved_endpoint: String = "") -> void:
func close_page() -> void: func close_page() -> void:
_clear_edit_state() _clear_edit_state()
_discovery_refresh_timer.stop()
hide() hide()
var current_viewport: Viewport = get_viewport() var current_viewport: Viewport = get_viewport()
if current_viewport != null: if current_viewport != null:
@ -166,9 +197,15 @@ func set_status(message: String) -> void:
func _set_mode(mode: Mode) -> void: func _set_mode(mode: Mode) -> void:
_mode = mode _mode = mode
_selected_entry = null _selected_entry = null
_selected_discovery_index = -1
_clear_edit_state() _clear_edit_state()
_refresh_entries() _refresh_entries()
_refresh() _refresh()
if mode == Mode.DISCOVER:
_discovery_refresh_timer.start()
_request_discovery_refresh()
else:
_discovery_refresh_timer.stop()
if not is_visible_in_tree(): if not is_visible_in_tree():
return return
if mode == Mode.DIRECT: if mode == Mode.DIRECT:
@ -183,11 +220,15 @@ func _request_join() -> void:
NetworkSession.State.SERVER_LOST, NetworkSession.State.SERVER_LOST,
]: ]:
_network_session.reset_failure() _network_session.reset_failure()
var endpoint_text: String = ( var endpoint_text: String = _address.text
_selected_entry.normalized_endpoint if _mode == Mode.DISCOVER:
if _mode != Mode.DIRECT and _selected_entry != null var room: Dictionary = _selected_discovery_room()
else _address.text if _discovery_room_is_full(room):
) _set_status("That room is full.", true)
return
endpoint_text = _discovery.room_endpoint(room) if _discovery != null else ""
elif _mode != Mode.DIRECT and _selected_entry != null:
endpoint_text = _selected_entry.normalized_endpoint
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text) var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
if not endpoint.is_valid(): if not endpoint.is_valid():
_set_status(endpoint.error_message, true) _set_status(endpoint.error_message, true)
@ -339,6 +380,13 @@ func _cancel_delete() -> void:
func _on_list_item_selected(index: int) -> void: func _on_list_item_selected(index: int) -> void:
if _mode == Mode.DISCOVER:
if index < 0 or index >= _discovery_rooms.size():
return
_selected_discovery_index = index
_selected_entry = null
_refresh()
return
if index < 0 or index >= _visible_entries.size(): if index < 0 or index >= _visible_entries.size():
return return
_selected_entry = _visible_entries[index] _selected_entry = _visible_entries[index]
@ -357,6 +405,19 @@ func _select_entry_id(entry_id: String) -> void:
func _refresh_entries() -> void: func _refresh_entries() -> void:
_visible_entries.clear() _visible_entries.clear()
_server_list.clear() _server_list.clear()
if _mode == Mode.DISCOVER:
for room: Dictionary in _discovery_rooms:
var player_count: int = int(room.get("current_players", 0))
var maximum: int = int(room.get("max_players", 0))
_server_list.add_item(
"%s%d / %d players%s" % [
str(room.get("room_name", "Public room")),
player_count,
maximum,
" — full" if player_count >= maximum else "",
]
)
return
if _saved_servers == null or _mode == Mode.DIRECT: if _saved_servers == null or _mode == Mode.DIRECT:
return return
_visible_entries = ( _visible_entries = (
@ -394,7 +455,16 @@ func _refresh() -> void:
NetworkSession.State.AUTHENTICATING, NetworkSession.State.AUTHENTICATING,
] ]
var direct: bool = _mode == Mode.DIRECT var direct: bool = _mode == Mode.DIRECT
var selected: bool = _selected_entry != null var discovery_mode: bool = _mode == Mode.DISCOVER
var selected: bool = (
_selected_discovery_index >= 0
if discovery_mode
else _selected_entry != null
)
_discover_button.button_pressed = discovery_mode
_direct_button.button_pressed = direct
_saved_button.button_pressed = _mode == Mode.SAVED
_recent_button.button_pressed = _mode == Mode.RECENT
_address.visible = direct or _name_entry_active _address.visible = direct or _name_entry_active
_address_label.visible = _address.visible _address_label.visible = _address.visible
_address_helper.visible = _address.visible _address_helper.visible = _address.visible
@ -407,9 +477,20 @@ func _refresh() -> void:
_name_helper.visible = _name_entry_active _name_helper.visible = _name_entry_active
_server_list.visible = not direct and not _name_entry_active _server_list.visible = not direct and not _name_entry_active
_details.visible = not direct and not _name_entry_active _details.visible = not direct and not _name_entry_active
_join_button.disabled = connecting or (not direct and not selected) _join_button.disabled = (
connecting
or (not direct and not selected)
or (
discovery_mode
and selected
and _discovery_room_is_full(_selected_discovery_room())
)
)
_join_button.text = "join\nnow" if direct else "join" _join_button.text = "join\nnow" if direct else "join"
_save_button.visible = direct or _mode == Mode.RECENT or _name_entry_active _refresh_button.visible = discovery_mode and not _name_entry_active
_save_button.visible = (
direct or _mode == Mode.RECENT or _name_entry_active
)
_save_button.disabled = connecting or ( _save_button.disabled = connecting or (
_mode == Mode.RECENT and not selected and not _name_entry_active _mode == Mode.RECENT and not selected and not _name_entry_active
) )
@ -418,10 +499,14 @@ func _refresh() -> void:
_favorite_button.visible = _mode == Mode.SAVED and selected _favorite_button.visible = _mode == Mode.SAVED and selected
_favorite_button.text = ( _favorite_button.text = (
"unfavorite" "unfavorite"
if selected and _selected_entry.favorite if _mode == Mode.SAVED
and _selected_entry != null
and _selected_entry.favorite
else "favorite" else "favorite"
) )
_delete_button.visible = not direct and selected _delete_button.visible = (
_mode in [Mode.SAVED, Mode.RECENT] and selected
)
_delete_button.text = "remove" if _mode == Mode.RECENT else "delete" _delete_button.text = "remove" if _mode == Mode.RECENT else "delete"
_cancel_button.visible = connecting _cancel_button.visible = connecting
_open_close_button.visible = ( _open_close_button.visible = (
@ -453,10 +538,20 @@ func _refresh() -> void:
) )
if not direct: if not direct:
if selected: if selected:
_details.text = _format_entry_details(_selected_entry)
elif _visible_entries.is_empty():
_details.text = ( _details.text = (
"No saved servers yet." _format_discovery_details(_selected_discovery_room())
if discovery_mode
else _format_entry_details(_selected_entry)
)
elif (
_discovery_rooms.is_empty()
if discovery_mode
else _visible_entries.is_empty()
):
_details.text = (
"No public rooms are available."
if discovery_mode
else "No saved servers yet."
if _mode == Mode.SAVED if _mode == Mode.SAVED
else "No recent connections yet." else "No recent connections yet."
) )
@ -464,7 +559,7 @@ func _refresh() -> void:
_details.text = "Select a server." _details.text = "Select a server."
var warning: String = ( var warning: String = (
_saved_servers.get_recovery_warning() _saved_servers.get_recovery_warning()
if _saved_servers != null if _saved_servers != null and not discovery_mode
else "" else ""
) )
if not warning.is_empty() and _status.text.is_empty(): if not warning.is_empty() and _status.text.is_empty():
@ -517,6 +612,69 @@ func _format_entry_details(entry: SavedServerEntry) -> String:
return "\n".join(parts) return "\n".join(parts)
func _format_discovery_details(room: Dictionary) -> String:
if room.is_empty():
return "Select a public room."
return "\n".join([
"Room: %s" % str(room.get("room_name", "Public room")),
"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 ""
),
"Direct UDP connection • ping is measured after joining",
])
func _selected_discovery_room() -> Dictionary:
if (
_selected_discovery_index < 0
or _selected_discovery_index >= _discovery_rooms.size()
):
return {}
return _discovery_rooms[_selected_discovery_index]
func _discovery_room_is_full(room: Dictionary) -> bool:
if room.is_empty():
return false
return int(room.get("current_players", 0)) >= int(room.get("max_players", 0))
func _request_discovery_refresh() -> void:
if (
_discovery == null
or _mode != Mode.DISCOVER
or not is_visible_in_tree()
):
return
_discovery.request_rooms()
func _on_discovery_rooms_updated(rooms: Array[Dictionary]) -> void:
var selected_id: String = str(
_selected_discovery_room().get("room_id", "")
)
_discovery_rooms = rooms.duplicate(true)
_selected_discovery_index = -1
if _mode == Mode.DISCOVER:
_refresh_entries()
if not selected_id.is_empty():
for index: int in _discovery_rooms.size():
if str(_discovery_rooms[index].get("room_id", "")) == selected_id:
_selected_discovery_index = index
_server_list.select(index)
break
_refresh()
func _on_discovery_status_changed(message: String, is_error: bool) -> void:
if _mode == Mode.DISCOVER and is_visible_in_tree():
_set_status(message, is_error)
func _format_result_code(result_code: String) -> String: func _format_result_code(result_code: String) -> String:
match result_code.strip_edges().to_upper(): match result_code.strip_edges().to_upper():
"SUCCESS": "SUCCESS":

View file

@ -114,22 +114,32 @@ layout_mode = 2
theme_override_constants/separation = 14 theme_override_constants/separation = 14
alignment = 1 alignment = 1
[node name="DiscoverButton" type="Button" parent="Paper/Margin/Layout/Modes"]
unique_name_in_owner = true
custom_minimum_size = Vector2(100, 72)
layout_mode = 2
toggle_mode = true
text = "discover"
[node name="DirectButton" type="Button" parent="Paper/Margin/Layout/Modes"] [node name="DirectButton" type="Button" parent="Paper/Margin/Layout/Modes"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(100, 72) custom_minimum_size = Vector2(100, 72)
layout_mode = 2 layout_mode = 2
toggle_mode = true
text = "direct" text = "direct"
[node name="SavedButton" type="Button" parent="Paper/Margin/Layout/Modes"] [node name="SavedButton" type="Button" parent="Paper/Margin/Layout/Modes"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(100, 72) custom_minimum_size = Vector2(100, 72)
layout_mode = 2 layout_mode = 2
toggle_mode = true
text = "saved" text = "saved"
[node name="RecentButton" type="Button" parent="Paper/Margin/Layout/Modes"] [node name="RecentButton" type="Button" parent="Paper/Margin/Layout/Modes"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(100, 72) custom_minimum_size = Vector2(100, 72)
layout_mode = 2 layout_mode = 2
toggle_mode = true
text = "recent" text = "recent"
[node name="AddressLabel" type="Label" parent="Paper/Margin/Layout"] [node name="AddressLabel" type="Label" parent="Paper/Margin/Layout"]
@ -258,6 +268,13 @@ layout_mode = 2
theme_override_constants/separation = 7 theme_override_constants/separation = 7
alignment = 1 alignment = 1
[node name="RefreshButton" type="Button" parent="Paper/Margin/Layout/Actions"]
unique_name_in_owner = true
visible = false
custom_minimum_size = Vector2(82, 72)
layout_mode = 2
text = "refresh"
[node name="JoinButton" type="Button" parent="Paper/Margin/Layout/Actions"] [node name="JoinButton" type="Button" parent="Paper/Margin/Layout/Actions"]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(82, 72) custom_minimum_size = Vector2(82, 72)

View file

@ -107,6 +107,7 @@ func setup(
network_session: NetworkSessionType, network_session: NetworkSessionType,
saved_servers: SavedServerStoreType, saved_servers: SavedServerStoreType,
server_trust: ServerTrustStore, server_trust: ServerTrustStore,
discovery: DiscoveryClient,
) -> void: ) -> void:
_player = player _player = player
_save_manager = save_manager _save_manager = save_manager
@ -114,7 +115,9 @@ func setup(
_fishing_spot = fishing_spot _fishing_spot = fishing_spot
_network_session = network_session _network_session = network_session
_saved_servers = saved_servers _saved_servers = saved_servers
_join_game_page.setup(network_session, saved_servers, true, server_trust) _join_game_page.setup(
network_session, saved_servers, true, server_trust, discovery
)
if not _fishing_spot.bite_activated.is_connected(_on_bite_activated): if not _fishing_spot.bite_activated.is_connected(_on_bite_activated):
_fishing_spot.bite_activated.connect(_on_bite_activated) _fishing_spot.bite_activated.connect(_on_bite_activated)

View file

@ -526,6 +526,7 @@ func setup(
reservations: PlayerAssetReservationService, reservations: PlayerAssetReservationService,
network_profile_service: NetworkProfileService, network_profile_service: NetworkProfileService,
network_player_list: NetworkPlayerListService, network_player_list: NetworkPlayerListService,
discovery: DiscoveryClient,
player_jobs: PlayerJobService, player_jobs: PlayerJobService,
world_time: WorldTimeService, world_time: WorldTimeService,
world_environment: WorldEnvironment, world_environment: WorldEnvironment,
@ -558,7 +559,7 @@ func setup(
world_environment, world_environment,
world_sun, world_sun,
) )
_players_page.setup(network_player_list) _players_page.setup(network_player_list, discovery)
_catalog_logbook.setup(collection_log, inventory, catalog) _catalog_logbook.setup(collection_log, inventory, catalog)
_the_net_page.setup(player_jobs, world_time) _the_net_page.setup(player_jobs, world_time)
_network_mail_service.unread_count_changed.connect( _network_mail_service.unread_count_changed.connect(

View file

@ -2,10 +2,15 @@ class_name PlayersPage
extends Control extends Control
var _service: NetworkPlayerListService var _service: NetworkPlayerListService
var _discovery: DiscoveryClient
var _count_label: Label var _count_label: Label
var _tabs: HBoxContainer var _tabs: HBoxContainer
var _list: VBoxContainer var _list: VBoxContainer
var _status: Label var _status: Label
var _host_settings_panel: PanelContainer
var _room_name_edit: LineEdit
var _discoverable_toggle: CheckButton
var _host_discovery_status: Label
var _current_tab := 0 var _current_tab := 0
@ -15,13 +20,22 @@ func _ready() -> void:
_build() _build()
func setup(service: NetworkPlayerListService) -> void: func setup(
service: NetworkPlayerListService,
discovery: DiscoveryClient,
) -> void:
_service = service _service = service
_discovery = discovery
_service.entries_changed.connect(_refresh) _service.entries_changed.connect(_refresh)
_service.moderation_finished.connect(func(_ok: bool, message: String) -> void: _service.moderation_finished.connect(func(_ok: bool, message: String) -> void:
_status.text = message _status.text = message
_refresh() _refresh()
) )
if _discovery != null:
_discovery.host_settings_changed.connect(
_on_host_settings_changed
)
_discovery.host_status_changed.connect(_on_host_status_changed)
_refresh() _refresh()
@ -61,6 +75,7 @@ func _build() -> void:
) )
_count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER _count_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
tab_row.add_child(_count_label) tab_row.add_child(_count_label)
_build_host_settings(root)
var scroll := ScrollContainer.new() var scroll := ScrollContainer.new()
scroll.custom_minimum_size = Vector2(0, 310) scroll.custom_minimum_size = Vector2(0, 310)
scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL scroll.size_flags_vertical = Control.SIZE_EXPAND_FILL
@ -77,6 +92,47 @@ func _build() -> void:
root.add_child(_status) root.add_child(_status)
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 = DiscoveryClient.DEFAULT_ROOM_NAME
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)
_discoverable_toggle = CheckButton.new()
_discoverable_toggle.text = "list publicly"
_discoverable_toggle.toggled.connect(_on_discoverable_toggled)
UtilityPageStyle.apply_ocean_button(_discoverable_toggle)
row.add_child(_discoverable_toggle)
_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.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 _select_tab(index: int) -> void: func _select_tab(index: int) -> void:
if index == 2 and (_service == null or not _service.is_local_host()): if index == 2 and (_service == null or not _service.is_local_host()):
return return
@ -95,6 +151,7 @@ func _refresh() -> void:
var button := _tabs.get_child(index) as Button var button := _tabs.get_child(index) as Button
button.button_pressed = index == _current_tab button.button_pressed = index == _current_tab
button.visible = index != 2 or _service.is_local_host() button.visible = index != 2 or _service.is_local_host()
_refresh_host_settings()
for child: Node in _list.get_children(): for child: Node in _list.get_children():
child.queue_free() child.queue_free()
match _current_tab: match _current_tab:
@ -106,6 +163,76 @@ func _refresh() -> void:
_build_ban_rows() _build_ban_rows()
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()
_discoverable_toggle.set_pressed_no_signal(_discovery.is_discoverable())
var can_publish: bool = (
_service.is_local_host()
and _discovery.is_configured()
and _service.is_open_host()
)
_discoverable_toggle.disabled = not can_publish
_discoverable_toggle.tooltip_text = (
"Advertise this open game in the public room browser."
if can_publish
else "Open the game before listing it publicly."
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_discoverable_toggled(enabled: bool) -> void:
if _discovery == null:
return
if not _discovery.set_discoverable(enabled):
_discoverable_toggle.set_pressed_no_signal(
_discovery.is_discoverable()
)
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_toggle != null:
_discoverable_toggle.set_pressed_no_signal(discoverable)
_refresh_host_settings()
func _on_host_status_changed(message: String, is_error: bool) -> void:
if _host_discovery_status == null:
return
_host_discovery_status.text = message
_host_discovery_status.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_DANGER
if is_error
else UtilityPageStyle.OCEAN_TEXT_SECONDARY,
)
func _build_active_rows() -> void: func _build_active_rows() -> void:
if _service.is_local_host(): if _service.is_local_host():
_build_session_artwork_controls() _build_session_artwork_controls()

View file

@ -236,10 +236,13 @@ func setup(
network_session: NetworkSessionType, network_session: NetworkSessionType,
saved_servers: SavedServerStoreType, saved_servers: SavedServerStoreType,
server_trust: ServerTrustStore, server_trust: ServerTrustStore,
discovery: DiscoveryClient,
) -> void: ) -> void:
_save_manager = save_manager _save_manager = save_manager
_settings_manager = settings_manager _settings_manager = settings_manager
_join_game_page.setup(network_session, saved_servers, false, server_trust) _join_game_page.setup(
network_session, saved_servers, false, server_trust, discovery
)
_join_game_page.join_requested.connect(join_game_requested.emit) _join_game_page.join_requested.connect(join_game_requested.emit)
_join_game_page.back_requested.connect(_close_join_game) _join_game_page.back_requested.connect(_close_join_game)
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE Input.mouse_mode = Input.MOUSE_MODE_VISIBLE