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
|
|
@ -38,11 +38,15 @@ node names, connect persistent and networked state to that data.
|
|||
network services validate and replicate bounded domains such as fishing,
|
||||
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
|
||||
`DiscoveryClient` is an optional directory layer beside `NetworkSession`. A
|
||||
player host cycles its discovery control through `private`, `direct`, and
|
||||
`relay`. Private leaves the room unlisted, direct discovery discloses the host
|
||||
route only after an explicit Join, and relay discovery allocates a UDP privacy
|
||||
route without making discovery a gameplay authority. Relay joins fail closed
|
||||
and never silently downgrade to direct. Dedicated servers are always direct:
|
||||
the game forces direct advertisements and discovery independently rejects a
|
||||
dedicated relay request. Public room browsing never receives a host address.
|
||||
The base URL comes from
|
||||
`network/discovery/base_url`, with `straywild_DISCOVERY_URL` available as a
|
||||
development/deployment override.
|
||||
|
||||
|
|
@ -53,8 +57,9 @@ friendship; unblocking does not recreate it. Discovery may publish opt-in,
|
|||
short-lived friend presence and deliver invitations only while both games are
|
||||
online. It stores hashed capability identifiers rather than identity
|
||||
fingerprints or a complete friend graph, keeps no durable social records, and
|
||||
provides no offline delivery. Direct joins still use the existing verified
|
||||
public-room and ENet connection path.
|
||||
provides no offline delivery. Public joins still use the existing verified
|
||||
server-identity and ENet connection path. Discovery endpoints remain transient:
|
||||
they are not shown, added to recent servers, or persisted in the trust store.
|
||||
|
||||
The host is authoritative. Clients submit requests or evidence; the host
|
||||
derives trusted context from registered peers, authoritative regions, and
|
||||
|
|
|
|||
|
|
@ -1,13 +1,18 @@
|
|||
straywild
|
||||
v0.20.2-alpha
|
||||
Alpha 0.20.2
|
||||
v0.20.3-alpha
|
||||
Alpha 0.20.3
|
||||
|
||||
Thank you for supporting this private testing release.
|
||||
|
||||
straywild is an early multiplayer fishing game. This alpha includes local
|
||||
progression, player-hosted and dedicated multiplayer sessions, public room
|
||||
discovery, and direct UDP connections. Expect rough edges and please keep a
|
||||
backup of any save or identity data you care about.
|
||||
discovery, player-selected direct or privacy-relayed hosting, and optional
|
||||
direct UDP connections. Player hosts can cycle discovery through private,
|
||||
direct, and relay. Dedicated servers are always direct and cannot use the
|
||||
shared relay. The relay hides peers' addresses from one another but not from
|
||||
the relay operator. Direct connections expose public IP addresses to the
|
||||
remote peer and are clearly labeled in game. Expect rough edges and please
|
||||
keep a backup of any save or identity data you care about.
|
||||
|
||||
INSTALLATION
|
||||
|
||||
|
|
@ -115,8 +120,9 @@ Please include:
|
|||
|
||||
KNOWN ALPHA LIMITATIONS
|
||||
|
||||
- Public sessions remain direct connections; restrictive NAT or carrier-grade
|
||||
networks may prevent some players from joining.
|
||||
- Direct public sessions may fail behind restrictive NAT or carrier-grade
|
||||
networks. Player-hosted relay sessions add an extra network hop and may have
|
||||
more latency. Dedicated servers are direct-only.
|
||||
- Multiplayer, dedicated-server deployment, UI, and balance are still alpha.
|
||||
- Some fish and character artwork remains temporary.
|
||||
- RV exteriors and decor use temporary prototype geometry and presentation.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ custom_features=""
|
|||
export_filter="all_resources"
|
||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||
exclude_filter="builds/*,scripts/*"
|
||||
export_path="builds/v0.20.2-alpha/windows-x86_64/straywild.exe"
|
||||
export_path="builds/v0.20.3-alpha/windows-x86_64/straywild.exe"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
|
|
@ -35,11 +35,11 @@ application/modify_resources=true
|
|||
application/icon="res://art/exported/system_icons/straywild.ico"
|
||||
application/console_wrapper_icon=""
|
||||
application/icon_interpolation=0
|
||||
application/file_version="0.20.2.0"
|
||||
application/product_version="0.20.2.0"
|
||||
application/file_version="0.20.3.0"
|
||||
application/product_version="0.20.3.0"
|
||||
application/company_name="Woofmeow"
|
||||
application/product_name="straywild"
|
||||
application/file_description="straywild v0.20.2-alpha"
|
||||
application/file_description="straywild v0.20.3-alpha"
|
||||
application/copyright="Copyright © 2026 Woofmeow"
|
||||
application/trademarks="straywild and Woofmeow branding is reserved; see TRADEMARKS.md"
|
||||
application/export_angle=0
|
||||
|
|
@ -62,7 +62,7 @@ custom_features=""
|
|||
export_filter="all_resources"
|
||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||
exclude_filter="builds/*,scripts/*"
|
||||
export_path="builds/v0.20.2-alpha/linux-arm64/straywild.arm64"
|
||||
export_path="builds/v0.20.3-alpha/linux-arm64/straywild.arm64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
|
|
@ -92,7 +92,7 @@ custom_features=""
|
|||
export_filter="all_resources"
|
||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||
exclude_filter="builds/*,scripts/*"
|
||||
export_path="builds/v0.20.2-alpha/macos/straywild.zip"
|
||||
export_path="builds/v0.20.3-alpha/macos/straywild.zip"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
|
|
@ -108,8 +108,8 @@ custom_template/release=""
|
|||
application/bundle_identifier="io.woofmeow.netfishing"
|
||||
application/icon="res://art/exported/system_icons/straywild_1024.png"
|
||||
application/icon_interpolation=0
|
||||
application/short_version="0.20.2"
|
||||
application/version="0.20.2"
|
||||
application/short_version="0.20.3"
|
||||
application/version="0.20.3"
|
||||
application/architecture="universal"
|
||||
codesign/enable=false
|
||||
notarization/enable=false
|
||||
|
|
@ -125,7 +125,7 @@ custom_features=""
|
|||
export_filter="all_resources"
|
||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||
exclude_filter="builds/*,scripts/*,tests/*"
|
||||
export_path="builds/v0.20.2-alpha/server-linux-x86_64/straywildServer.x86_64"
|
||||
export_path="builds/v0.20.3-alpha/server-linux-x86_64/straywildServer.x86_64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
|
|
@ -155,7 +155,7 @@ custom_features=""
|
|||
export_filter="all_resources"
|
||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||
exclude_filter="builds/*,scripts/*"
|
||||
export_path="builds/v0.20.2-alpha/android/straywild.apk"
|
||||
export_path="builds/v0.20.3-alpha/android/straywild.apk"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
|
|
@ -177,7 +177,7 @@ architectures/arm64-v8a=true
|
|||
architectures/x86=false
|
||||
architectures/x86_64=false
|
||||
version/code=200200
|
||||
version/name="v0.20.2-alpha"
|
||||
version/name="v0.20.3-alpha"
|
||||
package/unique_name="io.woofmeow.netfishing"
|
||||
package/name="straywild"
|
||||
package/signed=true
|
||||
|
|
@ -214,7 +214,7 @@ custom_features=""
|
|||
export_filter="all_resources"
|
||||
include_filter="LICENSE,ASSET-LICENSE.md,TRADEMARKS.md,docs/ATTRIBUTION.md,ui/fonts/*LICENSE.txt"
|
||||
exclude_filter="builds/*,scripts/*"
|
||||
export_path="builds/v0.20.2-alpha/linux-x86_64/straywild.x86_64"
|
||||
export_path="builds/v0.20.3-alpha/linux-x86_64/straywild.x86_64"
|
||||
patches=PackedStringArray()
|
||||
encryption_include_filters=""
|
||||
encryption_exclude_filters=""
|
||||
|
|
|
|||
69
main/main.gd
69
main/main.gd
|
|
@ -2234,9 +2234,15 @@ func _on_return_to_title_requested() -> void:
|
|||
_show_title_music(true)
|
||||
|
||||
|
||||
func _on_title_join_game_requested(endpoint: String) -> void:
|
||||
func _on_title_join_game_requested(requested_route: Variant) -> void:
|
||||
if _quit_in_progress or _gameplay_started:
|
||||
return
|
||||
var route := _coerce_connection_route(requested_route)
|
||||
if route == null:
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
"That connection address is invalid."
|
||||
)
|
||||
return
|
||||
var active_slot: Dictionary = _save_slots.ensure_active_slot()
|
||||
if not bool(active_slot.get("ok", false)):
|
||||
_game_ui.get_title_screen().report_network_error(
|
||||
|
|
@ -2247,14 +2253,26 @@ func _on_title_join_game_requested(endpoint: String) -> void:
|
|||
_network_session.disconnect_session("Preparing direct connection.")
|
||||
_join_requested_from_title = true
|
||||
_join_requested_from_pause = false
|
||||
_pending_join_endpoint = endpoint
|
||||
if not _network_session.join_direct(endpoint):
|
||||
_pending_join_endpoint = (
|
||||
route.direct_endpoint.normalized_display
|
||||
if route != null
|
||||
and not route.is_discovery_join()
|
||||
and route.direct_endpoint != null
|
||||
else ""
|
||||
)
|
||||
if not _network_session.join_route(route):
|
||||
_join_requested_from_title = false
|
||||
|
||||
|
||||
func _on_pause_join_game_requested(endpoint: String) -> void:
|
||||
func _on_pause_join_game_requested(requested_route: Variant) -> void:
|
||||
if _quit_in_progress or not _gameplay_started:
|
||||
return
|
||||
var route := _coerce_connection_route(requested_route)
|
||||
if route == null:
|
||||
_game_ui.get_pause_menu().report_network_error(
|
||||
"That connection address is invalid."
|
||||
)
|
||||
return
|
||||
if not _save_manager.save_world_time_checkpoint():
|
||||
_game_ui.get_pause_menu().report_network_error(
|
||||
"Could not save progression before leaving this session."
|
||||
|
|
@ -2262,20 +2280,36 @@ func _on_pause_join_game_requested(endpoint: String) -> void:
|
|||
return
|
||||
_join_requested_from_pause = true
|
||||
_join_requested_from_title = false
|
||||
_pending_join_endpoint = endpoint
|
||||
_pending_join_endpoint = (
|
||||
route.direct_endpoint.normalized_display
|
||||
if route != null
|
||||
and not route.is_discovery_join()
|
||||
and route.direct_endpoint != null
|
||||
else ""
|
||||
)
|
||||
_game_ui.get_pause_menu().close_for_title_transition()
|
||||
var preserved_public_join: bool = (
|
||||
_discovery.preserve_public_join_for_session_switch()
|
||||
)
|
||||
_network_session.disconnect_session("Connecting to another game.")
|
||||
if not _network_session.join_direct(endpoint):
|
||||
if not _network_session.join_route(route):
|
||||
if preserved_public_join:
|
||||
_discovery.cancel_pending_public_join()
|
||||
_handle_failed_session_switch(
|
||||
"Could not begin the direct connection."
|
||||
"Could not begin the connection."
|
||||
)
|
||||
|
||||
|
||||
func _coerce_connection_route(value: Variant) -> ConnectionRoute:
|
||||
if value is ConnectionRoute:
|
||||
var supplied_route := value as ConnectionRoute
|
||||
return supplied_route if supplied_route.is_valid() else null
|
||||
if typeof(value) != TYPE_STRING:
|
||||
return null
|
||||
var endpoint := EndpointParser.parse(str(value))
|
||||
return ConnectionRoute.direct(endpoint) if endpoint.is_valid() else null
|
||||
|
||||
|
||||
func _on_friend_request_received(
|
||||
request_id: String,
|
||||
fingerprint: String,
|
||||
|
|
@ -2335,8 +2369,17 @@ func _show_next_social_prompt() -> void:
|
|||
elif kind == "friend_invite":
|
||||
var room: Dictionary = _active_social_prompt.get("room", {})
|
||||
var room_name := str(room.get("room_name", "their room"))
|
||||
var relayed := str(room.get("connection_mode", "")) == "relay"
|
||||
var privacy_message := (
|
||||
"This join uses a privacy relay; the relay service still handles "
|
||||
+ "both network addresses."
|
||||
if relayed
|
||||
else "This is a direct connection. Joining shares your public IP "
|
||||
+ "address with the host."
|
||||
)
|
||||
shown = _game_ui.show_social_prompt(
|
||||
"%s invited you to join %s." % [display_name, room_name],
|
||||
"%s invited you to join %s.\n%s"
|
||||
% [display_name, room_name, privacy_message],
|
||||
"join",
|
||||
"not now",
|
||||
)
|
||||
|
|
@ -2375,16 +2418,16 @@ func _on_social_prompt_declined() -> void:
|
|||
call_deferred("_show_next_social_prompt")
|
||||
|
||||
|
||||
func _on_social_public_join_prepared(endpoint: String) -> void:
|
||||
func _on_social_public_join_prepared(route: ConnectionRoute) -> void:
|
||||
if not _pending_social_invite_join:
|
||||
return
|
||||
var from_gameplay := _pending_social_invite_gameplay
|
||||
_pending_social_invite_join = false
|
||||
_pending_social_invite_gameplay = false
|
||||
if from_gameplay:
|
||||
_on_pause_join_game_requested(endpoint)
|
||||
_on_pause_join_game_requested(route)
|
||||
else:
|
||||
_on_title_join_game_requested(endpoint)
|
||||
_on_title_join_game_requested(route)
|
||||
|
||||
|
||||
func _on_social_public_join_status_changed(
|
||||
|
|
@ -2412,7 +2455,7 @@ func _on_network_join_authenticated() -> void:
|
|||
var server_metadata: Dictionary = (
|
||||
_network_session.get_last_server_metadata()
|
||||
)
|
||||
if connected_endpoint != null:
|
||||
if connected_endpoint != null and not _network_session.is_current_route_discovery():
|
||||
_saved_servers.record_successful_connection(
|
||||
connected_endpoint,
|
||||
int(server_metadata.get("max_players", 0)),
|
||||
|
|
@ -2455,7 +2498,7 @@ func _on_network_connection_error(message: String) -> void:
|
|||
var failed_endpoint: ConnectionEndpoint = (
|
||||
_network_session.get_current_endpoint()
|
||||
)
|
||||
if failed_endpoint != null:
|
||||
if failed_endpoint != null and not _network_session.is_current_route_discovery():
|
||||
_saved_servers.record_connection_failure(
|
||||
failed_endpoint,
|
||||
_connection_result_code(message),
|
||||
|
|
|
|||
|
|
@ -3,22 +3,65 @@ extends RefCounted
|
|||
|
||||
enum Kind {
|
||||
DIRECT,
|
||||
DISCOVERY_DIRECT,
|
||||
DISCOVERY_RELAY,
|
||||
}
|
||||
|
||||
var kind: Kind = Kind.DIRECT
|
||||
var direct_endpoint: ConnectionEndpoint
|
||||
var display_description: String = ""
|
||||
var discovery_room_id: String = ""
|
||||
|
||||
|
||||
static func direct(endpoint: ConnectionEndpoint) -> ConnectionRoute:
|
||||
var route := ConnectionRoute.new()
|
||||
route.kind = Kind.DIRECT
|
||||
route.direct_endpoint = endpoint
|
||||
route.display_description = "direct server" if endpoint != null else ""
|
||||
return route
|
||||
|
||||
|
||||
static func discovery_direct(
|
||||
endpoint: ConnectionEndpoint,
|
||||
room_id: String,
|
||||
room_name: String,
|
||||
) -> ConnectionRoute:
|
||||
var route := ConnectionRoute.new()
|
||||
route.kind = Kind.DISCOVERY_DIRECT
|
||||
route.direct_endpoint = endpoint
|
||||
route.discovery_room_id = room_id
|
||||
var cleaned_name := room_name.strip_edges()
|
||||
route.display_description = (
|
||||
endpoint.normalized_display if endpoint != null else ""
|
||||
cleaned_name if not cleaned_name.is_empty() else "public room"
|
||||
)
|
||||
return route
|
||||
|
||||
|
||||
static func discovery_relay(
|
||||
endpoint: ConnectionEndpoint,
|
||||
room_id: String,
|
||||
room_name: String,
|
||||
) -> ConnectionRoute:
|
||||
var route := discovery_direct(endpoint, room_id, room_name)
|
||||
route.kind = Kind.DISCOVERY_RELAY
|
||||
return route
|
||||
|
||||
|
||||
func is_valid() -> bool:
|
||||
return kind == Kind.DIRECT and direct_endpoint != null and direct_endpoint.is_valid()
|
||||
return (
|
||||
kind in [Kind.DIRECT, Kind.DISCOVERY_DIRECT, Kind.DISCOVERY_RELAY]
|
||||
and direct_endpoint != null
|
||||
and direct_endpoint.is_valid()
|
||||
and (
|
||||
kind == Kind.DIRECT
|
||||
or not discovery_room_id.is_empty()
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func is_discovery_join() -> bool:
|
||||
return kind in [Kind.DISCOVERY_DIRECT, Kind.DISCOVERY_RELAY]
|
||||
|
||||
|
||||
func is_relay() -> bool:
|
||||
return kind == Kind.DISCOVERY_RELAY
|
||||
|
|
|
|||
|
|
@ -34,11 +34,12 @@ func connect_to_route(route: ConnectionRoute) -> Error:
|
|||
)
|
||||
if error != OK:
|
||||
transport_error.emit(
|
||||
"Unable to connect to %s." % endpoint.normalized_display
|
||||
"Unable to begin the connection to %s."
|
||||
% route.display_description
|
||||
)
|
||||
return error
|
||||
_peer = enet_peer
|
||||
_route_description = endpoint.normalized_display
|
||||
_route_description = route.display_description
|
||||
return OK
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ 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)
|
||||
signal host_state_changed(state: int)
|
||||
signal public_join_prepared(endpoint: String)
|
||||
signal public_join_prepared(route: ConnectionRoute)
|
||||
signal public_join_status_changed(message: String, is_error: bool)
|
||||
signal public_join_state_changed(state: int)
|
||||
signal friend_presence_updated(friends: Array[Dictionary])
|
||||
|
|
@ -28,6 +28,9 @@ const LEGACY_DEFAULT_ROOM_NAME: String = "straywild Room"
|
|||
const LEGACY_DEDICATED_DEFAULT_ROOM_NAME: String = (
|
||||
"straywild Dedicated Server"
|
||||
)
|
||||
const HOST_CONNECTION_DIRECT: String = "direct"
|
||||
const HOST_CONNECTION_RELAY: String = "relay"
|
||||
const HOST_DISCOVERY_PRIVATE: String = "private"
|
||||
const ROOM_NAME_SUFFIX: String = "'s Server"
|
||||
const MAX_ROOM_NAME_LENGTH: int = 48
|
||||
const HEARTBEAT_INTERVAL_SECONDS: float = 15.0
|
||||
|
|
@ -35,6 +38,7 @@ const REQUEST_TIMEOUT_SECONDS: float = 8.0
|
|||
const TRAVERSAL_POLL_INTERVAL_SECONDS: float = 1.0
|
||||
const JOIN_PROBE_INTERVAL_SECONDS: float = 0.35
|
||||
const TRAVERSAL_PACKET_PREFIX: String = "straywild_TRAVERSAL_V1 "
|
||||
const RELAY_AUTH_PACKET_PREFIX: String = "straywild_RELAY_AUTH_V1 "
|
||||
const UPNP_MAPPING_DURATION_SECONDS: int = 3600
|
||||
const UPNP_RENEW_INTERVAL_SECONDS: float = 2700.0
|
||||
const UPNP_RETRY_INTERVAL_SECONDS: float = 300.0
|
||||
|
|
@ -90,6 +94,7 @@ var _join_request: HTTPRequest
|
|||
var _traversal_timer: Timer
|
||||
var _join_probe_timer: Timer
|
||||
var _host_verified: bool = false
|
||||
var _host_connection_preference: String = HOST_CONNECTION_DIRECT
|
||||
var _traversal_host: String = ""
|
||||
var _traversal_port: int = 0
|
||||
var _host_verification_token: String = ""
|
||||
|
|
@ -97,6 +102,8 @@ var _join_request_in_flight: bool = false
|
|||
var _pending_join_endpoint: String = ""
|
||||
var _pending_join_token: String = ""
|
||||
var _pending_join_room_id: String = ""
|
||||
var _pending_join_room_name: String = ""
|
||||
var _pending_join_transport: String = ""
|
||||
var _preserve_pending_join_on_inactive: bool = false
|
||||
var _upnp_thread: Thread
|
||||
var _upnp_mapping_in_progress: bool = false
|
||||
|
|
@ -381,12 +388,25 @@ func get_host_state() -> HostState:
|
|||
return _host_state
|
||||
|
||||
|
||||
func get_host_connection_mode() -> String:
|
||||
return _host_connection_preference
|
||||
|
||||
|
||||
func get_host_discovery_mode() -> String:
|
||||
return (
|
||||
_host_connection_preference
|
||||
if _discoverable
|
||||
else HOST_DISCOVERY_PRIVATE
|
||||
)
|
||||
|
||||
|
||||
func get_public_join_state() -> PublicJoinState:
|
||||
return _public_join_state
|
||||
|
||||
|
||||
func configure_dedicated_runtime(room_name: String) -> bool:
|
||||
_host_settings_persistence_enabled = false
|
||||
_host_connection_preference = HOST_CONNECTION_DIRECT
|
||||
return set_room_name(room_name)
|
||||
|
||||
|
||||
|
|
@ -453,6 +473,49 @@ func set_discoverable(enabled: bool) -> bool:
|
|||
return true
|
||||
|
||||
|
||||
func set_host_connection_mode(connection_mode: String) -> bool:
|
||||
if connection_mode not in [
|
||||
HOST_CONNECTION_DIRECT,
|
||||
HOST_CONNECTION_RELAY,
|
||||
]:
|
||||
return false
|
||||
if (
|
||||
connection_mode == HOST_CONNECTION_RELAY
|
||||
and _session != null
|
||||
and _session.is_dedicated_host()
|
||||
):
|
||||
_set_host_state(HostState.ERROR)
|
||||
_set_host_status(
|
||||
"Dedicated servers must use direct discovery hosting.", true
|
||||
)
|
||||
return false
|
||||
if _host_connection_preference == connection_mode:
|
||||
return true
|
||||
_host_connection_preference = connection_mode
|
||||
_save_settings()
|
||||
host_settings_changed.emit(_room_name, _discoverable)
|
||||
if _discoverable:
|
||||
_set_host_state(HostState.REGISTERING)
|
||||
_set_host_status("Updating public connection mode…", false)
|
||||
_synchronize_host_lease()
|
||||
return true
|
||||
|
||||
|
||||
func cycle_host_discovery_mode() -> bool:
|
||||
match get_host_discovery_mode():
|
||||
HOST_DISCOVERY_PRIVATE:
|
||||
if not set_host_connection_mode(HOST_CONNECTION_DIRECT):
|
||||
return false
|
||||
return set_discoverable(true)
|
||||
HOST_CONNECTION_DIRECT:
|
||||
if _session != null and _session.is_dedicated_host():
|
||||
return set_discoverable(false)
|
||||
return set_host_connection_mode(HOST_CONNECTION_RELAY)
|
||||
HOST_CONNECTION_RELAY:
|
||||
return set_discoverable(false)
|
||||
return false
|
||||
|
||||
|
||||
func is_public_join_preparing() -> bool:
|
||||
return _join_request_in_flight
|
||||
|
||||
|
|
@ -480,13 +543,16 @@ func prepare_public_join(room: Dictionary) -> bool:
|
|||
"You are already hosting this room.", true
|
||||
)
|
||||
return false
|
||||
var endpoint: String = room_endpoint(room)
|
||||
var room_id: String = str(room.get("room_id", "")).strip_edges()
|
||||
if endpoint.is_empty() or room_id.is_empty():
|
||||
if room_id.is_empty():
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
return false
|
||||
_pending_join_endpoint = endpoint
|
||||
_pending_join_endpoint = ""
|
||||
_pending_join_room_id = room_id
|
||||
_pending_join_room_name = str(
|
||||
room.get("room_name", "public room")
|
||||
).strip_edges()
|
||||
_pending_join_transport = str(room.get("connection_mode", ""))
|
||||
_pending_join_token = ""
|
||||
var url: String = "%s/v1/rooms/%s/join-attempts" % [
|
||||
_base_url,
|
||||
|
|
@ -535,16 +601,6 @@ func request_rooms() -> bool:
|
|||
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 is_own_room(room: Dictionary) -> bool:
|
||||
return (
|
||||
not _lease_room_id.is_empty()
|
||||
|
|
@ -640,6 +696,14 @@ func _host_payload() -> Dictionary:
|
|||
ProjectSettings.get_setting("application/config/version", "unknown")
|
||||
),
|
||||
"protocol_version": NetworkProtocol.PROTOCOL_VERSION,
|
||||
"host_kind": (
|
||||
"dedicated" if _session.is_dedicated_host() else "player"
|
||||
),
|
||||
"connection_mode": (
|
||||
HOST_CONNECTION_DIRECT
|
||||
if _session.is_dedicated_host()
|
||||
else _host_connection_preference
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -688,7 +752,7 @@ func _on_host_request_completed(
|
|||
else HostState.VERIFYING
|
||||
)
|
||||
_set_host_status(
|
||||
"Room is listed publicly."
|
||||
_host_listing_status(room)
|
||||
if _host_verified
|
||||
else "Checking the public route…",
|
||||
false,
|
||||
|
|
@ -706,7 +770,7 @@ func _on_host_request_completed(
|
|||
else HostState.VERIFYING
|
||||
)
|
||||
_set_host_status(
|
||||
"Room is listed publicly."
|
||||
_host_listing_status(room)
|
||||
if _host_verified
|
||||
else "Checking the public route…",
|
||||
false,
|
||||
|
|
@ -755,10 +819,33 @@ func _on_join_request_completed(
|
|||
return
|
||||
_pending_join_token = str(response.get("join_token", ""))
|
||||
_apply_traversal_response(response)
|
||||
var route_transport := ""
|
||||
var route_privacy := ""
|
||||
var route_details: Variant = response.get("route", {})
|
||||
if typeof(route_details) == TYPE_DICTIONARY:
|
||||
var direct_route: Dictionary = route_details
|
||||
route_transport = str(direct_route.get("transport", ""))
|
||||
route_privacy = str(direct_route.get("ip_privacy", ""))
|
||||
if route_transport in ["direct", "relay"]:
|
||||
var address := str(direct_route.get("address", "")).strip_edges()
|
||||
var port := int(direct_route.get("port", 0))
|
||||
if not address.is_empty() and port >= 1 and port <= 65535:
|
||||
if ":" in address and not address.begins_with("["):
|
||||
address = "[%s]" % address
|
||||
_pending_join_endpoint = "%s:%d" % [address, port]
|
||||
if (
|
||||
_pending_join_token.is_empty()
|
||||
or _traversal_host.is_empty()
|
||||
or _traversal_port < 1
|
||||
or _pending_join_endpoint.is_empty()
|
||||
or route_transport != _pending_join_transport
|
||||
or (
|
||||
route_transport == "direct"
|
||||
and route_privacy != "peer_visible"
|
||||
)
|
||||
or (route_transport == "relay" and route_privacy != "relayed")
|
||||
or (
|
||||
route_transport == "direct"
|
||||
and (_traversal_host.is_empty() or _traversal_port < 1)
|
||||
)
|
||||
):
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
public_join_status_changed.emit(
|
||||
|
|
@ -768,7 +855,25 @@ func _on_join_request_completed(
|
|||
return
|
||||
_set_public_join_state(PublicJoinState.CONNECTING)
|
||||
public_join_status_changed.emit("Connecting…", false)
|
||||
public_join_prepared.emit(_pending_join_endpoint)
|
||||
var endpoint := EndpointParser.parse(_pending_join_endpoint)
|
||||
if not endpoint.is_valid():
|
||||
_set_public_join_state(PublicJoinState.ERROR)
|
||||
public_join_status_changed.emit("Could not reach this room.", true)
|
||||
_clear_pending_join()
|
||||
return
|
||||
public_join_prepared.emit(
|
||||
ConnectionRoute.discovery_relay(
|
||||
endpoint,
|
||||
_pending_join_room_id,
|
||||
_pending_join_room_name,
|
||||
)
|
||||
if route_transport == "relay"
|
||||
else ConnectionRoute.discovery_direct(
|
||||
endpoint,
|
||||
_pending_join_room_id,
|
||||
_pending_join_room_name,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
func _on_traversal_timer_timeout() -> void:
|
||||
|
|
@ -856,6 +961,15 @@ func _send_pending_join_probe() -> void:
|
|||
):
|
||||
_join_probe_timer.stop()
|
||||
return
|
||||
if _pending_join_transport == "relay":
|
||||
var relay_endpoint := EndpointParser.parse(_pending_join_endpoint)
|
||||
if relay_endpoint.is_valid():
|
||||
_session.send_traversal_packet(
|
||||
relay_endpoint.host,
|
||||
relay_endpoint.port,
|
||||
(RELAY_AUTH_PACKET_PREFIX + _pending_join_token).to_utf8_buffer(),
|
||||
)
|
||||
return
|
||||
_send_traversal_probe({
|
||||
"kind": "join",
|
||||
"room_id": _pending_join_room_id,
|
||||
|
|
@ -1013,10 +1127,24 @@ func _valid_public_room(room: Dictionary) -> bool:
|
|||
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 str(room.get("connection_mode", "")) in ["direct", "relay"]
|
||||
and (
|
||||
(
|
||||
str(room.get("connection_mode", "")) == "direct"
|
||||
and str(room.get("ip_privacy", "")) == "peer_visible"
|
||||
)
|
||||
or (
|
||||
str(room.get("connection_mode", "")) == "relay"
|
||||
and str(room.get("ip_privacy", "")) == "relayed"
|
||||
)
|
||||
)
|
||||
and str(room.get("host_kind", "")) in ["player", "dedicated"]
|
||||
and not (
|
||||
str(room.get("host_kind", "")) == "dedicated"
|
||||
and str(room.get("connection_mode", "")) == "relay"
|
||||
)
|
||||
and _valid_json_integer(room.get("current_players"), 0, 128)
|
||||
and _valid_json_integer(room.get("max_players"), 1, 128)
|
||||
and int(room["current_players"]) <= int(room["max_players"])
|
||||
|
|
@ -1370,6 +1498,18 @@ func _request_failure(response: Dictionary) -> String:
|
|||
return "Room discovery is temporarily unavailable."
|
||||
|
||||
|
||||
func _host_listing_status(room: Dictionary) -> String:
|
||||
if str(room.get("connection_mode", "")) == "relay":
|
||||
return (
|
||||
"Room is listed through the privacy relay. Players see a relay "
|
||||
+ "address; the relay service still handles network addresses."
|
||||
)
|
||||
return (
|
||||
"Room is listed. Its address is hidden while browsing, but direct "
|
||||
+ "players can see your public IP after joining."
|
||||
)
|
||||
|
||||
|
||||
func _discovery_version_mismatch_message(required_version: String) -> String:
|
||||
var local_version: String = NetworkProtocol.game_version()
|
||||
var rejection: NetworkProtocol.RejectionCode = (
|
||||
|
|
@ -1493,6 +1633,8 @@ func _clear_pending_join() -> void:
|
|||
_pending_join_endpoint = ""
|
||||
_pending_join_token = ""
|
||||
_pending_join_room_id = ""
|
||||
_pending_join_room_name = ""
|
||||
_pending_join_transport = ""
|
||||
_join_probe_timer.stop()
|
||||
|
||||
|
||||
|
|
@ -1571,6 +1713,14 @@ func _load_settings() -> void:
|
|||
_presence_sharing = bool(config.get_value(
|
||||
"social", "share_presence", false
|
||||
))
|
||||
var saved_connection_mode := str(config.get_value(
|
||||
"host", "connection_mode", HOST_CONNECTION_DIRECT
|
||||
))
|
||||
if saved_connection_mode in [
|
||||
HOST_CONNECTION_DIRECT,
|
||||
HOST_CONNECTION_RELAY,
|
||||
]:
|
||||
_host_connection_preference = saved_connection_mode
|
||||
|
||||
|
||||
func _save_settings() -> void:
|
||||
|
|
@ -1581,6 +1731,9 @@ func _save_settings() -> void:
|
|||
config.set_value(
|
||||
"host", "room_name_uses_default", _room_name_uses_default
|
||||
)
|
||||
config.set_value(
|
||||
"host", "connection_mode", _host_connection_preference
|
||||
)
|
||||
config.set_value("social", "share_presence", _presence_sharing)
|
||||
var error: Error = config.save(SETTINGS_PATH)
|
||||
if error != OK:
|
||||
|
|
|
|||
|
|
@ -485,17 +485,30 @@ func join_direct(endpoint_text: String) -> bool:
|
|||
if not endpoint.is_valid():
|
||||
_fail(endpoint.error_message)
|
||||
return false
|
||||
return join_route(ConnectionRoute.direct(endpoint))
|
||||
|
||||
|
||||
func join_route(route: ConnectionRoute) -> bool:
|
||||
if (
|
||||
state != State.INACTIVE
|
||||
or not _profile_ready
|
||||
or _profile == null
|
||||
or _spawn_service == null
|
||||
or route == null
|
||||
or not route.is_valid()
|
||||
):
|
||||
return false
|
||||
_operation_generation += 1
|
||||
var generation: int = _operation_generation
|
||||
_current_route = ConnectionRoute.direct(endpoint)
|
||||
_current_route = route
|
||||
_set_state(
|
||||
State.CONNECTING,
|
||||
"Connecting to %s..." % endpoint.normalized_display
|
||||
"Connecting to %s..." % route.display_description
|
||||
)
|
||||
_replace_transport()
|
||||
var error: Error = _transport.connect_to_route(_current_route)
|
||||
if error != OK:
|
||||
_fail("Could not begin the direct connection.")
|
||||
_fail("Could not begin the connection.")
|
||||
return false
|
||||
multiplayer.multiplayer_peer = _transport.get_multiplayer_peer()
|
||||
_connection_deadline = (
|
||||
|
|
@ -597,11 +610,23 @@ func get_current_endpoint() -> ConnectionEndpoint:
|
|||
return (
|
||||
_current_route.direct_endpoint
|
||||
if _current_route != null
|
||||
and _current_route.kind == ConnectionRoute.Kind.DIRECT
|
||||
and _current_route.kind in [
|
||||
ConnectionRoute.Kind.DIRECT,
|
||||
ConnectionRoute.Kind.DISCOVERY_DIRECT,
|
||||
ConnectionRoute.Kind.DISCOVERY_RELAY,
|
||||
]
|
||||
else null
|
||||
)
|
||||
|
||||
|
||||
func is_current_route_discovery() -> bool:
|
||||
return _current_route != null and _current_route.is_discovery_join()
|
||||
|
||||
|
||||
func is_current_route_relay() -> bool:
|
||||
return _current_route != null and _current_route.is_relay()
|
||||
|
||||
|
||||
func get_last_server_metadata() -> Dictionary:
|
||||
return {
|
||||
"server_display_name": _last_server_display_name,
|
||||
|
|
@ -1226,11 +1251,16 @@ func receive_server_identity_proof(data: Dictionary) -> void:
|
|||
_pending_server_proof = data.duplicate(true)
|
||||
_server_identity_fingerprint = fingerprint
|
||||
_server_identity_public_key = public_pem
|
||||
var verification := _server_trust.verify(
|
||||
get_current_endpoint(), fingerprint
|
||||
var verification := (
|
||||
_server_trust.verify_identity(fingerprint)
|
||||
if is_current_route_discovery()
|
||||
else _server_trust.verify(get_current_endpoint(), fingerprint)
|
||||
)
|
||||
if verification == ServerTrustStore.Verification.MATCH:
|
||||
_server_trust.touch(get_current_endpoint())
|
||||
if is_current_route_discovery():
|
||||
_server_trust.touch_identity(fingerprint)
|
||||
else:
|
||||
_server_trust.touch(get_current_endpoint())
|
||||
_send_client_identity_proof()
|
||||
return
|
||||
_set_state(
|
||||
|
|
@ -1238,9 +1268,12 @@ func receive_server_identity_proof(data: Dictionary) -> void:
|
|||
"Confirm this server identity before continuing.",
|
||||
)
|
||||
_connection_deadline = 0.0
|
||||
var expected := str(
|
||||
_server_trust.get_record(get_current_endpoint()).get("fingerprint", "")
|
||||
var trust_record: Dictionary = (
|
||||
_server_trust.get_identity_record(fingerprint)
|
||||
if is_current_route_discovery()
|
||||
else _server_trust.get_record(get_current_endpoint())
|
||||
)
|
||||
var expected := str(trust_record.get("fingerprint", ""))
|
||||
server_trust_required.emit(
|
||||
get_current_route_display(),
|
||||
expected,
|
||||
|
|
@ -1255,11 +1288,15 @@ func resolve_server_trust(accepted: bool) -> void:
|
|||
if not accepted:
|
||||
cancel_connection()
|
||||
return
|
||||
if not _server_trust.trust(
|
||||
get_current_endpoint(),
|
||||
str(_pending_server_proof["server_fingerprint"]),
|
||||
"straywild",
|
||||
):
|
||||
var fingerprint := str(_pending_server_proof["server_fingerprint"])
|
||||
var trusted := (
|
||||
_server_trust.trust_identity(fingerprint, get_current_route_display())
|
||||
if is_current_route_discovery()
|
||||
else _server_trust.trust(
|
||||
get_current_endpoint(), fingerprint, "straywild"
|
||||
)
|
||||
)
|
||||
if not trusted:
|
||||
_fail_identity("The server identity could not be pinned.")
|
||||
return
|
||||
_set_state(State.AUTHENTICATING, "Authenticating identity...")
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ func record_successful_connection(
|
|||
recent = _make_entry(endpoint, SavedServerEntry.Kind.RECENT)
|
||||
_recent_entries.append(recent)
|
||||
recent["display_name"] = (
|
||||
server_name if not server_name.is_empty() else endpoint.normalized_display
|
||||
server_name if not server_name.is_empty() else "Recent Server"
|
||||
)
|
||||
_apply_result_metadata(
|
||||
recent, now, "SUCCESS", server_name, protocol_version,
|
||||
|
|
@ -358,7 +358,11 @@ func _make_entry(
|
|||
var now: int = int(Time.get_unix_time_from_system())
|
||||
return {
|
||||
"entry_id": Crypto.new().generate_random_bytes(16).hex_encode(),
|
||||
"display_name": endpoint.host,
|
||||
"display_name": (
|
||||
"Saved Server"
|
||||
if kind == SavedServerEntry.Kind.SAVED
|
||||
else "Recent Server"
|
||||
),
|
||||
"route_kind": "DIRECT",
|
||||
"host": endpoint.host,
|
||||
"normalized_host": endpoint.host,
|
||||
|
|
|
|||
|
|
@ -39,6 +39,17 @@ func verify(endpoint: ConnectionEndpoint, fingerprint: String) -> Verification:
|
|||
)
|
||||
|
||||
|
||||
func verify_identity(fingerprint: String) -> Verification:
|
||||
_ensure_loaded()
|
||||
if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
|
||||
return Verification.CHANGED
|
||||
return (
|
||||
Verification.MATCH
|
||||
if _records.has(_identity_key(fingerprint))
|
||||
else Verification.FIRST_SEEN
|
||||
)
|
||||
|
||||
|
||||
func get_record(endpoint: ConnectionEndpoint) -> Dictionary:
|
||||
_ensure_loaded()
|
||||
if endpoint == null:
|
||||
|
|
@ -46,6 +57,11 @@ func get_record(endpoint: ConnectionEndpoint) -> Dictionary:
|
|||
return Dictionary(_records.get(endpoint.normalized_display, {})).duplicate(true)
|
||||
|
||||
|
||||
func get_identity_record(fingerprint: String) -> Dictionary:
|
||||
_ensure_loaded()
|
||||
return Dictionary(_records.get(_identity_key(fingerprint), {})).duplicate(true)
|
||||
|
||||
|
||||
func trust(
|
||||
endpoint: ConnectionEndpoint,
|
||||
fingerprint: String,
|
||||
|
|
@ -74,6 +90,25 @@ func trust(
|
|||
return _save()
|
||||
|
||||
|
||||
func trust_identity(fingerprint: String, server_name: String = "") -> bool:
|
||||
_ensure_loaded()
|
||||
if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
|
||||
return false
|
||||
var key := _identity_key(fingerprint)
|
||||
var now := int(Time.get_unix_time_from_system())
|
||||
var previous: Dictionary = _records.get(key, {})
|
||||
_records[key] = {
|
||||
"route_kind": "discovery",
|
||||
"identity_key": key,
|
||||
"fingerprint": fingerprint,
|
||||
"first_seen_unix": int(previous.get("first_seen_unix", now)),
|
||||
"last_seen_unix": now,
|
||||
"last_observed_server_name": server_name.left(80),
|
||||
"trust_state": "pinned",
|
||||
}
|
||||
return _save()
|
||||
|
||||
|
||||
func touch(endpoint: ConnectionEndpoint) -> void:
|
||||
_ensure_loaded()
|
||||
if endpoint == null:
|
||||
|
|
@ -85,6 +120,15 @@ func touch(endpoint: ConnectionEndpoint) -> void:
|
|||
_save()
|
||||
|
||||
|
||||
func touch_identity(fingerprint: String) -> void:
|
||||
_ensure_loaded()
|
||||
var record: Dictionary = _records.get(_identity_key(fingerprint), {})
|
||||
if record.is_empty():
|
||||
return
|
||||
record["last_seen_unix"] = int(Time.get_unix_time_from_system())
|
||||
_save()
|
||||
|
||||
|
||||
func _ensure_loaded() -> void:
|
||||
if _loaded:
|
||||
return
|
||||
|
|
@ -108,12 +152,21 @@ func _ensure_loaded() -> void:
|
|||
continue
|
||||
var record: Dictionary = value
|
||||
var endpoint := str(record.get("normalized_endpoint", ""))
|
||||
var identity_key := str(record.get("identity_key", ""))
|
||||
var fingerprint := str(record.get("fingerprint", ""))
|
||||
if not endpoint.is_empty() and NetworkIdentityCrypto.valid_fingerprint(fingerprint):
|
||||
if not NetworkIdentityCrypto.valid_fingerprint(fingerprint):
|
||||
continue
|
||||
if not endpoint.is_empty():
|
||||
_records[endpoint] = record.duplicate(true)
|
||||
elif identity_key == _identity_key(fingerprint):
|
||||
_records[identity_key] = record.duplicate(true)
|
||||
_expected_hash = PortableFileGuard.hash_file(_store_path)
|
||||
|
||||
|
||||
static func _identity_key(fingerprint: String) -> String:
|
||||
return "identity:%s" % fingerprint
|
||||
|
||||
|
||||
func _save() -> bool:
|
||||
if _write_blocked:
|
||||
return false
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ config_version=5
|
|||
[application]
|
||||
|
||||
config/name="straywild"
|
||||
config/version="0.20.2-alpha"
|
||||
config/version="0.20.3-alpha"
|
||||
run/main_scene="res://main/main.tscn"
|
||||
config/features=PackedStringArray("4.7", "GL Compatibility")
|
||||
config/icon="res://art/exported/system_icons/straywild_256.png"
|
||||
|
|
|
|||
|
|
@ -4,14 +4,14 @@ set -euo pipefail
|
|||
|
||||
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
readonly PROJECT_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.2-alpha"
|
||||
readonly BUILD_ROOT="${PROJECT_ROOT}/builds/v0.20.3-alpha"
|
||||
readonly WINDOWS_DIR="${BUILD_ROOT}/windows-x86_64"
|
||||
readonly LINUX_DIR="${BUILD_ROOT}/linux-x86_64"
|
||||
readonly README_SOURCE="${PROJECT_ROOT}/docs/README-PLAYTEST.txt"
|
||||
readonly SOURCE_COMMIT="$(git -C "${PROJECT_ROOT}" rev-parse HEAD)"
|
||||
readonly SOURCE_URL="https://forge.makearmy.io/woofmeow/straywild"
|
||||
readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-windows-x86_64.zip"
|
||||
readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.2-alpha-linux-x86_64.zip"
|
||||
readonly WINDOWS_ZIP="${BUILD_ROOT}/straywild-v0.20.3-alpha-windows-x86_64.zip"
|
||||
readonly LINUX_ZIP="${BUILD_ROOT}/straywild-v0.20.3-alpha-linux-x86_64.zip"
|
||||
readonly GODOT_BIN="${GODOT_BIN:-godot}"
|
||||
|
||||
if [[ ! -f "${PROJECT_ROOT}/project.godot" ]]; then
|
||||
|
|
@ -78,7 +78,7 @@ Source repository: ${SOURCE_URL}
|
|||
Source revision at build time: ${SOURCE_COMMIT}
|
||||
|
||||
This private testing release is distributed with an exact corresponding
|
||||
source archive named straywild-v0.20.2-alpha-source.tar.gz.
|
||||
source archive named straywild-v0.20.3-alpha-source.tar.gz.
|
||||
|
||||
If this package includes uncommitted development changes, it must not be
|
||||
publicly redistributed until the exact corresponding source is also made
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ readonly -a QUICK_TESTS=(
|
|||
"tests/controller_world_interaction_validation.gd"
|
||||
"tests/controller_ui_navigation_validation.gd"
|
||||
"tests/dedicated_server_config_validation.gd"
|
||||
"tests/discovery_privacy_validation.gd"
|
||||
"tests/digging_prototype_validation.gd"
|
||||
"tests/equipment_catalog_validation.gd"
|
||||
"tests/exported_decal_hotfix_validation.gd"
|
||||
|
|
|
|||
|
|
@ -178,16 +178,39 @@ func _run() -> void:
|
|||
)
|
||||
assert(not FileAccess.file_exists(discovery_settings_path))
|
||||
assert(discovery.set_room_name("Client Room"))
|
||||
assert(
|
||||
discovery.get_host_discovery_mode()
|
||||
== DiscoveryClient.HOST_DISCOVERY_PRIVATE
|
||||
)
|
||||
assert(discovery.set_host_connection_mode(
|
||||
DiscoveryClient.HOST_CONNECTION_RELAY
|
||||
))
|
||||
assert(
|
||||
discovery.get_host_connection_mode()
|
||||
== DiscoveryClient.HOST_CONNECTION_RELAY
|
||||
)
|
||||
assert(
|
||||
discovery.get_host_discovery_mode()
|
||||
== DiscoveryClient.HOST_DISCOVERY_PRIVATE
|
||||
)
|
||||
var client_settings: PackedByteArray = FileAccess.get_file_as_bytes(
|
||||
discovery_settings_path
|
||||
)
|
||||
assert(not client_settings.is_empty())
|
||||
var dedicated_discovery := DiscoveryClient.new()
|
||||
dedicated_discovery.call("_load_settings")
|
||||
dedicated_discovery.set(
|
||||
"_host_connection_preference",
|
||||
DiscoveryClient.HOST_CONNECTION_RELAY,
|
||||
)
|
||||
assert(
|
||||
dedicated_discovery.configure_dedicated_runtime("Headless Room")
|
||||
)
|
||||
assert(dedicated_discovery.get_room_name() == "Headless Room")
|
||||
assert(
|
||||
dedicated_discovery.get_host_connection_mode()
|
||||
== DiscoveryClient.HOST_CONNECTION_DIRECT
|
||||
)
|
||||
assert(
|
||||
FileAccess.get_file_as_bytes(discovery_settings_path)
|
||||
== client_settings
|
||||
|
|
@ -210,16 +233,26 @@ func _run() -> void:
|
|||
var listed_room := {
|
||||
"room_id": "empty-dedicated-room",
|
||||
"room_name": "Empty Dedicated Room",
|
||||
"address": "203.0.113.10",
|
||||
"port": 7777,
|
||||
"current_players": 0,
|
||||
"max_players": 8,
|
||||
"game_version": str(ProjectSettings.get_setting(
|
||||
"application/config/version", "unknown"
|
||||
)),
|
||||
"protocol_version": NetworkProtocol.PROTOCOL_VERSION,
|
||||
"connection_mode": "direct",
|
||||
"ip_privacy": "peer_visible",
|
||||
"host_kind": "dedicated",
|
||||
}
|
||||
assert(bool(discovery.call("_valid_public_room", listed_room)))
|
||||
assert(not listed_room.has("address"))
|
||||
assert(not listed_room.has("port"))
|
||||
var relayed_room: Dictionary = listed_room.duplicate(true)
|
||||
relayed_room["connection_mode"] = "relay"
|
||||
relayed_room["ip_privacy"] = "relayed"
|
||||
assert(not bool(discovery.call("_valid_public_room", relayed_room)))
|
||||
var player_relayed_room: Dictionary = relayed_room.duplicate(true)
|
||||
player_relayed_room["host_kind"] = "player"
|
||||
assert(bool(discovery.call("_valid_public_room", player_relayed_room)))
|
||||
var incompatible_room: Dictionary = listed_room.duplicate(true)
|
||||
incompatible_room["game_version"] = "0.0.0-alpha"
|
||||
assert(not bool(discovery.call("_valid_public_room", incompatible_room)))
|
||||
|
|
|
|||
98
tests/discovery_privacy_validation.gd
Normal file
98
tests/discovery_privacy_validation.gd
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
extends SceneTree
|
||||
|
||||
const FINGERPRINT: String = (
|
||||
"abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
|
||||
)
|
||||
const JoinGamePageScene: PackedScene = preload(
|
||||
"res://ui/network/join_game_page.tscn"
|
||||
)
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
call_deferred("_run")
|
||||
|
||||
|
||||
func _run() -> void:
|
||||
var endpoint := EndpointParser.parse("203.0.113.20:49152")
|
||||
assert(endpoint.is_valid())
|
||||
var direct_route := ConnectionRoute.direct(endpoint)
|
||||
assert(direct_route.is_valid())
|
||||
assert("203.0.113.20" not in direct_route.display_description)
|
||||
var route := ConnectionRoute.discovery_direct(
|
||||
endpoint,
|
||||
"opaque-room-id",
|
||||
"Pond Friends",
|
||||
)
|
||||
assert(route.is_valid())
|
||||
assert(route.is_discovery_join())
|
||||
assert(route.display_description == "Pond Friends")
|
||||
assert("203.0.113.20" not in route.display_description)
|
||||
var relay_route := ConnectionRoute.discovery_relay(
|
||||
endpoint,
|
||||
"opaque-room-id",
|
||||
"Pond Friends",
|
||||
)
|
||||
assert(relay_route.is_valid())
|
||||
assert(relay_route.is_discovery_join())
|
||||
assert(relay_route.is_relay())
|
||||
assert("203.0.113.20" not in relay_route.display_description)
|
||||
|
||||
var join_page := JoinGamePageScene.instantiate() as JoinGamePage
|
||||
root.add_child(join_page)
|
||||
assert((join_page.get_node("%Address") as LineEdit).secret)
|
||||
assert(str(join_page.call("_display_endpoint", "203.0.113.20:49152")) == "*****")
|
||||
join_page.set("_addresses_visible", true)
|
||||
join_page.call("_refresh_address_privacy_controls")
|
||||
assert(not (join_page.get_node("%Address") as LineEdit).secret)
|
||||
assert(
|
||||
str(join_page.call("_display_endpoint", "203.0.113.20:49152"))
|
||||
== "203.0.113.20:49152"
|
||||
)
|
||||
var saved_entry := SavedServerEntry.new()
|
||||
saved_entry.kind = SavedServerEntry.Kind.SAVED
|
||||
saved_entry.display_name = "203.0.113.20"
|
||||
saved_entry.host = "203.0.113.20"
|
||||
saved_entry.normalized_host = "203.0.113.20"
|
||||
saved_entry.normalized_endpoint = "203.0.113.20:49152"
|
||||
join_page.set("_addresses_visible", false)
|
||||
assert(str(join_page.call("_display_entry_name", saved_entry)) == "saved server")
|
||||
join_page.set("_addresses_visible", true)
|
||||
assert(str(join_page.call("_display_entry_name", saved_entry)) == "203.0.113.20")
|
||||
|
||||
var data_root := PlayerDataRoot.new()
|
||||
root.add_child(data_root)
|
||||
var portable_path := ProjectSettings.globalize_path(
|
||||
"user://discovery-privacy-validation"
|
||||
)
|
||||
var created := data_root.create_unbound_root(portable_path)
|
||||
assert(bool(created.get("ok", false)))
|
||||
assert(data_root.activate_process_root(portable_path))
|
||||
|
||||
var store := ServerTrustStore.new()
|
||||
root.add_child(store)
|
||||
var trust_path := data_root.path_for(&"server_trust")
|
||||
store.configure_storage(trust_path, data_root)
|
||||
assert(store.verify_identity(FINGERPRINT) == ServerTrustStore.Verification.FIRST_SEEN)
|
||||
assert(store.trust_identity(FINGERPRINT, "Pond Friends"))
|
||||
assert(store.verify_identity(FINGERPRINT) == ServerTrustStore.Verification.MATCH)
|
||||
var saved_text := FileAccess.get_file_as_string(trust_path)
|
||||
assert("203.0.113.20" not in saved_text)
|
||||
assert("normalized_endpoint" not in saved_text)
|
||||
assert("normalized_host" not in saved_text)
|
||||
assert("\"port\"" not in saved_text)
|
||||
|
||||
var reloaded := ServerTrustStore.new()
|
||||
root.add_child(reloaded)
|
||||
reloaded.configure_storage(trust_path, data_root)
|
||||
assert(
|
||||
reloaded.verify_identity(FINGERPRINT)
|
||||
== ServerTrustStore.Verification.MATCH
|
||||
)
|
||||
var record := reloaded.get_identity_record(FINGERPRINT)
|
||||
assert(record.get("route_kind") == "discovery")
|
||||
assert(not record.has("normalized_endpoint"))
|
||||
assert(not record.has("normalized_host"))
|
||||
assert(not record.has("port"))
|
||||
|
||||
print("Discovery privacy validation: PASS")
|
||||
quit()
|
||||
1
tests/discovery_privacy_validation.gd.uid
Normal file
1
tests/discovery_privacy_validation.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dmtlh1kpm7bl1
|
||||
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