Add saved servers and recent connections
This commit is contained in:
parent
918e75802a
commit
362273e834
7 changed files with 1250 additions and 168 deletions
|
|
@ -56,6 +56,9 @@ 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
|
||||
|
||||
|
||||
|
|
@ -229,6 +232,24 @@ 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()
|
||||
|
||||
|
|
@ -457,6 +478,11 @@ 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(
|
||||
|
|
@ -475,11 +501,6 @@ 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()
|
||||
|
||||
|
||||
|
|
|
|||
88
network/saved_server_entry.gd
Normal file
88
network/saved_server_entry.gd
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
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
network/saved_server_entry.gd.uid
Normal file
1
network/saved_server_entry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b3guw5fet7lv0
|
||||
|
|
@ -5,86 +5,235 @@ 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_ENTRIES: int = 100
|
||||
const MAX_SAVED_ENTRIES: int = 100
|
||||
const MAX_RECENT_ENTRIES: int = 20
|
||||
const MAX_DISPLAY_NAME_LENGTH: int = 80
|
||||
|
||||
var _entries: Array[Dictionary] = []
|
||||
signal data_changed
|
||||
signal recovery_warning_changed(message: String)
|
||||
|
||||
var _saved_entries: Array[Dictionary] = []
|
||||
var _recent_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 _entries.duplicate(true)
|
||||
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
|
||||
|
||||
|
||||
func find_by_normalized_endpoint(value: String) -> Dictionary:
|
||||
_ensure_loaded()
|
||||
for entry: Dictionary in _entries:
|
||||
if entry.get("normalized_endpoint", "") == value:
|
||||
return entry.duplicate(true)
|
||||
return {}
|
||||
var endpoint := EndpointParser.parse(value)
|
||||
var entry: SavedServerEntry = find_saved_by_endpoint(endpoint)
|
||||
return entry.to_dictionary() if entry != null else {}
|
||||
|
||||
|
||||
func save_entry(
|
||||
func save_or_update_entry(
|
||||
display_name: String,
|
||||
endpoint: ConnectionEndpoint,
|
||||
) -> bool:
|
||||
entry_id: String = "",
|
||||
) -> SavedServerEntry:
|
||||
_ensure_loaded()
|
||||
var clean_name: String = display_name.strip_edges()
|
||||
if (
|
||||
endpoint == null
|
||||
or not endpoint.is_valid()
|
||||
or display_name.strip_edges().is_empty()
|
||||
or display_name.length() > 80
|
||||
or clean_name.is_empty()
|
||||
or clean_name.length() > MAX_DISPLAY_NAME_LENGTH
|
||||
):
|
||||
return false
|
||||
return null
|
||||
var now: int = int(Time.get_unix_time_from_system())
|
||||
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()
|
||||
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
|
||||
|
||||
|
||||
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 index: int in range(_entries.size()):
|
||||
if _entries[index].get("entry_id", "") == entry_id:
|
||||
_entries.remove_at(index)
|
||||
return _save_atomic()
|
||||
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
|
||||
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
|
||||
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.
|
||||
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()
|
||||
return true
|
||||
|
||||
|
||||
|
|
@ -95,54 +244,158 @@ func _ensure_loaded() -> void:
|
|||
_recover_interrupted_write()
|
||||
if not FileAccess.file_exists(STORE_PATH):
|
||||
return
|
||||
var file := FileAccess.open(STORE_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
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."
|
||||
)
|
||||
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:
|
||||
return
|
||||
var raw_entries: Variant = data.get("entries")
|
||||
if typeof(raw_entries) != TYPE_ARRAY:
|
||||
return
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
for value: Variant in raw_entries:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
continue
|
||||
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]
|
||||
_write_blocked = true
|
||||
_set_warning(
|
||||
"Saved servers use a newer format. Direct connection is still available."
|
||||
)
|
||||
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
|
||||
):
|
||||
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.")
|
||||
return
|
||||
var target: Array[Dictionary] = (
|
||||
_saved_entries if is_saved else _recent_entries
|
||||
)
|
||||
var seen: Dictionary[String, bool] = {}
|
||||
for value: Variant in raw:
|
||||
if typeof(value) != TYPE_DICTIONARY:
|
||||
_set_warning("Some malformed server entries were skipped.")
|
||||
continue
|
||||
entry["normalized_endpoint"] = endpoint.normalized_display
|
||||
seen[endpoint.normalized_display] = true
|
||||
_entries.append(entry.duplicate(true))
|
||||
if _entries.size() >= MAX_ENTRIES:
|
||||
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
|
||||
):
|
||||
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,
|
||||
"entries": _entries,
|
||||
"saved_entries": _saved_entries,
|
||||
"recent_entries": _recent_entries,
|
||||
}, "\t"))
|
||||
file.flush()
|
||||
var error: Error = file.get_error()
|
||||
|
|
@ -163,16 +416,32 @@ 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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue