Improve interface accessibility and UI reliability

This commit is contained in:
Alexander Sellite 2026-07-30 00:54:53 -04:00
parent ba4476511f
commit 3734d4a168
29 changed files with 514 additions and 224 deletions

View file

@ -24,11 +24,15 @@ func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool:
return Dictionary(_namespaces.get(host_fingerprint, {})).has(target_fingerprint)
func ban(host_fingerprint: String, target_fingerprint: String, name: String) -> bool:
func ban(
host_fingerprint: String,
target_fingerprint: String,
display_name: String,
) -> bool:
if (
not NetworkIdentityCrypto.valid_fingerprint(host_fingerprint)
or not NetworkIdentityCrypto.valid_fingerprint(target_fingerprint)
or not NetworkProfilePreferences.is_valid_display_name(name)
or not NetworkProfilePreferences.is_valid_display_name(display_name)
):
return false
_ensure_loaded()
@ -41,7 +45,7 @@ func ban(host_fingerprint: String, target_fingerprint: String, name: String) ->
records[target_fingerprint] = {
"host_fingerprint": host_fingerprint,
"target_fingerprint": target_fingerprint,
"last_known_display_name": name,
"last_known_display_name": display_name,
"banned_unix": int(Time.get_unix_time_from_system()),
"reason": "host_ban",
}

View file

@ -25,12 +25,12 @@ func setup(
func default_export_path(identity_type: String) -> String:
var store := _store(identity_type)
var store: LocalSigningIdentityStore = _store(identity_type)
if store == null:
return ""
if not store.is_ready() and not store.load_or_create():
return ""
var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
return _data_root.identity_backup_directory().path_join(
"%s-%s-%s.nfidentity" % [
identity_type,
@ -50,14 +50,14 @@ func export_backup(
return _finish(false, "Passphrases must match and contain at least 12 characters.")
if FileAccess.file_exists(path):
return _finish(false, "Choose a new backup filename.")
var store := _store(identity_type)
var store: LocalSigningIdentityStore = _store(identity_type)
if store == null or (not store.is_ready() and not store.load_or_create()):
return _finish(false, "The active identity is unavailable.")
var material := store.export_identity_material()
var proof := store.sign("identity_backup_self_test", [
var material: Dictionary = store.export_identity_material()
var proof: PackedByteArray = store.sign("identity_backup_self_test", [
identity_type, material["fingerprint"],
])
var envelope := {
var envelope: Dictionary = {
"magic": MAGIC,
"format_version": FORMAT_VERSION,
"identity_type": identity_type,
@ -70,16 +70,18 @@ func export_backup(
"self_signature": Marshalls.raw_to_base64(proof),
}
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
var file := FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, passphrase)
var file: FileAccess = FileAccess.open_encrypted_with_pass(
path, FileAccess.WRITE, passphrase
)
if file == null:
return _finish(false, "Could not write this identity backup.")
file.store_string(JSON.stringify(envelope))
file.flush()
var ok := file.get_error() == OK
var ok: bool = file.get_error() == OK
file.close()
if not ok:
return _finish(false, "Could not write this identity backup.")
var verified := inspect_backup(path, passphrase, identity_type)
var verified: Dictionary = inspect_backup(path, passphrase, identity_type)
if not bool(verified.get("ok", false)):
DirAccess.remove_absolute(path)
return _finish(false, "Could not verify this identity backup.")
@ -97,12 +99,14 @@ func inspect_backup(
or FileAccess.get_size(path) > MAX_BACKUP_BYTES
):
return {"ok": false}
var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, passphrase)
var file: FileAccess = FileAccess.open_encrypted_with_pass(
path, FileAccess.READ, passphrase
)
if file == null:
return {"ok": false}
var text := file.get_as_text()
var text: String = file.get_as_text()
file.close()
var json := JSON.new()
var json: JSON = JSON.new()
if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY:
return {"ok": false}
var data: Dictionary = json.data
@ -113,21 +117,23 @@ func inspect_backup(
or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM
):
return {"ok": false}
var private_pem := str(data.get("private_pem", ""))
var public_pem := NetworkIdentityCrypto.normalize_public_pem(
var private_pem: String = str(data.get("private_pem", ""))
var public_pem: String = NetworkIdentityCrypto.normalize_public_pem(
str(data.get("public_pem", ""))
)
var fingerprint := str(data.get("fingerprint", ""))
var fingerprint: String = str(data.get("fingerprint", ""))
if (
private_pem.length() > 128 * 1024
or public_pem.length() > 32 * 1024
or NetworkIdentityCrypto.fingerprint_public_pem(public_pem) != fingerprint
):
return {"ok": false}
var key := CryptoKey.new()
var key: CryptoKey = CryptoKey.new()
if key.load_from_string(private_pem) != OK:
return {"ok": false}
var signature := Marshalls.base64_to_raw(str(data.get("self_signature", "")))
var signature: PackedByteArray = Marshalls.base64_to_raw(
str(data.get("self_signature", ""))
)
if not NetworkIdentityCrypto.verify_fields(
NetworkIdentityCrypto.load_public_key(public_pem),
"identity_backup_self_test",
@ -135,7 +141,7 @@ func inspect_backup(
signature,
):
return {"ok": false}
var fresh := NetworkIdentityCrypto.sign_fields(
var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields(
key, "identity_import_self_test", [fingerprint]
)
if not NetworkIdentityCrypto.verify_fields(
@ -160,12 +166,12 @@ func import_backup(
passphrase: String,
confirmed_replacement: bool,
) -> Dictionary:
var inspected := inspect_backup(path, passphrase, identity_type)
var inspected: Dictionary = inspect_backup(path, passphrase, identity_type)
if not bool(inspected.get("ok", false)):
_finish(false, "Could not open this identity backup.")
return {"ok": false}
var store := _store(identity_type)
var incoming := str(inspected["fingerprint"])
var store: LocalSigningIdentityStore = _store(identity_type)
var incoming: String = str(inspected["fingerprint"])
if store.fingerprint == incoming:
_finish(true, "This identity is already active.")
return {"ok": true, "same": true}
@ -176,7 +182,7 @@ func import_backup(
"current_fingerprint": store.fingerprint,
"incoming_fingerprint": incoming,
}
var result := store.install_identity_material(
var result: Dictionary = store.install_identity_material(
str(inspected["private_pem"]),
str(inspected["public_pem"]),
incoming,

View file

@ -245,10 +245,10 @@ func _on_peer_removed(peer_id: int) -> void:
_rate_times.erase(peer_id)
if not _session.is_host():
return
var name: String = _peer_names.get(peer_id, "Player")
var display_name: String = _peer_names.get(peer_id, "Player")
_peer_names.erase(peer_id)
_broadcast(_make_message(
NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s left." % name
NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s left." % display_name
))

View file

@ -144,14 +144,14 @@ func get_recipient_choices() -> Array[Dictionary]:
var normalized := str(choice["name"]).to_lower()
counts[normalized] = int(counts.get(normalized, 0)) + 1
for choice: Dictionary in choices:
var name: String = choice["name"]
if int(counts.get(name.to_lower(), 0)) > 1:
var display_name: String = choice["name"]
if int(counts.get(display_name.to_lower(), 0)) > 1:
choice["label"] = "%s · %s" % [
name,
display_name,
NetworkIdentityCrypto.compact_suffix(choice["fingerprint"]),
]
else:
choice["label"] = name
choice["label"] = display_name
return choices
@ -531,8 +531,8 @@ func prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
func _prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
var ready := _can_receive(letter["attachment"])
_send_phase_ack("recipient_prepared", transfer_id, ready)
var recipient_ready: bool = _can_receive(letter["attachment"])
_send_phase_ack("recipient_prepared", transfer_id, recipient_ready)
func _request_sender_commit(peer_id: int, transfer_id: String, letter: Dictionary) -> void:

View file

@ -117,12 +117,17 @@ func kick(peer_id: int, fingerprint: String, revision: int) -> bool:
return ok
func ban(peer_id: int, fingerprint: String, name: String, revision: int) -> bool:
func ban(
peer_id: int,
fingerprint: String,
display_name: String,
revision: int,
) -> bool:
if not _valid_moderation_target(peer_id, fingerprint, revision):
moderation_finished.emit(false, "That player is no longer connected.")
return false
var host_fingerprint := _session.get_host_identity_fingerprint()
if not _bans.ban(host_fingerprint, fingerprint, name):
if not _bans.ban(host_fingerprint, fingerprint, display_name):
moderation_finished.emit(false, "Ban could not be saved.")
return false
var ok := _session.kick_authenticated_peer(peer_id, fingerprint, true)

View file

@ -172,10 +172,14 @@ func submit_profile_apply(data: Dictionary) -> void:
func _process_apply_request(peer_id: int, data: Dictionary) -> void:
var name := str(data["display_name"]).strip_edges()
var conflict := _name_conflicts(peer_id, name)
var accepted := not conflict or bool(data["use_anyway"])
var suggestions := _make_suggestions(peer_id, name) if conflict else PackedStringArray()
var display_name: String = str(data["display_name"]).strip_edges()
var conflict: bool = _name_conflicts(peer_id, display_name)
var accepted: bool = not conflict or bool(data["use_anyway"])
var suggestions: PackedStringArray = (
_make_suggestions(peer_id, display_name)
if conflict
else PackedStringArray()
)
if peer_id == _session.get_local_peer_id():
_apply_local_result(
str(data["request_id"]), accepted, "", conflict, suggestions
@ -184,7 +188,7 @@ func _process_apply_request(peer_id: int, data: Dictionary) -> void:
if accepted:
_host_pending_apply[str(data["request_id"])] = {
"peer_id": peer_id,
"display_name": name,
"display_name": display_name,
"appearance": Dictionary(data["appearance"]).duplicate(true),
"authorization": {
"request_id": data["request_id"],
@ -214,10 +218,10 @@ func confirm_profile_saved(request_id: String) -> void:
):
return
_host_pending_apply.erase(request_id)
var name := str(pending["display_name"])
var appearance := Dictionary(pending["appearance"])
_session.apply_canonical_profile(sender_id, name, appearance)
var record := _session.get_peer_record(sender_id)
var display_name: String = str(pending["display_name"])
var appearance: Dictionary = Dictionary(pending["appearance"])
_session.apply_canonical_profile(sender_id, display_name, appearance)
var record: PeerRegistry.PeerRecord = _session.get_peer_record(sender_id)
if record != null:
record.profile_authorization = pending.get("authorization", {}).duplicate(true)
_apply_to_avatar(sender_id, appearance)

View file

@ -1145,13 +1145,13 @@ func _apply_spawn_entry(entry: Dictionary) -> void:
)
_registry.update_appearance(peer_id, appearance)
var transform: Transform3D = _spawn_service.get_spawn_transform_for_index(0)
var position: Array = entry["position"]
if position.size() != 3:
var spawn_position: Array = entry["position"]
if spawn_position.size() != 3:
return
transform.origin = Vector3(
float(position[0]),
float(position[1]),
float(position[2])
float(spawn_position[0]),
float(spawn_position[1]),
float(spawn_position[2])
)
transform.basis = Basis(Vector3.UP, float(entry["yaw"]))
var avatar := _spawn_service.spawn_remote_player(peer_id, transform, false)

View file

@ -34,7 +34,7 @@ var override_active := false
func resolve() -> bool:
_load_bootstrap_identity()
var command_line := _command_line_override()
var command_line: String = _command_line_override()
if not command_line.is_empty():
override_active = true
mode = Mode.COMMAND_LINE_OVERRIDE
@ -42,18 +42,18 @@ func resolve() -> bool:
if OS.has_environment(ENVIRONMENT_VARIABLE):
override_active = true
mode = Mode.ENVIRONMENT_OVERRIDE
var environment_path := OS.get_environment(ENVIRONMENT_VARIABLE)
var environment_path: String = OS.get_environment(ENVIRONMENT_VARIABLE)
if not environment_path.is_absolute_path():
return _fail("NETFISHING_DATA_DIR must be an absolute path.")
return _activate_existing(environment_path, "", false)
var bootstrap := _read_json(BOOTSTRAP_PATH, 64 * 1024)
var bootstrap: Dictionary = _read_json(BOOTSTRAP_PATH, 64 * 1024)
if bootstrap.is_empty() and FileAccess.file_exists(BOOTSTRAP_PATH + ".backup"):
bootstrap = _read_json(BOOTSTRAP_PATH + ".backup", 64 * 1024)
if not bootstrap.is_empty():
if bootstrap.get("format_version") != BOOTSTRAP_VERSION:
return _fail("The data-folder pointer uses an unsupported version.")
var selected := str(bootstrap.get("selected_absolute_path", ""))
var expected := str(bootstrap.get("expected_root_id", ""))
var selected: String = str(bootstrap.get("selected_absolute_path", ""))
var expected: String = str(bootstrap.get("expected_root_id", ""))
if selected.is_empty():
return _fail("The data-folder pointer is incomplete.")
mode = (
@ -68,7 +68,7 @@ func resolve() -> bool:
func default_visible_path() -> String:
var documents := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS)
return documents.path_join("NETFISHING") if not documents.is_empty() else ""
@ -77,14 +77,14 @@ func select_new_root(path: String, app_data: bool = false) -> bool:
return _fail("The data folder is controlled by a process override.")
if device_id.length() != 32:
device_id = Crypto.new().generate_random_bytes(16).hex_encode()
var normalized := _normalize(path)
var normalized: String = _normalize(path)
if app_data:
normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH)
if not _validate_candidate(normalized, true):
return false
var manifest_path := normalized.path_join(MANIFEST_FILENAME)
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
if FileAccess.file_exists(manifest_path):
var manifest := _read_json(manifest_path)
var manifest: Dictionary = _read_json(manifest_path)
if not _valid_manifest(manifest):
return _fail("The selected folder has a malformed NETFISHING manifest.")
root_id = str(manifest["root_id"])
@ -107,30 +107,30 @@ func select_new_root(path: String, app_data: bool = false) -> bool:
func create_unbound_root(path: String) -> Dictionary:
var normalized := _normalize(path)
var normalized: String = _normalize(path)
if not _validate_candidate(normalized, true):
return {"ok": false, "message": error_message}
if not _directory_is_empty(normalized):
return {"ok": false, "message": "The destination staging folder is not empty."}
var id := Crypto.new().generate_random_bytes(16).hex_encode()
var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
if not _create_layout(normalized, id):
return {"ok": false, "message": "The portable layout could not be created."}
return {"ok": true, "root_id": id}
func create_app_data_layout_for_migration(path: String) -> Dictionary:
var normalized := _normalize(path)
var normalized: String = _normalize(path)
if not _validate_candidate(normalized, false):
return {"ok": false, "message": error_message}
var manifest_path := normalized.path_join(MANIFEST_FILENAME)
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
if FileAccess.file_exists(manifest_path):
var manifest := _read_json(manifest_path)
var manifest: Dictionary = _read_json(manifest_path)
return (
{"ok": true, "root_id": str(manifest.get("root_id", ""))}
if _valid_manifest(manifest)
else {"ok": false, "message": "The app-data manifest is malformed."}
)
var id := Crypto.new().generate_random_bytes(16).hex_encode()
var id: String = Crypto.new().generate_random_bytes(16).hex_encode()
if not _create_layout(normalized, id):
return {"ok": false, "message": "The app-data layout could not be created."}
return {"ok": true, "root_id": id}
@ -141,7 +141,7 @@ func use_existing_root(path: String) -> bool:
return _fail("The data folder is controlled by a process override.")
if device_id.length() != 32:
device_id = Crypto.new().generate_random_bytes(16).hex_encode()
var normalized := _normalize(path)
var normalized: String = _normalize(path)
if not _activate_existing(normalized, "", true):
return false
if not _write_bootstrap(root_path, root_id):
@ -156,7 +156,7 @@ func use_existing_root(path: String) -> bool:
return true
func path_for(owner: StringName) -> String:
func path_for(store_owner: StringName) -> String:
var relative: String = {
&"player_save": "player/player_save.json",
&"network_profile": "player/network_profile.json",
@ -166,7 +166,7 @@ func path_for(owner: StringName) -> String:
&"player_relationships": "social/player_relationships.json",
&"server_trust": "social/server_trust.json",
&"host_bans": "social/host_bans.json",
}.get(owner, "")
}.get(store_owner, "")
return root_path.path_join(relative) if not relative.is_empty() else ""
@ -202,16 +202,16 @@ func report_conflict(message: String, conflict_path: String) -> void:
func _activate_existing(path: String, expected_id: String, permit_creation: bool) -> bool:
var normalized := _normalize(path)
var normalized: String = _normalize(path)
if not _validate_candidate(normalized, permit_creation):
return false
var manifest_path := normalized.path_join(MANIFEST_FILENAME)
var manifest_path: String = normalized.path_join(MANIFEST_FILENAME)
if not FileAccess.file_exists(manifest_path):
return _fail("The selected folder is not a NETFISHING data folder.")
var manifest := _read_json(manifest_path)
var manifest: Dictionary = _read_json(manifest_path)
if not _valid_manifest(manifest):
return _fail("The NETFISHING data-folder manifest is malformed.")
var found_id := str(manifest["root_id"])
var found_id: String = str(manifest["root_id"])
if not expected_id.is_empty() and expected_id != found_id:
return _fail("The selected data folder does not match this device pointer.")
if not _test_writable(normalized):
@ -228,7 +228,7 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool
func _validate_candidate(path: String, create: bool) -> bool:
if path.is_empty() or not path.is_absolute_path():
return _fail("Choose an absolute filesystem folder.")
var project := ProjectSettings.globalize_path("res://").trim_suffix("/")
var project: String = ProjectSettings.globalize_path("res://").trim_suffix("/")
if path == project or path.begins_with(project + "/"):
return _fail("The project folder cannot be used as the player data folder.")
if not DirAccess.dir_exists_absolute(path):
@ -238,8 +238,10 @@ func _validate_candidate(path: String, create: bool) -> bool:
func _test_writable(path: String) -> bool:
var probe := path.path_join(".netfishing-write-%s.tmp" % device_id.left(12))
var file := FileAccess.open(probe, FileAccess.WRITE)
var probe: String = path.path_join(
".netfishing-write-%s.tmp" % device_id.left(12)
)
var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE)
if file == null:
return false
file.store_string("probe")
@ -254,8 +256,8 @@ func _create_layout(path: String, id: String) -> bool:
]:
if DirAccess.make_dir_recursive_absolute(path.path_join(relative)) != OK:
return false
var now := int(Time.get_unix_time_from_system())
var manifest := {
var now: int = int(Time.get_unix_time_from_system())
var manifest: Dictionary = {
"format_version": MANIFEST_VERSION,
"layout_version": LAYOUT_VERSION,
"application": APPLICATION_ID,
@ -267,7 +269,7 @@ func _create_layout(path: String, id: String) -> bool:
path.path_join(MANIFEST_FILENAME), JSON.stringify(manifest, "\t")
):
return false
var readme := (
var readme: String = (
"NETFISHING player data\n\n"
+ "This folder is safe to synchronize with tools such as Syncthing.\n"
+ "Active private identity keys remain device-local.\n"
@ -280,8 +282,8 @@ func _create_layout(path: String, id: String) -> bool:
func _write_bootstrap(path: String, id: String) -> bool:
var now := int(Time.get_unix_time_from_system())
var data := {
var now: int = int(Time.get_unix_time_from_system())
var data: Dictionary = {
"format_version": BOOTSTRAP_VERSION,
"selected_absolute_path": path,
"expected_root_id": id,
@ -296,7 +298,7 @@ func _write_bootstrap(path: String, id: String) -> bool:
func _load_bootstrap_identity() -> void:
var data := _read_json(BOOTSTRAP_PATH, 64 * 1024)
var data: Dictionary = _read_json(BOOTSTRAP_PATH, 64 * 1024)
if data.is_empty():
data = _read_json(BOOTSTRAP_PATH + ".backup", 64 * 1024)
device_id = str(data.get("device_id", ""))
@ -305,9 +307,9 @@ func _load_bootstrap_identity() -> void:
func _command_line_override() -> String:
var args := OS.get_cmdline_user_args()
var args: PackedStringArray = OS.get_cmdline_user_args()
for index: int in args.size():
var value := args[index]
var value: String = args[index]
if value.begins_with("--data-dir="):
return value.trim_prefix("--data-dir=")
if value == "--data-dir" and index + 1 < args.size():
@ -326,15 +328,15 @@ func _valid_manifest(data: Dictionary) -> bool:
func _directory_is_empty(path: String) -> bool:
var access := DirAccess.open(path)
var access: DirAccess = DirAccess.open(path)
if access == null:
return true
access.list_dir_begin()
var name := access.get_next()
while name in [".", ".."]:
name = access.get_next()
var entry_name: String = access.get_next()
while entry_name in [".", ".."]:
entry_name = access.get_next()
access.list_dir_end()
return name.is_empty()
return entry_name.is_empty()
func _normalize(path: String) -> String:
@ -344,37 +346,37 @@ func _normalize(path: String) -> String:
func _read_json(path: String, maximum := 1024 * 1024) -> Dictionary:
if not FileAccess.file_exists(path):
return {}
var file := FileAccess.open(path, FileAccess.READ)
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
if file == null or file.get_length() > maximum:
return {}
var json := JSON.new()
var error := json.parse(file.get_as_text())
var json: 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 _write_text_atomic(path: String, text: String) -> bool:
var absolute := (
var absolute: String = (
ProjectSettings.globalize_path(path)
if path.begins_with("user://")
else path
)
if DirAccess.make_dir_recursive_absolute(absolute.get_base_dir()) != OK:
return false
var temporary := absolute + ".tmp"
var file := FileAccess.open(temporary, FileAccess.WRITE)
var temporary: String = absolute + ".tmp"
var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
if file == null:
return false
file.store_string(text)
file.flush()
var ok := file.get_error() == OK
var ok: bool = file.get_error() == OK
file.close()
if not ok:
return false
var backup := absolute + ".backup"
var backup: String = absolute + ".backup"
if FileAccess.file_exists(backup):
DirAccess.remove_absolute(backup)
var had_primary := FileAccess.file_exists(absolute)
var had_primary: bool = FileAccess.file_exists(absolute)
if had_primary and DirAccess.rename_absolute(absolute, backup) != OK:
DirAccess.remove_absolute(temporary)
return false

View file

@ -78,10 +78,10 @@ func get_avatar(peer_id: int) -> Player:
return _avatars.get(peer_id)
func set_peer_presentation_visible(peer_id: int, visible: bool) -> void:
func set_peer_presentation_visible(peer_id: int, should_be_visible: bool) -> void:
var avatar: Player = _avatars.get(peer_id)
if avatar != null and is_instance_valid(avatar):
avatar.set_remote_presentation_visible(visible)
avatar.set_remote_presentation_visible(should_be_visible)
func get_peer_ids() -> Array[int]:

View file

@ -24,9 +24,9 @@ static func migrate_legacy_to(
data_root: PlayerDataRoot,
destination: String,
) -> Dictionary:
var normalized := destination.simplify_path().trim_suffix("/")
var normalized: String = destination.simplify_path().trim_suffix("/")
if DirAccess.dir_exists_absolute(normalized):
var existing_manifest := normalized.path_join(
var existing_manifest: String = normalized.path_join(
PlayerDataRoot.MANIFEST_FILENAME
)
if FileAccess.file_exists(existing_manifest):
@ -40,24 +40,24 @@ static func migrate_legacy_to(
"ok": false,
"message": "Choose an empty folder or create a NETFISHING subfolder.",
}
var staging := "%s.migration-%s" % [
var staging: String = "%s.migration-%s" % [
normalized, Crypto.new().generate_random_bytes(8).hex_encode()
]
if DirAccess.make_dir_recursive_absolute(staging) != OK:
return {"ok": false, "message": "Migration staging could not be created."}
var created := data_root.create_unbound_root(staging)
var created: Dictionary = data_root.create_unbound_root(staging)
if not bool(created.get("ok", false)):
_remove_tree(staging)
return {"ok": false, "message": str(created.get("message", ""))}
var copied: Array[String] = []
for source_name: String in LEGACY_FILES:
var source := ProjectSettings.globalize_path(
var source: String = ProjectSettings.globalize_path(
"user://".path_join(source_name)
)
if not FileAccess.file_exists(source):
continue
var target := staging.path_join(LEGACY_FILES[source_name])
var result := _copy_verified_json(source, target)
var target: String = staging.path_join(str(LEGACY_FILES[source_name]))
var result: Dictionary = _copy_verified_json(source, target)
if not bool(result.get("ok", false)):
_remove_tree(staging)
return {
@ -70,11 +70,15 @@ static func migrate_legacy_to(
if DirAccess.rename_absolute(staging, normalized) != OK:
_remove_tree(staging)
return {"ok": false, "message": "Migration could not activate its destination."}
var manifest := _read_json(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME))
var migrated_root_id := str(manifest.get("root_id", ""))
var manifest: Dictionary = _read_json(
normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
)
var migrated_root_id: String = str(manifest.get("root_id", ""))
if migrated_root_id.is_empty() or not data_root.use_existing_root(normalized):
return {"ok": false, "message": "Migration completed but could not switch roots."}
var recovery := ProjectSettings.globalize_path("user://migration-recovery").path_join(
var recovery: String = ProjectSettings.globalize_path(
"user://migration-recovery"
).path_join(
Time.get_datetime_string_from_system().replace(":", "-")
)
DirAccess.make_dir_recursive_absolute(recovery)
@ -94,7 +98,7 @@ static func migrate_active_to(
data_root: PlayerDataRoot,
destination: String,
) -> Dictionary:
var normalized := destination.simplify_path().trim_suffix("/")
var normalized: String = destination.simplify_path().trim_suffix("/")
if normalized == data_root.root_path:
return {"ok": true, "message": "This data folder is already active."}
if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized):
@ -108,12 +112,12 @@ static func migrate_active_to(
),
}
return {"ok": false, "message": "Choose an empty folder or a NETFISHING subfolder."}
var staging := "%s.migration-%s" % [
var staging: String = "%s.migration-%s" % [
normalized, Crypto.new().generate_random_bytes(8).hex_encode()
]
if DirAccess.make_dir_recursive_absolute(staging) != OK:
return {"ok": false, "message": "Migration staging could not be created."}
var created := data_root.create_unbound_root(staging)
var created: Dictionary = data_root.create_unbound_root(staging)
if not bool(created.get("ok", false)):
_remove_tree(staging)
return {"ok": false, "message": str(created.get("message", ""))}
@ -127,7 +131,7 @@ static func migrate_active_to(
"social/server_trust.json",
"social/host_bans.json",
]:
var source := data_root.root_path.path_join(relative)
var source: String = data_root.root_path.path_join(relative)
if not FileAccess.file_exists(source):
continue
if not bool(_copy_verified_json(source, staging.path_join(relative)).get("ok", false)):
@ -138,7 +142,7 @@ static func migrate_active_to(
if DirAccess.rename_absolute(staging, normalized) != OK:
_remove_tree(staging)
return {"ok": false, "message": "Migration could not activate its destination."}
var old_root := data_root.root_path
var old_root: String = data_root.root_path
if not data_root.use_existing_root(normalized):
return {"ok": false, "message": "Migration copied data but did not change the pointer."}
return {
@ -149,18 +153,20 @@ static func migrate_active_to(
static func adopt_legacy_app_data(data_root: PlayerDataRoot) -> Dictionary:
var source_root := ProjectSettings.globalize_path("user://").trim_suffix("/")
var root := ProjectSettings.globalize_path(PlayerDataRoot.APP_DATA_PORTABLE_PATH)
var source_root: String = ProjectSettings.globalize_path("user://").trim_suffix("/")
var root: String = ProjectSettings.globalize_path(
PlayerDataRoot.APP_DATA_PORTABLE_PATH
)
if DirAccess.make_dir_recursive_absolute(root) != OK:
return {"ok": false, "message": "The app-data folder could not be created."}
var created := data_root.create_app_data_layout_for_migration(root)
var created: Dictionary = data_root.create_app_data_layout_for_migration(root)
if not bool(created.get("ok", false)):
return created
for source_name: String in LEGACY_FILES:
var source := source_root.path_join(source_name)
var source: String = source_root.path_join(source_name)
if not FileAccess.file_exists(source):
continue
var target := root.path_join(LEGACY_FILES[source_name])
var target: String = root.path_join(str(LEGACY_FILES[source_name]))
if not bool(_copy_verified_json(source, target).get("ok", false)):
return {
"ok": false,
@ -179,17 +185,19 @@ static func replace_existing_with_legacy(
data_root: PlayerDataRoot,
destination: String,
) -> Dictionary:
var normalized := destination.simplify_path().trim_suffix("/")
var manifest := normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
var normalized: String = destination.simplify_path().trim_suffix("/")
var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)
if not FileAccess.file_exists(manifest):
return {"ok": false, "message": "The selected folder is not a NETFISHING data root."}
var recovery := ProjectSettings.globalize_path("user://migration-recovery").path_join(
var recovery: String = ProjectSettings.globalize_path(
"user://migration-recovery"
).path_join(
"replaced-root-" + Time.get_datetime_string_from_system().replace(":", "-")
)
if not _copy_tree(normalized, recovery):
return {"ok": false, "message": "The selected data could not be backed up."}
_remove_tree(normalized)
var result := migrate_legacy_to(data_root, normalized)
var result: Dictionary = migrate_legacy_to(data_root, normalized)
if bool(result.get("ok", false)):
result["replaced_root_backup"] = recovery
return result
@ -199,26 +207,28 @@ static func replace_existing_with_active(
data_root: PlayerDataRoot,
destination: String,
) -> Dictionary:
var normalized := destination.simplify_path().trim_suffix("/")
var normalized: String = destination.simplify_path().trim_suffix("/")
if not FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)):
return {"ok": false, "message": "The selected folder is not a NETFISHING data root."}
var recovery := ProjectSettings.globalize_path("user://migration-recovery").path_join(
var recovery: String = ProjectSettings.globalize_path(
"user://migration-recovery"
).path_join(
"replaced-root-" + Time.get_datetime_string_from_system().replace(":", "-")
)
if not _copy_tree(normalized, recovery):
return {"ok": false, "message": "The selected data could not be backed up."}
_remove_tree(normalized)
var result := migrate_active_to(data_root, normalized)
var result: Dictionary = migrate_active_to(data_root, normalized)
if bool(result.get("ok", false)):
result["replaced_root_backup"] = recovery
return result
static func _copy_verified_json(source: String, destination: String) -> Dictionary:
var bytes := PortableFileGuard.read_bytes(source)
var bytes: PackedByteArray = PortableFileGuard.read_bytes(source)
if bytes.is_empty() and FileAccess.get_open_error() != OK:
return {"ok": false}
var json := JSON.new()
var json: JSON = JSON.new()
if (
json.parse(bytes.get_string_from_utf8()) != OK
or typeof(json.data) != TYPE_DICTIONARY
@ -229,7 +239,7 @@ static func _copy_verified_json(source: String, destination: String) -> Dictiona
return {"ok": false}
if not _write_bytes(destination, bytes):
return {"ok": false}
var copied := PortableFileGuard.read_bytes(destination)
var copied: PackedByteArray = PortableFileGuard.read_bytes(destination)
return {
"ok": (
PortableFileGuard.hash_bytes(copied)
@ -240,7 +250,7 @@ static func _copy_verified_json(source: String, destination: String) -> Dictiona
static func _valid_owned_data(filename: String, data: Dictionary) -> bool:
if filename == "player_save.json":
var version := int(data.get("save_version", -1))
var version: int = int(data.get("save_version", -1))
return version >= 1 and version <= PlayerSaveManager.SAVE_VERSION
return (
filename not in LEGACY_FILES
@ -255,14 +265,14 @@ static func _copy_bytes(source: String, destination: String) -> bool:
static func _copy_tree(source: String, destination: String) -> bool:
if DirAccess.make_dir_recursive_absolute(destination) != OK:
return false
var access := DirAccess.open(source)
var access: DirAccess = DirAccess.open(source)
if access == null:
return false
access.list_dir_begin()
var name := access.get_next()
var name: String = access.get_next()
while not name.is_empty():
var from := source.path_join(name)
var to := destination.path_join(name)
var from: String = source.path_join(name)
var to: String = destination.path_join(name)
if access.current_is_dir():
if not _copy_tree(from, to):
access.list_dir_end()
@ -277,19 +287,19 @@ static func _copy_tree(source: String, destination: String) -> bool:
static func _write_bytes(path: String, bytes: PackedByteArray) -> bool:
DirAccess.make_dir_recursive_absolute(path.get_base_dir())
var file := FileAccess.open(path, FileAccess.WRITE)
var file: FileAccess = FileAccess.open(path, FileAccess.WRITE)
if file == null:
return false
file.store_buffer(bytes)
file.flush()
var ok := file.get_error() == OK
var ok: bool = file.get_error() == OK
file.close()
return ok
static func _read_json(path: String) -> Dictionary:
var bytes := PortableFileGuard.read_bytes(path)
var json := JSON.new()
var bytes: PackedByteArray = PortableFileGuard.read_bytes(path)
var json: JSON = JSON.new()
return (
json.data
if json.parse(bytes.get_string_from_utf8()) == OK
@ -299,23 +309,23 @@ static func _read_json(path: String) -> Dictionary:
static func _directory_empty(path: String) -> bool:
var access := DirAccess.open(path)
var access: DirAccess = DirAccess.open(path)
if access == null:
return true
access.list_dir_begin()
var name := access.get_next()
var name: String = access.get_next()
access.list_dir_end()
return name.is_empty()
static func _remove_tree(path: String) -> void:
var access := DirAccess.open(path)
var access: DirAccess = DirAccess.open(path)
if access == null:
return
access.list_dir_begin()
var name := access.get_next()
var name: String = access.get_next()
while not name.is_empty():
var child := path.path_join(name)
var child: String = path.path_join(name)
if access.current_is_dir():
_remove_tree(child)
else:

View file

@ -7,19 +7,21 @@ const MAX_PORTABLE_FILE_BYTES := 16 * 1024 * 1024
static func hash_file(path: String) -> String:
if not FileAccess.file_exists(path):
return ""
var file := FileAccess.open(path, FileAccess.READ)
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
if file == null or file.get_length() > MAX_PORTABLE_FILE_BYTES:
return ""
var bytes := file.get_buffer(file.get_length())
var bytes: PackedByteArray = file.get_buffer(file.get_length())
file.close()
return hash_bytes(bytes)
static func hash_bytes(bytes: PackedByteArray) -> String:
var context := HashingContext.new()
if context.start(HashingContext.HASH_SHA256) != OK:
var context: HashingContext = HashingContext.new()
var start_error: Error = context.start(HashingContext.HASH_SHA256)
if start_error != OK:
return ""
if context.update(bytes) != OK:
var update_error: Error = context.update(bytes)
if update_error != OK:
return ""
return context.finish().hex_encode()
@ -27,10 +29,10 @@ static func hash_bytes(bytes: PackedByteArray) -> String:
static func read_bytes(path: String, maximum_bytes: int = MAX_PORTABLE_FILE_BYTES) -> PackedByteArray:
if not FileAccess.file_exists(path):
return PackedByteArray()
var file := FileAccess.open(path, FileAccess.READ)
var file: FileAccess = FileAccess.open(path, FileAccess.READ)
if file == null or file.get_length() > maximum_bytes:
return PackedByteArray()
var bytes := file.get_buffer(file.get_length())
var bytes: PackedByteArray = file.get_buffer(file.get_length())
file.close()
return bytes
@ -42,10 +44,10 @@ static func write_guarded(
conflict_directory: String,
device_id: String,
) -> Dictionary:
var current_exists := FileAccess.file_exists(path)
var current_hash := hash_file(path) if current_exists else ""
var current_exists: bool = FileAccess.file_exists(path)
var current_hash: String = hash_file(path) if current_exists else ""
if current_hash != expected_hash:
var conflict_path := _write_conflict_copy(
var conflict_path: String = _write_conflict_copy(
path, bytes, conflict_directory, device_id
)
return {
@ -60,14 +62,14 @@ static func write_guarded(
}
if not _ensure_parent(path):
return {"ok": false, "conflict": false, "hash": expected_hash}
var temporary := path + ".tmp"
var backup := path + ".backup"
var file := FileAccess.open(temporary, FileAccess.WRITE)
var temporary: String = path + ".tmp"
var backup: String = path + ".backup"
var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE)
if file == null:
return {"ok": false, "conflict": false, "hash": expected_hash}
file.store_buffer(bytes)
file.flush()
var write_error := file.get_error()
var write_error: Error = file.get_error()
file.close()
if write_error != OK:
_remove(temporary)
@ -90,13 +92,13 @@ static func write_guarded(
static func has_syncthing_conflict(directory: String) -> bool:
var access := DirAccess.open(directory)
var access: DirAccess = DirAccess.open(directory)
if access == null:
return false
access.list_dir_begin()
var name := access.get_next()
var name: String = access.get_next()
while not name.is_empty():
var lower := name.to_lower()
var lower: String = name.to_lower()
if "sync-conflict" in lower or ".syncthing." in lower:
access.list_dir_end()
return true
@ -113,24 +115,24 @@ static func _write_conflict_copy(
) -> String:
if DirAccess.make_dir_recursive_absolute(conflict_directory) != OK:
return ""
var filename := canonical_path.get_file()
var safe_device := device_id.left(16)
var timestamp := Time.get_datetime_string_from_system().replace(":", "-")
var destination := conflict_directory.path_join(
var filename: String = canonical_path.get_file()
var safe_device: String = device_id.left(16)
var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
var destination: String = conflict_directory.path_join(
"%s.local-%s-%s" % [filename, safe_device, timestamp]
)
var file := FileAccess.open(destination, FileAccess.WRITE)
var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE)
if file == null:
return ""
file.store_buffer(bytes)
file.flush()
var ok := file.get_error() == OK
var ok: bool = file.get_error() == OK
file.close()
return destination if ok else ""
static func _ensure_parent(path: String) -> bool:
var parent := path.get_base_dir()
var parent: String = path.get_base_dir()
return (
DirAccess.dir_exists_absolute(parent)
or DirAccess.make_dir_recursive_absolute(parent) == OK

View file

@ -323,9 +323,12 @@ func _validate_entry(value: Dictionary, is_saved: bool) -> Dictionary:
var endpoint := EndpointParser.parse(endpoint_text)
if not endpoint.is_valid():
return {}
var name: String = str(value.get("display_name", "")).strip_edges()
var display_name: String = str(
value.get("display_name", "")
).strip_edges()
if is_saved and (
name.is_empty() or name.length() > MAX_DISPLAY_NAME_LENGTH
display_name.is_empty()
or display_name.length() > MAX_DISPLAY_NAME_LENGTH
):
return {}
var result: Dictionary = _make_entry(