Add direct-connect multiplayer foundation
This commit is contained in:
parent
9e9850dd20
commit
24f8263175
36 changed files with 2633 additions and 45 deletions
187
network/saved_server_store.gd
Normal file
187
network/saved_server_store.gd
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
class_name SavedServerStore
|
||||
extends Node
|
||||
|
||||
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
|
||||
|
||||
var _entries: Array[Dictionary] = []
|
||||
var _loaded: bool = false
|
||||
|
||||
|
||||
func list_entries() -> Array[Dictionary]:
|
||||
_ensure_loaded()
|
||||
return _entries.duplicate(true)
|
||||
|
||||
|
||||
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 {}
|
||||
|
||||
|
||||
func save_entry(
|
||||
display_name: String,
|
||||
endpoint: ConnectionEndpoint,
|
||||
) -> bool:
|
||||
_ensure_loaded()
|
||||
if (
|
||||
endpoint == null
|
||||
or not endpoint.is_valid()
|
||||
or display_name.strip_edges().is_empty()
|
||||
or display_name.length() > 80
|
||||
):
|
||||
return false
|
||||
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()
|
||||
|
||||
|
||||
func remove_entry(entry_id: String) -> 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()
|
||||
return false
|
||||
|
||||
|
||||
func record_successful_connection(
|
||||
endpoint: ConnectionEndpoint,
|
||||
observed_max_players: int,
|
||||
) -> 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.
|
||||
return true
|
||||
|
||||
|
||||
func _ensure_loaded() -> void:
|
||||
if _loaded:
|
||||
return
|
||||
_loaded = true
|
||||
_recover_interrupted_write()
|
||||
if not FileAccess.file_exists(STORE_PATH):
|
||||
return
|
||||
var file := FileAccess.open(STORE_PATH, FileAccess.READ)
|
||||
if file == null:
|
||||
return
|
||||
var json := JSON.new()
|
||||
var error: Error = json.parse(file.get_as_text())
|
||||
file.close()
|
||||
if error != OK or typeof(json.data) != TYPE_DICTIONARY:
|
||||
return
|
||||
var data: Dictionary = json.data
|
||||
if data.get("format_version") != FORMAT_VERSION:
|
||||
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]
|
||||
)
|
||||
var endpoint: ConnectionEndpoint = EndpointParser.parse(endpoint_text)
|
||||
if (
|
||||
not endpoint.is_valid()
|
||||
or seen.has(endpoint.normalized_display)
|
||||
or typeof(entry.get("entry_id")) != TYPE_STRING
|
||||
or typeof(entry.get("display_name")) != TYPE_STRING
|
||||
):
|
||||
continue
|
||||
entry["normalized_endpoint"] = endpoint.normalized_display
|
||||
seen[endpoint.normalized_display] = true
|
||||
_entries.append(entry.duplicate(true))
|
||||
if _entries.size() >= MAX_ENTRIES:
|
||||
break
|
||||
|
||||
|
||||
func _save_atomic() -> bool:
|
||||
var file := FileAccess.open(TEMP_PATH, FileAccess.WRITE)
|
||||
if file == null:
|
||||
return false
|
||||
file.store_string(JSON.stringify({
|
||||
"format_version": FORMAT_VERSION,
|
||||
"entries": _entries,
|
||||
}, "\t"))
|
||||
file.flush()
|
||||
var error: Error = file.get_error()
|
||||
file.close()
|
||||
if error != OK:
|
||||
_remove_if_present(TEMP_PATH)
|
||||
return false
|
||||
_remove_if_present(BACKUP_PATH)
|
||||
var had_primary: bool = FileAccess.file_exists(STORE_PATH)
|
||||
if had_primary and not _rename(STORE_PATH, BACKUP_PATH):
|
||||
_remove_if_present(TEMP_PATH)
|
||||
return false
|
||||
if not _rename(TEMP_PATH, STORE_PATH):
|
||||
if had_primary:
|
||||
_rename(BACKUP_PATH, STORE_PATH)
|
||||
return false
|
||||
_remove_if_present(BACKUP_PATH)
|
||||
return true
|
||||
|
||||
|
||||
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)
|
||||
_remove_if_present(TEMP_PATH)
|
||||
|
||||
|
||||
func _rename(from_path: String, to_path: String) -> bool:
|
||||
return DirAccess.rename_absolute(
|
||||
ProjectSettings.globalize_path(from_path),
|
||||
ProjectSettings.globalize_path(to_path)
|
||||
) == OK
|
||||
|
||||
|
||||
func _remove_if_present(path: String) -> bool:
|
||||
return (
|
||||
not FileAccess.file_exists(path)
|
||||
or DirAccess.remove_absolute(ProjectSettings.globalize_path(path)) == OK
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue