diff --git a/art/source/characters/base/netfishing_base_character.blend b/art/source/characters/base/netfishing_base_character.blend index 33dfc3c..12aa0fa 100644 Binary files a/art/source/characters/base/netfishing_base_character.blend and b/art/source/characters/base/netfishing_base_character.blend differ diff --git a/main/main.gd b/main/main.gd index bd18c78..d2f5542 100644 --- a/main/main.gd +++ b/main/main.gd @@ -455,20 +455,6 @@ func _on_pause_join_game_requested(endpoint: String) -> void: func _on_network_join_authenticated() -> void: - var connected_endpoint: ConnectionEndpoint = ( - _network_session.get_current_endpoint() - ) - var server_metadata: Dictionary = ( - _network_session.get_last_server_metadata() - ) - if connected_endpoint != null: - _saved_servers.record_successful_connection( - connected_endpoint, - int(server_metadata.get("max_players", 0)), - str(server_metadata.get("server_display_name", "")), - int(server_metadata.get("protocol_version", 0)), - int(server_metadata.get("player_count", 0)), - ) if _join_requested_from_title: var inspection = _save_manager.inspect_save() var progression_ready: bool = ( @@ -496,33 +482,12 @@ func _on_network_join_authenticated() -> void: func _on_network_connection_error(message: String) -> void: - var failed_endpoint: ConnectionEndpoint = ( - _network_session.get_current_endpoint() - ) - if failed_endpoint != null: - _saved_servers.record_connection_failure( - failed_endpoint, - _connection_result_code(message), - ) if _join_requested_from_title: _game_ui.get_title_screen().report_network_error(message) elif _join_requested_from_pause: _handle_failed_session_switch(message) -func _connection_result_code(message: String) -> String: - var lowered: String = message.to_lower() - if lowered.contains("protocol"): - return "PROTOCOL_MISMATCH" - if lowered.contains("full"): - return "SERVER_FULL" - if lowered.contains("cancel"): - return "CANCELLED" - if lowered.contains("timed out"): - return "TIMEOUT" - return "UNAVAILABLE" - - func _handle_failed_session_switch(message: String) -> void: _join_requested_from_pause = false _set_gameplay_active(false) diff --git a/network/network_session.gd b/network/network_session.gd index 5c8e1e1..0bb8255 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -56,9 +56,6 @@ var _input_sequence: int = 0 var _input_accumulator: float = 0.0 var _snapshot_accumulator: float = 0.0 var _last_server_max_players: int = DEFAULT_SESSION_MAX_PLAYERS -var _last_server_player_count: int = 0 -var _last_server_display_name: String = "" -var _last_server_protocol_version: int = 0 var _profile_ready: bool = false @@ -232,24 +229,6 @@ func get_current_route_display() -> String: ) -func get_current_endpoint() -> ConnectionEndpoint: - return ( - _current_route.direct_endpoint - if _current_route != null - and _current_route.kind == ConnectionRoute.Kind.DIRECT - else null - ) - - -func get_last_server_metadata() -> Dictionary: - return { - "server_display_name": _last_server_display_name, - "protocol_version": _last_server_protocol_version, - "player_count": _last_server_player_count, - "max_players": _last_server_max_players, - } - - func can_use_host_gameplay() -> bool: return is_host() @@ -478,11 +457,6 @@ func receive_server_hello(data: Dictionary) -> void: "max_players", DEFAULT_SESSION_MAX_PLAYERS )) - _last_server_player_count = int(data.get("player_count", 1)) - _last_server_display_name = str(data.get("server_display_name", "")) - _last_server_protocol_version = int(data.get( - "protocol_version", NetworkProtocol.PROTOCOL_VERSION - )) var local_peer_id: int = multiplayer.get_unique_id() _registry.clear() _registry.add_peer( @@ -501,6 +475,11 @@ func receive_server_hello(data: Dictionary) -> void: _last_server_max_players, ] ) + if _current_route != null and _saved_servers != null: + _saved_servers.record_successful_connection( + _current_route.direct_endpoint, + _last_server_max_players + ) join_authenticated.emit() diff --git a/network/saved_server_entry.gd b/network/saved_server_entry.gd deleted file mode 100644 index 00052bc..0000000 --- a/network/saved_server_entry.gd +++ /dev/null @@ -1,88 +0,0 @@ -class_name SavedServerEntry -extends RefCounted - -enum Kind { - SAVED, - RECENT, -} - -var kind: Kind = Kind.SAVED -var entry_id: String = "" -var display_name: String = "" -var route_kind: String = "DIRECT" -var host: String = "" -var normalized_host: String = "" -var port: int = EndpointParser.DEFAULT_PORT -var normalized_endpoint: String = "" -var favorite: bool = false -var created_at_unix: int = 0 -var updated_at_unix: int = 0 -var last_success_at_unix: int = 0 -var last_result_code: String = "" -var last_observed_server_name: String = "" -var last_observed_protocol_version: int = 0 -var last_observed_player_count: int = 0 -var last_observed_max_players: int = 0 - - -static func from_dictionary( - data: Dictionary, - entry_kind: Kind, -) -> SavedServerEntry: - var entry := SavedServerEntry.new() - entry.kind = entry_kind - entry.entry_id = str(data.get("entry_id", "")) - entry.display_name = str(data.get("display_name", "")) - entry.route_kind = str(data.get("route_kind", "DIRECT")) - entry.host = str(data.get("host", "")) - entry.normalized_host = str(data.get("normalized_host", entry.host)) - entry.port = int(data.get("port", EndpointParser.DEFAULT_PORT)) - entry.normalized_endpoint = str(data.get("normalized_endpoint", "")) - entry.favorite = bool(data.get("favorite", false)) - entry.created_at_unix = int(data.get("created_at_unix", 0)) - entry.updated_at_unix = int(data.get("updated_at_unix", 0)) - entry.last_success_at_unix = int(data.get("last_success_at_unix", 0)) - entry.last_result_code = str(data.get("last_result_code", "")) - entry.last_observed_server_name = str( - data.get("last_observed_server_name", "") - ) - entry.last_observed_protocol_version = int( - data.get("last_observed_protocol_version", 0) - ) - entry.last_observed_player_count = int( - data.get("last_observed_player_count", 0) - ) - entry.last_observed_max_players = int( - data.get("last_observed_max_players", 0) - ) - return entry - - -func to_dictionary() -> Dictionary: - return { - "entry_id": entry_id, - "display_name": display_name, - "route_kind": route_kind, - "host": host, - "normalized_host": normalized_host, - "port": port, - "normalized_endpoint": normalized_endpoint, - "favorite": favorite, - "created_at_unix": created_at_unix, - "updated_at_unix": updated_at_unix, - "last_success_at_unix": last_success_at_unix, - "last_result_code": last_result_code, - "last_observed_server_name": last_observed_server_name, - "last_observed_protocol_version": last_observed_protocol_version, - "last_observed_player_count": last_observed_player_count, - "last_observed_max_players": last_observed_max_players, - } - - -func get_endpoint() -> ConnectionEndpoint: - var endpoint_text: String = ( - "[%s]:%d" % [host, port] - if host.contains(":") - else "%s:%d" % [host, port] - ) - return EndpointParser.parse(endpoint_text) diff --git a/network/saved_server_entry.gd.uid b/network/saved_server_entry.gd.uid deleted file mode 100644 index 97f6372..0000000 --- a/network/saved_server_entry.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://b3guw5fet7lv0 diff --git a/network/saved_server_store.gd b/network/saved_server_store.gd index d17f82d..ca2f5b8 100644 --- a/network/saved_server_store.gd +++ b/network/saved_server_store.gd @@ -5,235 +5,86 @@ const FORMAT_VERSION: int = 1 const STORE_PATH: String = "user://saved_servers.json" const TEMP_PATH: String = "user://saved_servers.json.tmp" const BACKUP_PATH: String = "user://saved_servers.json.backup" -const MAX_SAVED_ENTRIES: int = 100 -const MAX_RECENT_ENTRIES: int = 20 -const MAX_DISPLAY_NAME_LENGTH: int = 80 +const MAX_ENTRIES: int = 100 -signal data_changed -signal recovery_warning_changed(message: String) - -var _saved_entries: Array[Dictionary] = [] -var _recent_entries: Array[Dictionary] = [] +var _entries: Array[Dictionary] = [] var _loaded: bool = false -var _recovery_warning: String = "" -var _write_blocked: bool = false - - -func get_saved_entries() -> Array[SavedServerEntry]: - _ensure_loaded() - var result: Array[SavedServerEntry] = [] - for data: Dictionary in _saved_entries: - result.append(SavedServerEntry.from_dictionary( - data, SavedServerEntry.Kind.SAVED - )) - result.sort_custom(_sort_saved_entries) - return result - - -func get_recent_entries() -> Array[SavedServerEntry]: - _ensure_loaded() - var result: Array[SavedServerEntry] = [] - for data: Dictionary in _recent_entries: - result.append(SavedServerEntry.from_dictionary( - data, SavedServerEntry.Kind.RECENT - )) - result.sort_custom(func(a: SavedServerEntry, b: SavedServerEntry) -> bool: - return a.last_success_at_unix > b.last_success_at_unix - ) - return result func list_entries() -> Array[Dictionary]: _ensure_loaded() - return _saved_entries.duplicate(true) - - -func get_recovery_warning() -> String: - _ensure_loaded() - return _recovery_warning - - -func find_saved_by_endpoint( - endpoint: ConnectionEndpoint, -) -> SavedServerEntry: - _ensure_loaded() - if endpoint == null or not endpoint.is_valid(): - return null - for data: Dictionary in _saved_entries: - if data.get("normalized_endpoint", "") == endpoint.normalized_display: - return SavedServerEntry.from_dictionary( - data, SavedServerEntry.Kind.SAVED - ) - return null + return _entries.duplicate(true) func find_by_normalized_endpoint(value: String) -> Dictionary: - var endpoint := EndpointParser.parse(value) - var entry: SavedServerEntry = find_saved_by_endpoint(endpoint) - return entry.to_dictionary() if entry != null else {} + _ensure_loaded() + for entry: Dictionary in _entries: + if entry.get("normalized_endpoint", "") == value: + return entry.duplicate(true) + return {} -func save_or_update_entry( +func save_entry( display_name: String, endpoint: ConnectionEndpoint, - entry_id: String = "", -) -> SavedServerEntry: +) -> bool: _ensure_loaded() - var clean_name: String = display_name.strip_edges() if ( endpoint == null or not endpoint.is_valid() - or clean_name.is_empty() - or clean_name.length() > MAX_DISPLAY_NAME_LENGTH + or display_name.strip_edges().is_empty() + or display_name.length() > 80 ): - return null + return false var now: int = int(Time.get_unix_time_from_system()) - var target: Dictionary = {} - for data: Dictionary in _saved_entries: - if not entry_id.is_empty() and data.get("entry_id", "") == entry_id: - target = data - break - for data: Dictionary in _saved_entries: - if ( - data.get("normalized_endpoint", "") == endpoint.normalized_display - and data != target - ): - return null - if target.is_empty(): - if _saved_entries.size() >= MAX_SAVED_ENTRIES: - return null - target = _make_entry(endpoint, SavedServerEntry.Kind.SAVED) - _saved_entries.append(target) - target["display_name"] = clean_name - target["host"] = endpoint.host - target["normalized_host"] = endpoint.host - target["port"] = endpoint.port - target["normalized_endpoint"] = endpoint.normalized_display - target["updated_at_unix"] = now - if not _save_atomic(): - return null - data_changed.emit() - return SavedServerEntry.from_dictionary( - target, SavedServerEntry.Kind.SAVED - ) - - -func save_entry(display_name: String, endpoint: ConnectionEndpoint) -> bool: - return save_or_update_entry(display_name, endpoint) != null - - -func remove_saved_entry(entry_id: String) -> bool: - _ensure_loaded() - for index: int in range(_saved_entries.size()): - if _saved_entries[index].get("entry_id", "") == entry_id: - _saved_entries.remove_at(index) - if not _save_atomic(): - return false - data_changed.emit() - return true - return false + for entry: Dictionary in _entries: + if entry.get("normalized_endpoint", "") == endpoint.normalized_display: + entry["display_name"] = display_name.strip_edges() + entry["host"] = endpoint.host + entry["port"] = endpoint.port + entry["updated_at_unix"] = now + return _save_atomic() + if _entries.size() >= MAX_ENTRIES: + return false + _entries.append({ + "entry_id": Crypto.new().generate_random_bytes(16).hex_encode(), + "display_name": display_name.strip_edges(), + "route_kind": "DIRECT", + "host": endpoint.host, + "port": endpoint.port, + "normalized_endpoint": endpoint.normalized_display, + "favorite": false, + "created_at_unix": now, + "updated_at_unix": now, + "last_success_at_unix": 0, + "last_observed_max_players": 0, + }) + return _save_atomic() func remove_entry(entry_id: String) -> bool: - return remove_saved_entry(entry_id) - - -func set_favorite(entry_id: String, favorite: bool) -> bool: _ensure_loaded() - for data: Dictionary in _saved_entries: - if data.get("entry_id", "") == entry_id: - data["favorite"] = favorite - data["updated_at_unix"] = int(Time.get_unix_time_from_system()) - if not _save_atomic(): - return false - data_changed.emit() - return true + for index: int in range(_entries.size()): + if _entries[index].get("entry_id", "") == entry_id: + _entries.remove_at(index) + return _save_atomic() return false func record_successful_connection( endpoint: ConnectionEndpoint, observed_max_players: int, - server_name: String = "", - protocol_version: int = 0, - player_count: int = 0, ) -> bool: _ensure_loaded() if endpoint == null or not endpoint.is_valid(): return false - var now: int = int(Time.get_unix_time_from_system()) - for data: Dictionary in _saved_entries: - if data.get("normalized_endpoint", "") == endpoint.normalized_display: - _apply_result_metadata( - data, now, "SUCCESS", server_name, protocol_version, - player_count, observed_max_players - ) - var recent: Dictionary = {} - for data: Dictionary in _recent_entries: - if data.get("normalized_endpoint", "") == endpoint.normalized_display: - recent = data - break - if recent.is_empty(): - 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 - ) - _apply_result_metadata( - recent, now, "SUCCESS", server_name, protocol_version, - player_count, observed_max_players - ) - _recent_entries.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: - return int(a.get("last_success_at_unix", 0)) > int( - b.get("last_success_at_unix", 0) - ) - ) - if _recent_entries.size() > MAX_RECENT_ENTRIES: - _recent_entries.resize(MAX_RECENT_ENTRIES) - if not _save_atomic(): - return false - data_changed.emit() - return true - - -func record_connection_failure( - endpoint: ConnectionEndpoint, - result_code: String, -) -> bool: - _ensure_loaded() - if endpoint == null or not endpoint.is_valid(): - return false - for data: Dictionary in _saved_entries: - if data.get("normalized_endpoint", "") == endpoint.normalized_display: - data["last_result_code"] = result_code.left(48) - data["updated_at_unix"] = int(Time.get_unix_time_from_system()) - if not _save_atomic(): - return false - data_changed.emit() - break - return true - - -func remove_recent_entry(entry_id: String) -> bool: - _ensure_loaded() - for index: int in range(_recent_entries.size()): - if _recent_entries[index].get("entry_id", "") == entry_id: - _recent_entries.remove_at(index) - if not _save_atomic(): - return false - data_changed.emit() - return true - return false - - -func clear_recent_entries() -> bool: - _ensure_loaded() - if _recent_entries.is_empty(): - return true - _recent_entries.clear() - if not _save_atomic(): - return false - data_changed.emit() + for entry: Dictionary in _entries: + if entry.get("normalized_endpoint", "") == endpoint.normalized_display: + entry["last_success_at_unix"] = int(Time.get_unix_time_from_system()) + entry["last_observed_max_players"] = maxi(observed_max_players, 0) + entry["updated_at_unix"] = int(Time.get_unix_time_from_system()) + return _save_atomic() + # Successful manual connections are deliberately not auto-saved. return true @@ -244,158 +95,54 @@ func _ensure_loaded() -> void: _recover_interrupted_write() if not FileAccess.file_exists(STORE_PATH): return - var data: Dictionary = _read_store(STORE_PATH) - if data.is_empty() and FileAccess.file_exists(BACKUP_PATH): - data = _read_store(BACKUP_PATH) - if not data.is_empty(): - _set_warning("Recovered saved servers from the local backup.") - if data.is_empty(): - if FileAccess.file_exists(STORE_PATH): - _set_warning( - "Saved servers could not be read. Direct connection is still available." - ) + var file := FileAccess.open(STORE_PATH, FileAccess.READ) + if file == null: return + var json := JSON.new() + var error: Error = json.parse(file.get_as_text()) + file.close() + if error != OK or typeof(json.data) != TYPE_DICTIONARY: + return + var data: Dictionary = json.data if data.get("format_version") != FORMAT_VERSION: - _write_blocked = true - _set_warning( - "Saved servers use a newer format. Direct connection is still available." - ) return - _load_collection(data.get("saved_entries", data.get("entries", [])), true) - _load_collection(data.get("recent_entries", []), false) - - -func _load_collection(raw: Variant, is_saved: bool) -> void: - if typeof(raw) != TYPE_ARRAY: - _set_warning("Some saved-server data was unavailable.") + var raw_entries: Variant = data.get("entries") + if typeof(raw_entries) != TYPE_ARRAY: return - var target: Array[Dictionary] = ( - _saved_entries if is_saved else _recent_entries - ) var seen: Dictionary[String, bool] = {} - for value: Variant in raw: + for value: Variant in raw_entries: if typeof(value) != TYPE_DICTIONARY: - _set_warning("Some malformed server entries were skipped.") continue - var validated: Dictionary = _validate_entry(value, is_saved) - if validated.is_empty(): - _set_warning("Some malformed server entries were skipped.") - continue - var identity: String = validated["normalized_endpoint"] - if seen.has(identity): - continue - seen[identity] = true - target.append(validated) - if target.size() >= ( - MAX_SAVED_ENTRIES if is_saved else MAX_RECENT_ENTRIES + var entry: Dictionary = value + var stored_host: String = str(entry.get("host", "")) + var stored_port: int = int(entry.get("port", 0)) + var endpoint_text: String = ( + "[%s]:%d" % [stored_host, stored_port] + if stored_host.contains(":") + else "%s:%d" % [stored_host, stored_port] + ) + var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text) + if ( + not endpoint.is_valid() + or seen.has(endpoint.normalized_display) + or typeof(entry.get("entry_id")) != TYPE_STRING + or typeof(entry.get("display_name")) != TYPE_STRING ): + continue + entry["normalized_endpoint"] = endpoint.normalized_display + seen[endpoint.normalized_display] = true + _entries.append(entry.duplicate(true)) + if _entries.size() >= MAX_ENTRIES: break -func _validate_entry(value: Dictionary, is_saved: bool) -> Dictionary: - if ( - typeof(value.get("entry_id")) != TYPE_STRING - or str(value.get("entry_id", "")).is_empty() - or typeof(value.get("host")) != TYPE_STRING - ): - return {} - var stored_host: String = str(value.get("host", "")) - var stored_port: int = int(value.get("port", 0)) - var endpoint_text: String = ( - "[%s]:%d" % [stored_host, stored_port] - if stored_host.contains(":") - else "%s:%d" % [stored_host, stored_port] - ) - var endpoint := EndpointParser.parse(endpoint_text) - if not endpoint.is_valid(): - return {} - var name: String = str(value.get("display_name", "")).strip_edges() - if is_saved and ( - name.is_empty() or name.length() > MAX_DISPLAY_NAME_LENGTH - ): - return {} - var result: Dictionary = _make_entry( - endpoint, - SavedServerEntry.Kind.SAVED if is_saved else SavedServerEntry.Kind.RECENT - ) - for key: String in result: - if value.has(key) and typeof(value[key]) == typeof(result[key]): - result[key] = value[key] - result["host"] = endpoint.host - result["normalized_host"] = endpoint.host - result["port"] = endpoint.port - result["normalized_endpoint"] = endpoint.normalized_display - return result - - -func _make_entry( - endpoint: ConnectionEndpoint, - kind: SavedServerEntry.Kind, -) -> Dictionary: - 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, - "route_kind": "DIRECT", - "host": endpoint.host, - "normalized_host": endpoint.host, - "port": endpoint.port, - "normalized_endpoint": endpoint.normalized_display, - "favorite": false, - "created_at_unix": now, - "updated_at_unix": now, - "last_success_at_unix": 0, - "last_result_code": "", - "last_observed_server_name": "", - "last_observed_protocol_version": 0, - "last_observed_player_count": 0, - "last_observed_max_players": 0, - "entry_kind": int(kind), - } - - -func _apply_result_metadata( - data: Dictionary, - now: int, - result_code: String, - server_name: String, - protocol_version: int, - player_count: int, - max_players: int, -) -> void: - data["last_success_at_unix"] = now - data["updated_at_unix"] = now - data["last_result_code"] = result_code - data["last_observed_server_name"] = server_name.left(80) - data["last_observed_protocol_version"] = maxi(protocol_version, 0) - data["last_observed_player_count"] = maxi(player_count, 0) - data["last_observed_max_players"] = maxi(max_players, 0) - - -func _sort_saved_entries( - a: SavedServerEntry, - b: SavedServerEntry, -) -> bool: - if a.favorite != b.favorite: - return a.favorite - if a.last_success_at_unix != b.last_success_at_unix: - return a.last_success_at_unix > b.last_success_at_unix - var name_compare: int = a.display_name.naturalnocasecmp_to(b.display_name) - if name_compare != 0: - return name_compare < 0 - return a.entry_id < b.entry_id - - func _save_atomic() -> bool: - if _write_blocked: - return false var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE) if file == null: return false file.store_string(JSON.stringify({ "format_version": FORMAT_VERSION, - "saved_entries": _saved_entries, - "recent_entries": _recent_entries, + "entries": _entries, }, "\t")) file.flush() var error: Error = file.get_error() @@ -416,32 +163,16 @@ func _save_atomic() -> bool: return true -func _read_store(path: String) -> Dictionary: - var file := FileAccess.open(path, FileAccess.READ) - if file == null: - return {} - var json := JSON.new() - var error: Error = json.parse(file.get_as_text()) - file.close() - return json.data if error == OK and typeof(json.data) == TYPE_DICTIONARY else {} - - func _recover_interrupted_write() -> void: if FileAccess.file_exists(STORE_PATH): _remove_if_present(TEMP_PATH) + _remove_if_present(BACKUP_PATH) return if FileAccess.file_exists(BACKUP_PATH): _rename(BACKUP_PATH, STORE_PATH) - _set_warning("Recovered saved servers after an interrupted write.") _remove_if_present(TEMP_PATH) -func _set_warning(message: String) -> void: - if _recovery_warning.is_empty(): - _recovery_warning = message - recovery_warning_changed.emit(_recovery_warning) - - func _rename(from_path: String, to_path: String) -> bool: return DirAccess.rename_absolute( ProjectSettings.globalize_path(from_path), diff --git a/ui/network/join_game_page.gd b/ui/network/join_game_page.gd index a34da52..9952e0c 100644 --- a/ui/network/join_game_page.gd +++ b/ui/network/join_game_page.gd @@ -4,84 +4,25 @@ extends Control signal join_requested(endpoint: String) signal back_requested -enum Mode { - DIRECT, - SAVED, - RECENT, -} - -const ADDRESS_FORMAT_HELP: String = ( - "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted." -) -const DIRECT_WORKFLOW_HELP: String = ( - "%s\nJoin Now connects once; Save Server stores this address locally." - % ADDRESS_FORMAT_HELP -) - @onready var _address: LineEdit = %Address -@onready var _address_label: Label = %AddressLabel -@onready var _address_helper: Label = %AddressHelper -@onready var _name_edit: LineEdit = %NameEdit -@onready var _name_label: Label = %NameLabel -@onready var _name_helper: Label = %NameHelper -@onready var _server_list: ItemList = %ServerList -@onready var _details: Label = %Details -@onready var _direct_button: BubbleButton = %DirectButton -@onready var _saved_button: BubbleButton = %SavedButton -@onready var _recent_button: BubbleButton = %RecentButton @onready var _join_button: BubbleButton = %JoinButton -@onready var _save_button: BubbleButton = %SaveButton -@onready var _edit_button: BubbleButton = %EditButton -@onready var _favorite_button: BubbleButton = %FavoriteButton -@onready var _delete_button: BubbleButton = %DeleteButton @onready var _cancel_button: BubbleButton = %CancelButton @onready var _back_button: BubbleButton = %BackButton @onready var _open_close_button: BubbleButton = %OpenCloseButton @onready var _status: Label = %Status @onready var _session_summary: Label = %SessionSummary -@onready var _delete_confirmation: BubbleConfirmationPage = ( - %DeleteConfirmation -) var _network_session: NetworkSession var _saved_servers: SavedServerStore var _gameplay_context: bool = false -var _mode: Mode = Mode.DIRECT -var _visible_entries: Array[SavedServerEntry] = [] -var _selected_entry: SavedServerEntry -var _editing_entry_id: String = "" -var _name_entry_active: bool = false -var _delete_armed: bool = false func _ready() -> void: - _direct_button.pressed.connect(_set_mode.bind(Mode.DIRECT)) - _saved_button.pressed.connect(_set_mode.bind(Mode.SAVED)) - _recent_button.pressed.connect(_set_mode.bind(Mode.RECENT)) - _join_button.pressed.connect(_request_join) - _save_button.pressed.connect(_on_save_pressed) - _edit_button.pressed.connect(_on_edit_pressed) - _favorite_button.pressed.connect(_on_favorite_pressed) - _delete_button.pressed.connect(_on_delete_pressed) + _join_button.pressed.connect(_on_join_pressed) _cancel_button.pressed.connect(_on_cancel_pressed) _back_button.pressed.connect(_on_back_pressed) _open_close_button.pressed.connect(_on_open_close_pressed) - _address.text_submitted.connect(func(_value: String) -> void: - if _name_entry_active: - _name_edit.grab_focus() - _name_edit.select_all() - else: - _request_join() - ) - _name_edit.text_submitted.connect(func(_value: String) -> void: - _commit_name_entry() - ) - _server_list.item_selected.connect(_on_list_item_selected) - _server_list.item_activated.connect(func(_index: int) -> void: - _request_join() - ) - _delete_confirmation.confirmed.connect(_confirm_delete) - _delete_confirmation.cancelled.connect(_cancel_delete) + _address.text_submitted.connect(_on_address_submitted) hide() @@ -109,11 +50,6 @@ func setup( _on_peer_count_changed ): _network_session.peer_count_changed.connect(_on_peer_count_changed) - if ( - _saved_servers != null - and not _saved_servers.data_changed.is_connected(_on_store_changed) - ): - _saved_servers.data_changed.connect(_on_store_changed) _refresh() @@ -123,14 +59,12 @@ func open_page(preserved_endpoint: String = "") -> void: elif _address.text.is_empty(): _address.text = "127.0.0.1:7777" show() - _set_mode(_mode) - if _mode == Mode.DIRECT: - _address.grab_focus() - _address.select_all() + _refresh() + _address.grab_focus() + _address.select_all() func close_page() -> void: - _clear_edit_state() hide() get_viewport().gui_release_focus() @@ -140,21 +74,15 @@ func get_endpoint_text() -> String: func set_status(message: String) -> void: - _set_status(_friendly_connection_message(message), true) + _status.text = message -func _set_mode(mode: Mode) -> void: - _mode = mode - _selected_entry = null - _clear_edit_state() - _refresh_entries() - _refresh() - if not is_visible_in_tree(): - return - if mode == Mode.DIRECT: - _address.grab_focus() - else: - _server_list.grab_focus() +func _on_join_pressed() -> void: + _request_join() + + +func _on_address_submitted(_value: String) -> void: + _request_join() func _request_join() -> void: @@ -163,376 +91,14 @@ func _request_join() -> void: NetworkSession.State.SERVER_LOST, ]: _network_session.reset_failure() - var endpoint_text: String = ( - _selected_entry.normalized_endpoint - if _mode != Mode.DIRECT and _selected_entry != null - else _address.text - ) - var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text) + var endpoint: ConnectionEndpoint = EndpointParser.parse(_address.text) if not endpoint.is_valid(): - _set_status(endpoint.error_message, true) + _status.text = endpoint.error_message return - _set_status("Connecting…") _address.text = endpoint.normalized_display join_requested.emit(endpoint.normalized_display) -func _on_save_pressed() -> void: - if _name_entry_active: - _commit_name_entry() - return - var endpoint: ConnectionEndpoint = ( - _selected_entry.get_endpoint() - if _mode == Mode.RECENT and _selected_entry != null - else EndpointParser.parse(_address.text) - ) - if not endpoint.is_valid(): - _set_status(endpoint.error_message, true) - return - var existing: SavedServerEntry = ( - _saved_servers.find_saved_by_endpoint(endpoint) - ) - if existing != null: - _set_status( - "Already saved locally as “%s”." % existing.display_name, - true, - ) - _set_mode(Mode.SAVED) - _select_entry_id(existing.entry_id) - return - _address.text = endpoint.normalized_display - _name_edit.text = ( - _selected_entry.last_observed_server_name - if _mode == Mode.RECENT - and _selected_entry != null - and not _selected_entry.last_observed_server_name.is_empty() - else endpoint.host - ) - _name_entry_active = true - _name_edit.show() - _save_button.text = "save" - _name_edit.grab_focus() - _name_edit.select_all() - _refresh() - - -func _commit_name_entry() -> void: - var endpoint := EndpointParser.parse(_address.text) - if not endpoint.is_valid(): - _set_status(endpoint.error_message, true) - return - var saved: SavedServerEntry = _saved_servers.save_or_update_entry( - _name_edit.text, - endpoint, - _editing_entry_id, - ) - if saved == null: - _set_status( - "Could not save. Check the server name and address.", - true, - ) - return - var was_editing: bool = not _editing_entry_id.is_empty() - _set_status( - "Server updated." - if was_editing - else "Saved locally as “%s”." % saved.display_name - ) - _clear_edit_state() - _mode = Mode.SAVED - _refresh_entries() - _select_entry_id(saved.entry_id) - _refresh() - - -func _on_edit_pressed() -> void: - if _selected_entry == null or _mode != Mode.SAVED: - return - _editing_entry_id = _selected_entry.entry_id - _address.text = _selected_entry.normalized_endpoint - _name_edit.text = _selected_entry.display_name - _name_entry_active = true - _name_edit.show() - _save_button.text = "save" - _name_edit.grab_focus() - _name_edit.select_all() - _refresh() - - -func _on_favorite_pressed() -> void: - if _selected_entry == null or _mode != Mode.SAVED: - return - var entry_id: String = _selected_entry.entry_id - _saved_servers.set_favorite(entry_id, not _selected_entry.favorite) - _refresh_entries() - _select_entry_id(entry_id) - _refresh() - - -func _on_delete_pressed() -> void: - if _selected_entry == null or _mode == Mode.DIRECT: - return - _delete_armed = true - _delete_confirmation.configure( - ( - "Remove this recent connection from this device?" - if _mode == Mode.RECENT - else "Delete this saved-server bookmark from this device?" - ), - "remove" if _mode == Mode.RECENT else "delete", - "cancel", - BubbleConfirmationPage.InitialFocus.CANCEL, - ) - _delete_confirmation.transition_in(0.18, func() -> void: pass) - - -func _confirm_delete() -> void: - if _selected_entry == null or not _delete_armed: - _cancel_delete() - return - var removed: bool = ( - _saved_servers.remove_recent_entry(_selected_entry.entry_id) - if _mode == Mode.RECENT - else _saved_servers.remove_saved_entry(_selected_entry.entry_id) - ) - _set_status( - ( - "Removed from recent connections." - if _mode == Mode.RECENT - else "Removed from saved servers." - ) - if removed - else "Could not remove the local entry.", - not removed, - ) - _selected_entry = null - _delete_armed = false - _refresh_entries() - _refresh() - _delete_confirmation.hide_page() - - -func _cancel_delete() -> void: - _delete_armed = false - _delete_confirmation.hide_page() - _delete_button.grab_focus() - - -func _on_list_item_selected(index: int) -> void: - if index < 0 or index >= _visible_entries.size(): - return - _selected_entry = _visible_entries[index] - _delete_armed = false - _refresh() - - -func _select_entry_id(entry_id: String) -> void: - for index: int in range(_visible_entries.size()): - if _visible_entries[index].entry_id == entry_id: - _server_list.select(index) - _selected_entry = _visible_entries[index] - return - - -func _refresh_entries() -> void: - _visible_entries.clear() - _server_list.clear() - if _saved_servers == null or _mode == Mode.DIRECT: - return - _visible_entries = ( - _saved_servers.get_saved_entries() - if _mode == Mode.SAVED - else _saved_servers.get_recent_entries() - ) - for entry: SavedServerEntry in _visible_entries: - var prefix: String = "★ " if entry.favorite else "" - var count: String = ( - " — %d / %d players" - % [ - entry.last_observed_player_count, - entry.last_observed_max_players, - ] - if entry.last_observed_max_players > 0 - else "" - ) - _server_list.add_item( - "%s%s — %s%s" - % [ - prefix, - entry.display_name, - entry.normalized_endpoint, - count, - ] - ) - - -func _refresh() -> void: - if not is_node_ready() or _network_session == null: - return - var connecting: bool = _network_session.state in [ - NetworkSession.State.CONNECTING, - NetworkSession.State.AUTHENTICATING, - ] - var direct: bool = _mode == Mode.DIRECT - var selected: bool = _selected_entry != null - _address.visible = direct or _name_entry_active - _address_label.visible = _address.visible - _address_helper.visible = _address.visible - _address_helper.text = ( - ADDRESS_FORMAT_HELP if _name_entry_active else DIRECT_WORKFLOW_HELP - ) - _address.editable = not connecting - _name_edit.visible = _name_entry_active - _name_label.visible = _name_entry_active - _name_helper.visible = _name_entry_active - _server_list.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.text = "join\nnow" if direct else "join" - _save_button.visible = direct or _mode == Mode.RECENT or _name_entry_active - _save_button.disabled = connecting or ( - _mode == Mode.RECENT and not selected and not _name_entry_active - ) - _save_button.text = "save" if _name_entry_active else "save\nserver" - _edit_button.visible = _mode == Mode.SAVED and selected - _favorite_button.visible = _mode == Mode.SAVED and selected - _favorite_button.text = ( - "unfavorite" - if selected and _selected_entry.favorite - else "favorite" - ) - _delete_button.visible = not direct and selected - _delete_button.text = "remove" if _mode == Mode.RECENT else "delete" - _cancel_button.visible = connecting - _open_close_button.visible = ( - _gameplay_context and _network_session.is_host() - ) - if _open_close_button.visible: - _open_close_button.text = ( - "close\ngame" - if _network_session.is_open_host() - else "open\ngame" - ) - _session_summary.visible = ( - _gameplay_context - and _network_session.state != NetworkSession.State.INACTIVE - ) - if _session_summary.visible: - _session_summary.text = "%d / %d players" % [ - _network_session.get_player_count(), - _network_session.get_session_max_players(), - ] - if not direct: - if selected: - _details.text = _format_entry_details(_selected_entry) - elif _visible_entries.is_empty(): - _details.text = ( - "No saved servers yet." - if _mode == Mode.SAVED - else "No recent connections yet." - ) - else: - _details.text = "Select a server." - var warning: String = ( - _saved_servers.get_recovery_warning() - if _saved_servers != null - else "" - ) - if not warning.is_empty() and _status.text.is_empty(): - _status.text = warning - - -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) - if ( - not entry.last_observed_server_name.is_empty() - and ( - _mode != Mode.SAVED - or entry.last_observed_server_name != entry.display_name - ) - ): - parts.append("Server name: %s" % entry.last_observed_server_name) - if entry.last_observed_max_players > 0: - parts.append( - "Players: %d / %d" - % [ - entry.last_observed_player_count, - entry.last_observed_max_players, - ] - ) - if entry.last_success_at_unix > 0: - parts.append( - "Last connected: %s" - % Time.get_datetime_string_from_unix_time( - entry.last_success_at_unix, true - ) - ) - if not entry.last_result_code.is_empty(): - parts.append( - "Last result: %s" % _format_result_code(entry.last_result_code) - ) - return "\n".join(parts) - - -func _format_result_code(result_code: String) -> String: - match result_code.strip_edges().to_upper(): - "SUCCESS": - return "Connected successfully" - "PROTOCOL_MISMATCH": - return "Protocol versions did not match" - "SERVER_FULL": - return "Server was full" - "CANCELLED": - return "Connection cancelled" - "TIMEOUT", "CONNECTION_FAILED", "SERVER_UNAVAILABLE": - return "Could not reach the server" - _: - return "Connection was unsuccessful" - - -func _friendly_connection_message(message: String) -> String: - var normalized: String = message.strip_edges().to_lower() - if "protocol" in normalized and ( - "mismatch" in normalized or "version" in normalized - ): - return "Protocol versions do not match." - if "full" in normalized or "capacity" in normalized: - return "Server is full." - if "cancel" in normalized: - return "Connection cancelled." - if ( - "timeout" in normalized - or "unavailable" in normalized - or "refused" in normalized - or "reach" in normalized - or "failed" in normalized - ): - return "Could not reach the server." - return message - - -func _set_status(message: String, is_error: bool = false) -> void: - _status.text = message - _status.add_theme_color_override( - "font_color", - Color(0.45, 0.09, 0.08, 1.0) - if is_error - else Color(0.035, 0.145, 0.22, 1.0), - ) - - -func _clear_edit_state() -> void: - _editing_entry_id = "" - _name_entry_active = false - _delete_armed = false - if is_node_ready(): - _name_edit.hide() - _delete_confirmation.hide_page() - - func _on_cancel_pressed() -> void: if _network_session != null: _network_session.cancel_connection() @@ -540,10 +106,6 @@ func _on_cancel_pressed() -> void: func _on_back_pressed() -> void: - if _name_entry_active or _delete_armed: - _clear_edit_state() - _refresh() - return if ( _network_session != null and _network_session.state in [ @@ -567,12 +129,12 @@ func _on_state_changed(_state: NetworkSession.State) -> void: func _on_status_message_changed(message: String) -> void: - _set_status(_friendly_connection_message(message)) + _status.text = message _refresh() func _on_connection_error(message: String) -> void: - _set_status(_friendly_connection_message(message), true) + _status.text = message _refresh() @@ -583,11 +145,31 @@ func _on_peer_count_changed(player_count: int, max_players: int) -> void: ] -func _on_store_changed() -> void: - var selected_id: String = ( - _selected_entry.entry_id if _selected_entry != null else "" +func _refresh() -> void: + if not is_node_ready() or _network_session == null: + return + var connecting: bool = _network_session.state in [ + NetworkSession.State.CONNECTING, + NetworkSession.State.AUTHENTICATING, + ] + _address.editable = not connecting + _join_button.disabled = connecting + _cancel_button.visible = connecting + _open_close_button.visible = ( + _gameplay_context and _network_session.is_host() ) - _refresh_entries() - if not selected_id.is_empty(): - _select_entry_id(selected_id) - _refresh() + if _open_close_button.visible: + _open_close_button.text = ( + "close\ngame" + if _network_session.is_open_host() + else "open\ngame" + ) + _session_summary.visible = ( + _gameplay_context + and _network_session.state != NetworkSession.State.INACTIVE + ) + if _session_summary.visible: + _session_summary.text = "%d / %d players" % [ + _network_session.get_player_count(), + _network_session.get_session_max_players(), + ] diff --git a/ui/network/join_game_page.tscn b/ui/network/join_game_page.tscn index 5c09840..75e7e8f 100644 --- a/ui/network/join_game_page.tscn +++ b/ui/network/join_game_page.tscn @@ -1,9 +1,8 @@ -[gd_scene load_steps=13 format=3] +[gd_scene load_steps=5 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="PackedScene" path="res://ui/components/bubble_menu/bubble_button.tscn" id="3_bubble"] -[ext_resource type="PackedScene" path="res://ui/components/bubble_menu/bubble_confirmation_page.tscn" id="4_confirmation"] [sub_resource type="StyleBoxFlat" id="StyleBox_paper"] bg_color = Color(0.93, 0.885, 0.73, 1) @@ -15,76 +14,6 @@ corner_radius_top_right = 46 corner_radius_bottom_right = 58 corner_radius_bottom_left = 48 -[sub_resource type="StyleBoxFlat" id="StyleBox_list"] -bg_color = Color(0.89, 0.835, 0.67, 1) -border_width_left = 2 -border_width_top = 2 -border_width_right = 2 -border_width_bottom = 2 -border_color = Color(0.035, 0.145, 0.22, 0.36) -corner_radius_top_left = 12 -corner_radius_top_right = 9 -corner_radius_bottom_right = 13 -corner_radius_bottom_left = 10 - -[sub_resource type="StyleBoxFlat" id="StyleBox_selected"] -bg_color = Color(0.08, 0.24, 0.31, 0.92) -corner_radius_top_left = 8 -corner_radius_top_right = 7 -corner_radius_bottom_right = 9 -corner_radius_bottom_left = 6 - -[sub_resource type="StyleBoxEmpty" id="StyleBox_focus"] - -[sub_resource type="StyleBoxFlat" id="StyleBox_input"] -content_margin_left = 12.0 -content_margin_top = 7.0 -content_margin_right = 12.0 -content_margin_bottom = 7.0 -bg_color = Color(0.035, 0.145, 0.22, 1) -corner_radius_top_left = 8 -corner_radius_top_right = 7 -corner_radius_bottom_right = 9 -corner_radius_bottom_left = 6 - -[sub_resource type="StyleBoxFlat" id="StyleBox_input_focus"] -content_margin_left = 12.0 -content_margin_top = 7.0 -content_margin_right = 12.0 -content_margin_bottom = 7.0 -bg_color = Color(0.035, 0.145, 0.22, 1) -border_width_left = 2 -border_width_top = 2 -border_width_right = 2 -border_width_bottom = 2 -border_color = Color(0.93, 0.885, 0.73, 0.9) -corner_radius_top_left = 8 -corner_radius_top_right = 7 -corner_radius_bottom_right = 9 -corner_radius_bottom_left = 6 - -[sub_resource type="StyleBoxFlat" id="StyleBox_input_read_only"] -content_margin_left = 12.0 -content_margin_top = 7.0 -content_margin_right = 12.0 -content_margin_bottom = 7.0 -bg_color = Color(0.035, 0.145, 0.22, 0.82) -corner_radius_top_left = 8 -corner_radius_top_right = 7 -corner_radius_bottom_right = 9 -corner_radius_bottom_left = 6 - -[sub_resource type="StyleBoxFlat" id="StyleBox_status"] -content_margin_left = 10.0 -content_margin_top = 4.0 -content_margin_right = 10.0 -content_margin_bottom = 4.0 -bg_color = Color(0.89, 0.835, 0.67, 0.72) -corner_radius_top_left = 7 -corner_radius_top_right = 6 -corner_radius_bottom_right = 8 -corner_radius_bottom_left = 5 - [node name="JoinGamePage" type="Control"] visible = false layout_mode = 3 @@ -98,179 +27,54 @@ script = ExtResource("1_script") [node name="Paper" type="PanelContainer" parent="."] layout_mode = 0 -offset_left = 230.0 -offset_top = 64.0 -offset_right = 1050.0 -offset_bottom = 656.0 +offset_left = 340.0 +offset_top = 132.0 +offset_right = 940.0 +offset_bottom = 570.0 theme_override_styles/panel = SubResource("StyleBox_paper") [node name="Margin" type="MarginContainer" parent="Paper"] layout_mode = 2 -theme_override_constants/margin_left = 54 -theme_override_constants/margin_top = 30 -theme_override_constants/margin_right = 54 -theme_override_constants/margin_bottom = 30 +theme_override_constants/margin_left = 70 +theme_override_constants/margin_top = 52 +theme_override_constants/margin_right = 70 +theme_override_constants/margin_bottom = 52 [node name="Layout" type="VBoxContainer" parent="Paper/Margin"] layout_mode = 2 -theme_override_constants/separation = 10 +theme_override_constants/separation = 18 alignment = 1 [node name="Title" type="Label" parent="Paper/Margin/Layout"] layout_mode = 2 theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_font_sizes/font_size = 30 +theme_override_font_sizes/font_size = 34 text = "join game" horizontal_alignment = 1 -[node name="Modes" type="HBoxContainer" parent="Paper/Margin/Layout"] -layout_mode = 2 -theme_override_constants/separation = 14 -alignment = 1 - -[node name="DirectButton" parent="Paper/Margin/Layout/Modes" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -custom_minimum_size = Vector2(100, 72) -layout_mode = 2 -text = "direct" -neutral_size = Vector2(100, 72) -minimum_font_size = 16 -maximum_font_size = 19 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="SavedButton" parent="Paper/Margin/Layout/Modes" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -custom_minimum_size = Vector2(100, 72) -layout_mode = 2 -text = "saved" -neutral_size = Vector2(100, 72) -minimum_font_size = 16 -maximum_font_size = 19 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="RecentButton" parent="Paper/Margin/Layout/Modes" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -custom_minimum_size = Vector2(100, 72) -layout_mode = 2 -text = "recent" -neutral_size = Vector2(100, 72) -minimum_font_size = 16 -maximum_font_size = 19 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="AddressLabel" type="Label" parent="Paper/Margin/Layout"] -unique_name_in_owner = true +[node name="Hint" type="Label" parent="Paper/Margin/Layout"] layout_mode = 2 theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) theme_override_font_sizes/font_size = 17 -text = "server address" +text = "hostname or IP address • optional port" horizontal_alignment = 1 [node name="Address" type="LineEdit" parent="Paper/Margin/Layout"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 42) +custom_minimum_size = Vector2(0, 52) layout_mode = 2 -theme_override_colors/font_color = Color(0.96, 0.93, 0.82, 1) -theme_override_colors/font_uneditable_color = Color(0.8, 0.77, 0.68, 0.9) -theme_override_colors/font_selected_color = Color(1, 0.98, 0.9, 1) -theme_override_colors/font_placeholder_color = Color(0.78, 0.75, 0.66, 0.86) -theme_override_colors/caret_color = Color(1, 0.96, 0.8, 1) -theme_override_colors/selection_color = Color(0.2, 0.48, 0.59, 0.9) -theme_override_font_sizes/font_size = 19 -theme_override_styles/normal = SubResource("StyleBox_input") -theme_override_styles/focus = SubResource("StyleBox_input_focus") -theme_override_styles/read_only = SubResource("StyleBox_input_read_only") -placeholder_text = "example.net or 192.168.1.50:7777" +theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) +theme_override_font_sizes/font_size = 22 +placeholder_text = "example.net:7777" alignment = 1 max_length = 300 -[node name="AddressHelper" type="Label" parent="Paper/Margin/Layout"] -unique_name_in_owner = true -layout_mode = 2 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 0.78) -theme_override_font_sizes/font_size = 13 -text = "Hostname, IPv4, or [IPv6]. Port 7777 is used when omitted.\nJoin Now connects once; Save Server stores this address locally." -horizontal_alignment = 1 -autowrap_mode = 2 - -[node name="NameLabel" type="Label" parent="Paper/Margin/Layout"] -unique_name_in_owner = true -visible = false -layout_mode = 2 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_font_sizes/font_size = 17 -text = "server name" -horizontal_alignment = 1 - -[node name="NameEdit" type="LineEdit" parent="Paper/Margin/Layout"] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(0, 42) -layout_mode = 2 -theme_override_colors/font_color = Color(0.96, 0.93, 0.82, 1) -theme_override_colors/font_uneditable_color = Color(0.8, 0.77, 0.68, 0.9) -theme_override_colors/font_selected_color = Color(1, 0.98, 0.9, 1) -theme_override_colors/font_placeholder_color = Color(0.78, 0.75, 0.66, 0.86) -theme_override_colors/caret_color = Color(1, 0.96, 0.8, 1) -theme_override_colors/selection_color = Color(0.2, 0.48, 0.59, 0.9) -theme_override_font_sizes/font_size = 19 -theme_override_styles/normal = SubResource("StyleBox_input") -theme_override_styles/focus = SubResource("StyleBox_input_focus") -theme_override_styles/read_only = SubResource("StyleBox_input_read_only") -placeholder_text = "Friend's server" -alignment = 1 -max_length = 80 - -[node name="NameHelper" type="Label" parent="Paper/Margin/Layout"] -unique_name_in_owner = true -visible = false -layout_mode = 2 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 0.78) -theme_override_font_sizes/font_size = 13 -text = "Only visible on this device." -horizontal_alignment = 1 - -[node name="ServerList" type="ItemList" parent="Paper/Margin/Layout"] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(0, 145) -layout_mode = 2 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_colors/font_selected_color = Color(0.93, 0.885, 0.73, 1) -theme_override_colors/guide_color = Color(0.035, 0.145, 0.22, 0.25) -theme_override_font_sizes/font_size = 17 -theme_override_styles/panel = SubResource("StyleBox_list") -theme_override_styles/focus = SubResource("StyleBox_focus") -theme_override_styles/selected = SubResource("StyleBox_selected") -theme_override_styles/selected_focus = SubResource("StyleBox_selected") -allow_reselect = true -same_column_width = true - -[node name="Details" type="Label" parent="Paper/Margin/Layout"] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(0, 92) -layout_mode = 2 -theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_font_sizes/font_size = 16 -text = "Select a server." -horizontal_alignment = 1 -vertical_alignment = 1 -autowrap_mode = 2 - [node name="Status" type="Label" parent="Paper/Margin/Layout"] unique_name_in_owner = true -custom_minimum_size = Vector2(0, 38) +custom_minimum_size = Vector2(0, 42) layout_mode = 2 theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_font_sizes/font_size = 15 -theme_override_styles/normal = SubResource("StyleBox_status") +theme_override_font_sizes/font_size = 17 text = "Direct UDP connection • default port 7777" horizontal_alignment = 1 vertical_alignment = 1 @@ -281,74 +85,23 @@ unique_name_in_owner = true visible = false layout_mode = 2 theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1) -theme_override_font_sizes/font_size = 15 +theme_override_font_sizes/font_size = 17 text = "1 / 8 players" horizontal_alignment = 1 [node name="Actions" type="HBoxContainer" parent="Paper/Margin/Layout"] layout_mode = 2 -theme_override_constants/separation = 7 +theme_override_constants/separation = 12 alignment = 1 [node name="JoinButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(82, 72) +custom_minimum_size = Vector2(112, 106) layout_mode = 2 text = "join" -neutral_size = Vector2(82, 72) -minimum_font_size = 15 -maximum_font_size = 18 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="SaveButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -custom_minimum_size = Vector2(82, 72) -layout_mode = 2 -text = "save\nserver" -neutral_size = Vector2(82, 72) -minimum_font_size = 14 -maximum_font_size = 17 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="EditButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(76, 68) -layout_mode = 2 -text = "edit" -neutral_size = Vector2(76, 68) -minimum_font_size = 14 -maximum_font_size = 17 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="FavoriteButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(88, 68) -layout_mode = 2 -text = "favorite" -neutral_size = Vector2(88, 68) -minimum_font_size = 13 -maximum_font_size = 16 -horizontal_amplitude = 0.0 -vertical_amplitude = 0.0 -deformation_amplitude = 0.0 - -[node name="DeleteButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] -unique_name_in_owner = true -visible = false -custom_minimum_size = Vector2(82, 68) -layout_mode = 2 -text = "delete" -neutral_size = Vector2(82, 68) -minimum_font_size = 13 -maximum_font_size = 16 +neutral_size = Vector2(112, 106) +minimum_font_size = 18 +maximum_font_size = 24 horizontal_amplitude = 0.0 vertical_amplitude = 0.0 deformation_amplitude = 0.0 @@ -356,12 +109,12 @@ deformation_amplitude = 0.0 [node name="OpenCloseButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] unique_name_in_owner = true visible = false -custom_minimum_size = Vector2(88, 72) +custom_minimum_size = Vector2(112, 106) layout_mode = 2 text = "open\ngame" -neutral_size = Vector2(88, 72) -minimum_font_size = 14 -maximum_font_size = 17 +neutral_size = Vector2(112, 106) +minimum_font_size = 17 +maximum_font_size = 22 horizontal_amplitude = 0.0 vertical_amplitude = 0.0 deformation_amplitude = 0.0 @@ -369,27 +122,24 @@ deformation_amplitude = 0.0 [node name="CancelButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] unique_name_in_owner = true visible = false -custom_minimum_size = Vector2(76, 68) +custom_minimum_size = Vector2(98, 94) layout_mode = 2 text = "cancel" -neutral_size = Vector2(76, 68) -minimum_font_size = 14 -maximum_font_size = 17 +neutral_size = Vector2(98, 94) +minimum_font_size = 16 +maximum_font_size = 20 horizontal_amplitude = 0.0 vertical_amplitude = 0.0 deformation_amplitude = 0.0 [node name="BackButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")] unique_name_in_owner = true -custom_minimum_size = Vector2(76, 68) +custom_minimum_size = Vector2(98, 94) layout_mode = 2 text = "back" -neutral_size = Vector2(76, 68) -minimum_font_size = 14 -maximum_font_size = 17 +neutral_size = Vector2(98, 94) +minimum_font_size = 16 +maximum_font_size = 20 horizontal_amplitude = 0.0 vertical_amplitude = 0.0 deformation_amplitude = 0.0 - -[node name="DeleteConfirmation" parent="." instance=ExtResource("4_confirmation")] -unique_name_in_owner = true