Compare commits
No commits in common. "b0e2e84ba4ffdd4cbb99f3b03540dbc1520a35f8" and "918e75802ae59ff9c264fab535250432e862abf9" have entirely different histories.
b0e2e84ba4
...
918e75802a
8 changed files with 164 additions and 1246 deletions
Binary file not shown.
35
main/main.gd
35
main/main.gd
|
|
@ -455,20 +455,6 @@ func _on_pause_join_game_requested(endpoint: String) -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_network_join_authenticated() -> 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:
|
if _join_requested_from_title:
|
||||||
var inspection = _save_manager.inspect_save()
|
var inspection = _save_manager.inspect_save()
|
||||||
var progression_ready: bool = (
|
var progression_ready: bool = (
|
||||||
|
|
@ -496,33 +482,12 @@ func _on_network_join_authenticated() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_network_connection_error(message: String) -> 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:
|
if _join_requested_from_title:
|
||||||
_game_ui.get_title_screen().report_network_error(message)
|
_game_ui.get_title_screen().report_network_error(message)
|
||||||
elif _join_requested_from_pause:
|
elif _join_requested_from_pause:
|
||||||
_handle_failed_session_switch(message)
|
_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:
|
func _handle_failed_session_switch(message: String) -> void:
|
||||||
_join_requested_from_pause = false
|
_join_requested_from_pause = false
|
||||||
_set_gameplay_active(false)
|
_set_gameplay_active(false)
|
||||||
|
|
|
||||||
|
|
@ -56,9 +56,6 @@ var _input_sequence: int = 0
|
||||||
var _input_accumulator: float = 0.0
|
var _input_accumulator: float = 0.0
|
||||||
var _snapshot_accumulator: float = 0.0
|
var _snapshot_accumulator: float = 0.0
|
||||||
var _last_server_max_players: int = DEFAULT_SESSION_MAX_PLAYERS
|
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
|
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:
|
func can_use_host_gameplay() -> bool:
|
||||||
return is_host()
|
return is_host()
|
||||||
|
|
||||||
|
|
@ -478,11 +457,6 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
"max_players",
|
"max_players",
|
||||||
DEFAULT_SESSION_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()
|
var local_peer_id: int = multiplayer.get_unique_id()
|
||||||
_registry.clear()
|
_registry.clear()
|
||||||
_registry.add_peer(
|
_registry.add_peer(
|
||||||
|
|
@ -501,6 +475,11 @@ func receive_server_hello(data: Dictionary) -> void:
|
||||||
_last_server_max_players,
|
_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()
|
join_authenticated.emit()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
uid://b3guw5fet7lv0
|
|
||||||
|
|
@ -5,235 +5,86 @@ const FORMAT_VERSION: int = 1
|
||||||
const STORE_PATH: String = "user://saved_servers.json"
|
const STORE_PATH: String = "user://saved_servers.json"
|
||||||
const TEMP_PATH: String = "user://saved_servers.json.tmp"
|
const TEMP_PATH: String = "user://saved_servers.json.tmp"
|
||||||
const BACKUP_PATH: String = "user://saved_servers.json.backup"
|
const BACKUP_PATH: String = "user://saved_servers.json.backup"
|
||||||
const MAX_SAVED_ENTRIES: int = 100
|
const MAX_ENTRIES: int = 100
|
||||||
const MAX_RECENT_ENTRIES: int = 20
|
|
||||||
const MAX_DISPLAY_NAME_LENGTH: int = 80
|
|
||||||
|
|
||||||
signal data_changed
|
var _entries: Array[Dictionary] = []
|
||||||
signal recovery_warning_changed(message: String)
|
|
||||||
|
|
||||||
var _saved_entries: Array[Dictionary] = []
|
|
||||||
var _recent_entries: Array[Dictionary] = []
|
|
||||||
var _loaded: bool = false
|
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]:
|
func list_entries() -> Array[Dictionary]:
|
||||||
_ensure_loaded()
|
_ensure_loaded()
|
||||||
return _saved_entries.duplicate(true)
|
return _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
|
|
||||||
|
|
||||||
|
|
||||||
func find_by_normalized_endpoint(value: String) -> Dictionary:
|
func find_by_normalized_endpoint(value: String) -> Dictionary:
|
||||||
var endpoint := EndpointParser.parse(value)
|
_ensure_loaded()
|
||||||
var entry: SavedServerEntry = find_saved_by_endpoint(endpoint)
|
for entry: Dictionary in _entries:
|
||||||
return entry.to_dictionary() if entry != null else {}
|
if entry.get("normalized_endpoint", "") == value:
|
||||||
|
return entry.duplicate(true)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
func save_or_update_entry(
|
func save_entry(
|
||||||
display_name: String,
|
display_name: String,
|
||||||
endpoint: ConnectionEndpoint,
|
endpoint: ConnectionEndpoint,
|
||||||
entry_id: String = "",
|
) -> bool:
|
||||||
) -> SavedServerEntry:
|
|
||||||
_ensure_loaded()
|
_ensure_loaded()
|
||||||
var clean_name: String = display_name.strip_edges()
|
|
||||||
if (
|
if (
|
||||||
endpoint == null
|
endpoint == null
|
||||||
or not endpoint.is_valid()
|
or not endpoint.is_valid()
|
||||||
or clean_name.is_empty()
|
or display_name.strip_edges().is_empty()
|
||||||
or clean_name.length() > MAX_DISPLAY_NAME_LENGTH
|
or display_name.length() > 80
|
||||||
):
|
):
|
||||||
return null
|
return false
|
||||||
var now: int = int(Time.get_unix_time_from_system())
|
var now: int = int(Time.get_unix_time_from_system())
|
||||||
var target: Dictionary = {}
|
for entry: Dictionary in _entries:
|
||||||
for data: Dictionary in _saved_entries:
|
if entry.get("normalized_endpoint", "") == endpoint.normalized_display:
|
||||||
if not entry_id.is_empty() and data.get("entry_id", "") == entry_id:
|
entry["display_name"] = display_name.strip_edges()
|
||||||
target = data
|
entry["host"] = endpoint.host
|
||||||
break
|
entry["port"] = endpoint.port
|
||||||
for data: Dictionary in _saved_entries:
|
entry["updated_at_unix"] = now
|
||||||
if (
|
return _save_atomic()
|
||||||
data.get("normalized_endpoint", "") == endpoint.normalized_display
|
if _entries.size() >= MAX_ENTRIES:
|
||||||
and data != target
|
return false
|
||||||
):
|
_entries.append({
|
||||||
return null
|
"entry_id": Crypto.new().generate_random_bytes(16).hex_encode(),
|
||||||
if target.is_empty():
|
"display_name": display_name.strip_edges(),
|
||||||
if _saved_entries.size() >= MAX_SAVED_ENTRIES:
|
"route_kind": "DIRECT",
|
||||||
return null
|
"host": endpoint.host,
|
||||||
target = _make_entry(endpoint, SavedServerEntry.Kind.SAVED)
|
"port": endpoint.port,
|
||||||
_saved_entries.append(target)
|
"normalized_endpoint": endpoint.normalized_display,
|
||||||
target["display_name"] = clean_name
|
"favorite": false,
|
||||||
target["host"] = endpoint.host
|
"created_at_unix": now,
|
||||||
target["normalized_host"] = endpoint.host
|
"updated_at_unix": now,
|
||||||
target["port"] = endpoint.port
|
"last_success_at_unix": 0,
|
||||||
target["normalized_endpoint"] = endpoint.normalized_display
|
"last_observed_max_players": 0,
|
||||||
target["updated_at_unix"] = now
|
})
|
||||||
if not _save_atomic():
|
return _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
|
|
||||||
|
|
||||||
|
|
||||||
func remove_entry(entry_id: String) -> bool:
|
func remove_entry(entry_id: String) -> bool:
|
||||||
return remove_saved_entry(entry_id)
|
|
||||||
|
|
||||||
|
|
||||||
func set_favorite(entry_id: String, favorite: bool) -> bool:
|
|
||||||
_ensure_loaded()
|
_ensure_loaded()
|
||||||
for data: Dictionary in _saved_entries:
|
for index: int in range(_entries.size()):
|
||||||
if data.get("entry_id", "") == entry_id:
|
if _entries[index].get("entry_id", "") == entry_id:
|
||||||
data["favorite"] = favorite
|
_entries.remove_at(index)
|
||||||
data["updated_at_unix"] = int(Time.get_unix_time_from_system())
|
return _save_atomic()
|
||||||
if not _save_atomic():
|
|
||||||
return false
|
|
||||||
data_changed.emit()
|
|
||||||
return true
|
|
||||||
return false
|
return false
|
||||||
|
|
||||||
|
|
||||||
func record_successful_connection(
|
func record_successful_connection(
|
||||||
endpoint: ConnectionEndpoint,
|
endpoint: ConnectionEndpoint,
|
||||||
observed_max_players: int,
|
observed_max_players: int,
|
||||||
server_name: String = "",
|
|
||||||
protocol_version: int = 0,
|
|
||||||
player_count: int = 0,
|
|
||||||
) -> bool:
|
) -> bool:
|
||||||
_ensure_loaded()
|
_ensure_loaded()
|
||||||
if endpoint == null or not endpoint.is_valid():
|
if endpoint == null or not endpoint.is_valid():
|
||||||
return false
|
return false
|
||||||
var now: int = int(Time.get_unix_time_from_system())
|
for entry: Dictionary in _entries:
|
||||||
for data: Dictionary in _saved_entries:
|
if entry.get("normalized_endpoint", "") == endpoint.normalized_display:
|
||||||
if data.get("normalized_endpoint", "") == endpoint.normalized_display:
|
entry["last_success_at_unix"] = int(Time.get_unix_time_from_system())
|
||||||
_apply_result_metadata(
|
entry["last_observed_max_players"] = maxi(observed_max_players, 0)
|
||||||
data, now, "SUCCESS", server_name, protocol_version,
|
entry["updated_at_unix"] = int(Time.get_unix_time_from_system())
|
||||||
player_count, observed_max_players
|
return _save_atomic()
|
||||||
)
|
# Successful manual connections are deliberately not auto-saved.
|
||||||
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()
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -244,158 +95,54 @@ func _ensure_loaded() -> void:
|
||||||
_recover_interrupted_write()
|
_recover_interrupted_write()
|
||||||
if not FileAccess.file_exists(STORE_PATH):
|
if not FileAccess.file_exists(STORE_PATH):
|
||||||
return
|
return
|
||||||
var data: Dictionary = _read_store(STORE_PATH)
|
var file := FileAccess.open(STORE_PATH, FileAccess.READ)
|
||||||
if data.is_empty() and FileAccess.file_exists(BACKUP_PATH):
|
if file == null:
|
||||||
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."
|
|
||||||
)
|
|
||||||
return
|
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:
|
if data.get("format_version") != FORMAT_VERSION:
|
||||||
_write_blocked = true
|
|
||||||
_set_warning(
|
|
||||||
"Saved servers use a newer format. Direct connection is still available."
|
|
||||||
)
|
|
||||||
return
|
return
|
||||||
_load_collection(data.get("saved_entries", data.get("entries", [])), true)
|
var raw_entries: Variant = data.get("entries")
|
||||||
_load_collection(data.get("recent_entries", []), false)
|
if typeof(raw_entries) != TYPE_ARRAY:
|
||||||
|
|
||||||
|
|
||||||
func _load_collection(raw: Variant, is_saved: bool) -> void:
|
|
||||||
if typeof(raw) != TYPE_ARRAY:
|
|
||||||
_set_warning("Some saved-server data was unavailable.")
|
|
||||||
return
|
return
|
||||||
var target: Array[Dictionary] = (
|
|
||||||
_saved_entries if is_saved else _recent_entries
|
|
||||||
)
|
|
||||||
var seen: Dictionary[String, bool] = {}
|
var seen: Dictionary[String, bool] = {}
|
||||||
for value: Variant in raw:
|
for value: Variant in raw_entries:
|
||||||
if typeof(value) != TYPE_DICTIONARY:
|
if typeof(value) != TYPE_DICTIONARY:
|
||||||
_set_warning("Some malformed server entries were skipped.")
|
|
||||||
continue
|
continue
|
||||||
var validated: Dictionary = _validate_entry(value, is_saved)
|
var entry: Dictionary = value
|
||||||
if validated.is_empty():
|
var stored_host: String = str(entry.get("host", ""))
|
||||||
_set_warning("Some malformed server entries were skipped.")
|
var stored_port: int = int(entry.get("port", 0))
|
||||||
continue
|
var endpoint_text: String = (
|
||||||
var identity: String = validated["normalized_endpoint"]
|
"[%s]:%d" % [stored_host, stored_port]
|
||||||
if seen.has(identity):
|
if stored_host.contains(":")
|
||||||
continue
|
else "%s:%d" % [stored_host, stored_port]
|
||||||
seen[identity] = true
|
)
|
||||||
target.append(validated)
|
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
|
||||||
if target.size() >= (
|
if (
|
||||||
MAX_SAVED_ENTRIES if is_saved else MAX_RECENT_ENTRIES
|
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
|
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:
|
func _save_atomic() -> bool:
|
||||||
if _write_blocked:
|
|
||||||
return false
|
|
||||||
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
||||||
if file == null:
|
if file == null:
|
||||||
return false
|
return false
|
||||||
file.store_string(JSON.stringify({
|
file.store_string(JSON.stringify({
|
||||||
"format_version": FORMAT_VERSION,
|
"format_version": FORMAT_VERSION,
|
||||||
"saved_entries": _saved_entries,
|
"entries": _entries,
|
||||||
"recent_entries": _recent_entries,
|
|
||||||
}, "\t"))
|
}, "\t"))
|
||||||
file.flush()
|
file.flush()
|
||||||
var error: Error = file.get_error()
|
var error: Error = file.get_error()
|
||||||
|
|
@ -416,32 +163,16 @@ func _save_atomic() -> bool:
|
||||||
return true
|
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:
|
func _recover_interrupted_write() -> void:
|
||||||
if FileAccess.file_exists(STORE_PATH):
|
if FileAccess.file_exists(STORE_PATH):
|
||||||
_remove_if_present(TEMP_PATH)
|
_remove_if_present(TEMP_PATH)
|
||||||
|
_remove_if_present(BACKUP_PATH)
|
||||||
return
|
return
|
||||||
if FileAccess.file_exists(BACKUP_PATH):
|
if FileAccess.file_exists(BACKUP_PATH):
|
||||||
_rename(BACKUP_PATH, STORE_PATH)
|
_rename(BACKUP_PATH, STORE_PATH)
|
||||||
_set_warning("Recovered saved servers after an interrupted write.")
|
|
||||||
_remove_if_present(TEMP_PATH)
|
_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:
|
func _rename(from_path: String, to_path: String) -> bool:
|
||||||
return DirAccess.rename_absolute(
|
return DirAccess.rename_absolute(
|
||||||
ProjectSettings.globalize_path(from_path),
|
ProjectSettings.globalize_path(from_path),
|
||||||
|
|
|
||||||
|
|
@ -4,84 +4,25 @@ extends Control
|
||||||
signal join_requested(endpoint: String)
|
signal join_requested(endpoint: String)
|
||||||
signal back_requested
|
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: 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 _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 _cancel_button: BubbleButton = %CancelButton
|
||||||
@onready var _back_button: BubbleButton = %BackButton
|
@onready var _back_button: BubbleButton = %BackButton
|
||||||
@onready var _open_close_button: BubbleButton = %OpenCloseButton
|
@onready var _open_close_button: BubbleButton = %OpenCloseButton
|
||||||
@onready var _status: Label = %Status
|
@onready var _status: Label = %Status
|
||||||
@onready var _session_summary: Label = %SessionSummary
|
@onready var _session_summary: Label = %SessionSummary
|
||||||
@onready var _delete_confirmation: BubbleConfirmationPage = (
|
|
||||||
%DeleteConfirmation
|
|
||||||
)
|
|
||||||
|
|
||||||
var _network_session: NetworkSession
|
var _network_session: NetworkSession
|
||||||
var _saved_servers: SavedServerStore
|
var _saved_servers: SavedServerStore
|
||||||
var _gameplay_context: bool = false
|
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:
|
func _ready() -> void:
|
||||||
_direct_button.pressed.connect(_set_mode.bind(Mode.DIRECT))
|
_join_button.pressed.connect(_on_join_pressed)
|
||||||
_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)
|
|
||||||
_cancel_button.pressed.connect(_on_cancel_pressed)
|
_cancel_button.pressed.connect(_on_cancel_pressed)
|
||||||
_back_button.pressed.connect(_on_back_pressed)
|
_back_button.pressed.connect(_on_back_pressed)
|
||||||
_open_close_button.pressed.connect(_on_open_close_pressed)
|
_open_close_button.pressed.connect(_on_open_close_pressed)
|
||||||
_address.text_submitted.connect(func(_value: String) -> void:
|
_address.text_submitted.connect(_on_address_submitted)
|
||||||
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)
|
|
||||||
hide()
|
hide()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -109,11 +50,6 @@ func setup(
|
||||||
_on_peer_count_changed
|
_on_peer_count_changed
|
||||||
):
|
):
|
||||||
_network_session.peer_count_changed.connect(_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()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -123,14 +59,12 @@ func open_page(preserved_endpoint: String = "") -> void:
|
||||||
elif _address.text.is_empty():
|
elif _address.text.is_empty():
|
||||||
_address.text = "127.0.0.1:7777"
|
_address.text = "127.0.0.1:7777"
|
||||||
show()
|
show()
|
||||||
_set_mode(_mode)
|
_refresh()
|
||||||
if _mode == Mode.DIRECT:
|
_address.grab_focus()
|
||||||
_address.grab_focus()
|
_address.select_all()
|
||||||
_address.select_all()
|
|
||||||
|
|
||||||
|
|
||||||
func close_page() -> void:
|
func close_page() -> void:
|
||||||
_clear_edit_state()
|
|
||||||
hide()
|
hide()
|
||||||
get_viewport().gui_release_focus()
|
get_viewport().gui_release_focus()
|
||||||
|
|
||||||
|
|
@ -140,21 +74,15 @@ func get_endpoint_text() -> String:
|
||||||
|
|
||||||
|
|
||||||
func set_status(message: String) -> void:
|
func set_status(message: String) -> void:
|
||||||
_set_status(_friendly_connection_message(message), true)
|
_status.text = message
|
||||||
|
|
||||||
|
|
||||||
func _set_mode(mode: Mode) -> void:
|
func _on_join_pressed() -> void:
|
||||||
_mode = mode
|
_request_join()
|
||||||
_selected_entry = null
|
|
||||||
_clear_edit_state()
|
|
||||||
_refresh_entries()
|
func _on_address_submitted(_value: String) -> void:
|
||||||
_refresh()
|
_request_join()
|
||||||
if not is_visible_in_tree():
|
|
||||||
return
|
|
||||||
if mode == Mode.DIRECT:
|
|
||||||
_address.grab_focus()
|
|
||||||
else:
|
|
||||||
_server_list.grab_focus()
|
|
||||||
|
|
||||||
|
|
||||||
func _request_join() -> void:
|
func _request_join() -> void:
|
||||||
|
|
@ -163,376 +91,14 @@ func _request_join() -> void:
|
||||||
NetworkSession.State.SERVER_LOST,
|
NetworkSession.State.SERVER_LOST,
|
||||||
]:
|
]:
|
||||||
_network_session.reset_failure()
|
_network_session.reset_failure()
|
||||||
var endpoint_text: String = (
|
var endpoint: ConnectionEndpoint = EndpointParser.parse(_address.text)
|
||||||
_selected_entry.normalized_endpoint
|
|
||||||
if _mode != Mode.DIRECT and _selected_entry != null
|
|
||||||
else _address.text
|
|
||||||
)
|
|
||||||
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
|
|
||||||
if not endpoint.is_valid():
|
if not endpoint.is_valid():
|
||||||
_set_status(endpoint.error_message, true)
|
_status.text = endpoint.error_message
|
||||||
return
|
return
|
||||||
_set_status("Connecting…")
|
|
||||||
_address.text = endpoint.normalized_display
|
_address.text = endpoint.normalized_display
|
||||||
join_requested.emit(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:
|
func _on_cancel_pressed() -> void:
|
||||||
if _network_session != null:
|
if _network_session != null:
|
||||||
_network_session.cancel_connection()
|
_network_session.cancel_connection()
|
||||||
|
|
@ -540,10 +106,6 @@ func _on_cancel_pressed() -> void:
|
||||||
|
|
||||||
|
|
||||||
func _on_back_pressed() -> void:
|
func _on_back_pressed() -> void:
|
||||||
if _name_entry_active or _delete_armed:
|
|
||||||
_clear_edit_state()
|
|
||||||
_refresh()
|
|
||||||
return
|
|
||||||
if (
|
if (
|
||||||
_network_session != null
|
_network_session != null
|
||||||
and _network_session.state in [
|
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:
|
func _on_status_message_changed(message: String) -> void:
|
||||||
_set_status(_friendly_connection_message(message))
|
_status.text = message
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
func _on_connection_error(message: String) -> void:
|
func _on_connection_error(message: String) -> void:
|
||||||
_set_status(_friendly_connection_message(message), true)
|
_status.text = message
|
||||||
_refresh()
|
_refresh()
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -583,11 +145,31 @@ func _on_peer_count_changed(player_count: int, max_players: int) -> void:
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
func _on_store_changed() -> void:
|
func _refresh() -> void:
|
||||||
var selected_id: String = (
|
if not is_node_ready() or _network_session == null:
|
||||||
_selected_entry.entry_id if _selected_entry != null else ""
|
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 _open_close_button.visible:
|
||||||
if not selected_id.is_empty():
|
_open_close_button.text = (
|
||||||
_select_entry_id(selected_id)
|
"close\ngame"
|
||||||
_refresh()
|
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(),
|
||||||
|
]
|
||||||
|
|
|
||||||
|
|
@ -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="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="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_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"]
|
[sub_resource type="StyleBoxFlat" id="StyleBox_paper"]
|
||||||
bg_color = Color(0.93, 0.885, 0.73, 1)
|
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_right = 58
|
||||||
corner_radius_bottom_left = 48
|
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"]
|
[node name="JoinGamePage" type="Control"]
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 3
|
layout_mode = 3
|
||||||
|
|
@ -98,179 +27,54 @@ script = ExtResource("1_script")
|
||||||
|
|
||||||
[node name="Paper" type="PanelContainer" parent="."]
|
[node name="Paper" type="PanelContainer" parent="."]
|
||||||
layout_mode = 0
|
layout_mode = 0
|
||||||
offset_left = 230.0
|
offset_left = 340.0
|
||||||
offset_top = 64.0
|
offset_top = 132.0
|
||||||
offset_right = 1050.0
|
offset_right = 940.0
|
||||||
offset_bottom = 656.0
|
offset_bottom = 570.0
|
||||||
theme_override_styles/panel = SubResource("StyleBox_paper")
|
theme_override_styles/panel = SubResource("StyleBox_paper")
|
||||||
|
|
||||||
[node name="Margin" type="MarginContainer" parent="Paper"]
|
[node name="Margin" type="MarginContainer" parent="Paper"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/margin_left = 54
|
theme_override_constants/margin_left = 70
|
||||||
theme_override_constants/margin_top = 30
|
theme_override_constants/margin_top = 52
|
||||||
theme_override_constants/margin_right = 54
|
theme_override_constants/margin_right = 70
|
||||||
theme_override_constants/margin_bottom = 30
|
theme_override_constants/margin_bottom = 52
|
||||||
|
|
||||||
[node name="Layout" type="VBoxContainer" parent="Paper/Margin"]
|
[node name="Layout" type="VBoxContainer" parent="Paper/Margin"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 10
|
theme_override_constants/separation = 18
|
||||||
alignment = 1
|
alignment = 1
|
||||||
|
|
||||||
[node name="Title" type="Label" parent="Paper/Margin/Layout"]
|
[node name="Title" type="Label" parent="Paper/Margin/Layout"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
|
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"
|
text = "join game"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="Modes" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
[node name="Hint" type="Label" 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
|
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
|
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
|
||||||
theme_override_font_sizes/font_size = 17
|
theme_override_font_sizes/font_size = 17
|
||||||
text = "server address"
|
text = "hostname or IP address • optional port"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="Address" type="LineEdit" parent="Paper/Margin/Layout"]
|
[node name="Address" type="LineEdit" parent="Paper/Margin/Layout"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 42)
|
custom_minimum_size = Vector2(0, 52)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.96, 0.93, 0.82, 1)
|
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
|
||||||
theme_override_colors/font_uneditable_color = Color(0.8, 0.77, 0.68, 0.9)
|
theme_override_font_sizes/font_size = 22
|
||||||
theme_override_colors/font_selected_color = Color(1, 0.98, 0.9, 1)
|
placeholder_text = "example.net:7777"
|
||||||
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"
|
|
||||||
alignment = 1
|
alignment = 1
|
||||||
max_length = 300
|
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"]
|
[node name="Status" type="Label" parent="Paper/Margin/Layout"]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(0, 38)
|
custom_minimum_size = Vector2(0, 42)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
|
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
|
||||||
theme_override_styles/normal = SubResource("StyleBox_status")
|
|
||||||
text = "Direct UDP connection • default port 7777"
|
text = "Direct UDP connection • default port 7777"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
vertical_alignment = 1
|
vertical_alignment = 1
|
||||||
|
|
@ -281,74 +85,23 @@ unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_colors/font_color = Color(0.035, 0.145, 0.22, 1)
|
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"
|
text = "1 / 8 players"
|
||||||
horizontal_alignment = 1
|
horizontal_alignment = 1
|
||||||
|
|
||||||
[node name="Actions" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
[node name="Actions" type="HBoxContainer" parent="Paper/Margin/Layout"]
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
theme_override_constants/separation = 7
|
theme_override_constants/separation = 12
|
||||||
alignment = 1
|
alignment = 1
|
||||||
|
|
||||||
[node name="JoinButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
|
[node name="JoinButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(82, 72)
|
custom_minimum_size = Vector2(112, 106)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "join"
|
text = "join"
|
||||||
neutral_size = Vector2(82, 72)
|
neutral_size = Vector2(112, 106)
|
||||||
minimum_font_size = 15
|
minimum_font_size = 18
|
||||||
maximum_font_size = 18
|
maximum_font_size = 24
|
||||||
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
|
|
||||||
horizontal_amplitude = 0.0
|
horizontal_amplitude = 0.0
|
||||||
vertical_amplitude = 0.0
|
vertical_amplitude = 0.0
|
||||||
deformation_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")]
|
[node name="OpenCloseButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
custom_minimum_size = Vector2(88, 72)
|
custom_minimum_size = Vector2(112, 106)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "open\ngame"
|
text = "open\ngame"
|
||||||
neutral_size = Vector2(88, 72)
|
neutral_size = Vector2(112, 106)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 17
|
||||||
maximum_font_size = 17
|
maximum_font_size = 22
|
||||||
horizontal_amplitude = 0.0
|
horizontal_amplitude = 0.0
|
||||||
vertical_amplitude = 0.0
|
vertical_amplitude = 0.0
|
||||||
deformation_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")]
|
[node name="CancelButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
visible = false
|
visible = false
|
||||||
custom_minimum_size = Vector2(76, 68)
|
custom_minimum_size = Vector2(98, 94)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "cancel"
|
text = "cancel"
|
||||||
neutral_size = Vector2(76, 68)
|
neutral_size = Vector2(98, 94)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 16
|
||||||
maximum_font_size = 17
|
maximum_font_size = 20
|
||||||
horizontal_amplitude = 0.0
|
horizontal_amplitude = 0.0
|
||||||
vertical_amplitude = 0.0
|
vertical_amplitude = 0.0
|
||||||
deformation_amplitude = 0.0
|
deformation_amplitude = 0.0
|
||||||
|
|
||||||
[node name="BackButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
|
[node name="BackButton" parent="Paper/Margin/Layout/Actions" instance=ExtResource("3_bubble")]
|
||||||
unique_name_in_owner = true
|
unique_name_in_owner = true
|
||||||
custom_minimum_size = Vector2(76, 68)
|
custom_minimum_size = Vector2(98, 94)
|
||||||
layout_mode = 2
|
layout_mode = 2
|
||||||
text = "back"
|
text = "back"
|
||||||
neutral_size = Vector2(76, 68)
|
neutral_size = Vector2(98, 94)
|
||||||
minimum_font_size = 14
|
minimum_font_size = 16
|
||||||
maximum_font_size = 17
|
maximum_font_size = 20
|
||||||
horizontal_amplitude = 0.0
|
horizontal_amplitude = 0.0
|
||||||
vertical_amplitude = 0.0
|
vertical_amplitude = 0.0
|
||||||
deformation_amplitude = 0.0
|
deformation_amplitude = 0.0
|
||||||
|
|
||||||
[node name="DeleteConfirmation" parent="." instance=ExtResource("4_confirmation")]
|
|
||||||
unique_name_in_owner = true
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue