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

@ -216,12 +216,12 @@ func show_bite() -> void:
_bite_tween.finished.connect(_on_bite_tween_finished) _bite_tween.finished.connect(_on_bite_tween_finished)
func show_withdrawal_position(position: Vector3) -> void: func show_withdrawal_position(world_position: Vector3) -> void:
if _mode != VisualMode.FISHING: if _mode != VisualMode.FISHING:
return return
_kill_active_tween() _kill_active_tween()
_bobber.global_position = _with_water_height(position) _bobber.global_position = _with_water_height(world_position)
func begin_reeling() -> void: func begin_reeling() -> void:
@ -232,12 +232,12 @@ func begin_reeling() -> void:
_cast_position = _with_water_height(_bobber.global_position) _cast_position = _with_water_height(_bobber.global_position)
func show_reel_position(position: Vector3, _input_held: bool) -> void: func show_reel_position(world_position: Vector3, _input_held: bool) -> void:
if _mode != VisualMode.FISHING: if _mode != VisualMode.FISHING:
return return
_kill_active_tween() _kill_active_tween()
_bobber.global_position = _with_water_height(position) _bobber.global_position = _with_water_height(world_position)
func play_outcome(outcome: StringName) -> void: func play_outcome(outcome: StringName) -> void:
@ -371,8 +371,12 @@ func _calculate_pickup_position(target: Vector3) -> Vector3:
return player_on_water + outward * pickup_distance return player_on_water + outward * pickup_distance
func _with_water_height(position: Vector3) -> Vector3: func _with_water_height(world_position: Vector3) -> Vector3:
return Vector3(position.x, get_water_surface_height(), position.z) return Vector3(
world_position.x,
get_water_surface_height(),
world_position.z,
)
func _update_line(delta: float) -> void: func _update_line(delta: float) -> void:

View file

@ -1082,7 +1082,7 @@ func get_fishable_water_region(
query, query,
32 32
) )
var selected_region: FishableWaterRegionType var selected_region: FishableWaterRegionType = null
for result: Dictionary in results: for result: Dictionary in results:
var collider: Object = result.get("collider") var collider: Object = result.get("collider")
var region: FishableWaterRegionType = collider as FishableWaterRegionType var region: FishableWaterRegionType = collider as FishableWaterRegionType

View file

@ -9,8 +9,8 @@ var _target: Vector3
var _active: bool = false var _active: bool = false
func setup(owner: Player) -> void: func setup(owning_player: Player) -> void:
_owner = owner _owner = owning_player
_bobber = MeshInstance3D.new() _bobber = MeshInstance3D.new()
var bobber_mesh := SphereMesh.new() var bobber_mesh := SphereMesh.new()
bobber_mesh.radius = 0.12 bobber_mesh.radius = 0.12
@ -43,11 +43,11 @@ func show_cast(target: Vector3) -> void:
_redraw_line() _redraw_line()
func update_bobber(position: Vector3) -> void: func update_bobber(world_position: Vector3) -> void:
if not _active or not position.is_finite(): if not _active or not world_position.is_finite():
return return
_target = position _target = world_position
_bobber.global_position = position _bobber.global_position = world_position
_redraw_line() _redraw_line()

View file

@ -88,6 +88,7 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0
@onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery
@onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager
@onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager
@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController
@onready var _title_music: AudioStreamPlayer = %TitleMusic @onready var _title_music: AudioStreamPlayer = %TitleMusic
@onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation @onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation
@onready var _pixelation_reset: PixelationResetOverlayType = ( @onready var _pixelation_reset: PixelationResetOverlayType = (
@ -146,11 +147,21 @@ var _pending_trust_changed: bool = false
var _identity_notice_dialog: AcceptDialog var _identity_notice_dialog: AcceptDialog
var _data_setup_dialog: ConfirmationDialog var _data_setup_dialog: ConfirmationDialog
var _data_folder_dialog: FileDialog var _data_folder_dialog: FileDialog
var _data_folder_picker_generation: int = 0
var _restore_data_setup_after_picker: bool = false
var _application_initialized := false var _application_initialized := false
var _pending_existing_root_path := "" var _pending_existing_root_path := ""
func _ready() -> void: func _ready() -> void:
if not _settings_manager.settings_changed.is_connected(
_apply_runtime_settings
):
_settings_manager.settings_changed.connect(_apply_runtime_settings)
_settings_manager.load_settings()
_interface_fonts.set_readable_font_enabled(
_settings_manager.current_settings.use_readable_interface_font
)
if _data_root.resolve(): if _data_root.resolve():
_configure_portable_stores() _configure_portable_stores()
_initialize_after_data_root() _initialize_after_data_root()
@ -377,6 +388,7 @@ func _initialize_after_data_root() -> void:
_player_identity, _player_identity,
_host_identity, _host_identity,
_network_session, _network_session,
_interface_fonts,
) )
_data_root.conflict_detected.connect(_on_portable_conflict) _data_root.conflict_detected.connect(_on_portable_conflict)
_data_root.status_changed.connect(_on_data_root_status) _data_root.status_changed.connect(_on_data_root_status)
@ -400,10 +412,6 @@ func _initialize_after_data_root() -> void:
_test_world.get_player_water_triggers(), _test_world.get_player_water_triggers(),
_test_world.get_safe_respawn_points() _test_world.get_safe_respawn_points()
) )
if not _settings_manager.settings_changed.is_connected(
_apply_runtime_settings
):
_settings_manager.settings_changed.connect(_apply_runtime_settings)
_ui_pixelation.effective_pixel_size_changed.connect( _ui_pixelation.effective_pixel_size_changed.connect(
_on_effective_ui_pixel_size_changed _on_effective_ui_pixel_size_changed
) )
@ -420,7 +428,6 @@ func _initialize_after_data_root() -> void:
_game_ui.focus_open_settings_back_button _game_ui.focus_open_settings_back_button
) )
_pixelation_reset.reset_requested.connect(_reset_pixelation) _pixelation_reset.reset_requested.connect(_reset_pixelation)
_settings_manager.load_settings()
_apply_runtime_settings(_settings_manager.current_settings) _apply_runtime_settings(_settings_manager.current_settings)
_set_gameplay_active(false) _set_gameplay_active(false)
var title_screen: TitleScreenType = _game_ui.get_title_screen() var title_screen: TitleScreenType = _game_ui.get_title_screen()
@ -491,9 +498,10 @@ func _show_data_root_setup() -> void:
"Keep Current Location", false, "current" "Keep Current Location", false, "current"
) )
_data_setup_dialog.custom_action.connect(_on_data_setup_action) _data_setup_dialog.custom_action.connect(_on_data_setup_action)
_interface_fonts.apply_utility_theme(_data_setup_dialog)
add_child(_data_setup_dialog) add_child(_data_setup_dialog)
var default_path := _data_root.default_visible_path() var default_path: String = _data_root.default_visible_path()
var legacy := PortableDataMigration.legacy_files_present() var legacy: bool = PortableDataMigration.legacy_files_present()
_data_setup_dialog.dialog_text = ( _data_setup_dialog.dialog_text = (
("Move your existing NETFISHING data to an easy-to-find folder." ("Move your existing NETFISHING data to an easy-to-find folder."
if legacy else "Choose where NETFISHING stores your player data.") if legacy else "Choose where NETFISHING stores your player data.")
@ -512,7 +520,7 @@ func _show_data_root_setup() -> void:
func _use_default_data_root() -> void: func _use_default_data_root() -> void:
var path := _data_root.default_visible_path() var path: String = _data_root.default_visible_path()
if path.is_empty(): if path.is_empty():
_show_folder_picker() _show_folder_picker()
return return
@ -535,7 +543,29 @@ func _show_folder_picker() -> void:
_data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM _data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM
_data_folder_dialog.use_native_dialog = false _data_folder_dialog.use_native_dialog = false
_data_folder_dialog.dir_selected.connect(_activate_selected_data_path) _data_folder_dialog.dir_selected.connect(_activate_selected_data_path)
_data_folder_dialog.canceled.connect(_on_data_folder_picker_canceled)
_interface_fonts.apply_utility_theme(_data_folder_dialog)
add_child(_data_folder_dialog) add_child(_data_folder_dialog)
if _data_folder_dialog.visible:
return
_data_folder_picker_generation += 1
var generation: int = _data_folder_picker_generation
_restore_data_setup_after_picker = (
_data_setup_dialog != null and _data_setup_dialog.visible
)
if _data_setup_dialog != null:
_data_setup_dialog.hide()
_open_folder_picker_after_modal.call_deferred(generation)
func _open_folder_picker_after_modal(generation: int) -> void:
await get_tree().process_frame
if (
generation != _data_folder_picker_generation
or _data_folder_dialog == null
or _data_folder_dialog.visible
):
return
_data_folder_dialog.current_dir = ( _data_folder_dialog.current_dir = (
_data_root.default_visible_path().get_base_dir() _data_root.default_visible_path().get_base_dir()
if not _data_root.default_visible_path().is_empty() if not _data_root.default_visible_path().is_empty()
@ -544,10 +574,29 @@ func _show_folder_picker() -> void:
_data_folder_dialog.popup_centered_ratio(0.75) _data_folder_dialog.popup_centered_ratio(0.75)
func _on_data_folder_picker_canceled() -> void:
_data_folder_picker_generation += 1
var generation: int = _data_folder_picker_generation
var should_restore_setup: bool = _restore_data_setup_after_picker
_restore_data_setup_after_picker = false
if _data_folder_dialog != null:
_data_folder_dialog.hide()
if should_restore_setup:
_restore_data_setup_after_picker_after_modal.call_deferred(generation)
func _restore_data_setup_after_picker_after_modal(generation: int) -> void:
await get_tree().process_frame
if generation != _data_folder_picker_generation:
return
_show_data_root_setup()
func _activate_selected_data_path(path: String, app_data: bool = false) -> void: func _activate_selected_data_path(path: String, app_data: bool = false) -> void:
var ok := false _restore_data_setup_after_picker = false
var ok: bool = false
if PortableDataMigration.legacy_files_present(): if PortableDataMigration.legacy_files_present():
var result := ( var result: Dictionary = (
PortableDataMigration.adopt_legacy_app_data(_data_root) PortableDataMigration.adopt_legacy_app_data(_data_root)
if app_data if app_data
else PortableDataMigration.migrate_legacy_to(_data_root, path) else PortableDataMigration.migrate_legacy_to(_data_root, path)
@ -571,7 +620,7 @@ func _activate_selected_data_path(path: String, app_data: bool = false) -> void:
func _show_existing_root_choice(path: String) -> void: func _show_existing_root_choice(path: String) -> void:
_pending_existing_root_path = path _pending_existing_root_path = path
var dialog := ConfirmationDialog.new() var dialog: ConfirmationDialog = ConfirmationDialog.new()
dialog.title = "Existing NETFISHING data" dialog.title = "Existing NETFISHING data"
dialog.ok_button_text = "Use Data Already in Selected Folder" dialog.ok_button_text = "Use Data Already in Selected Folder"
dialog.cancel_button_text = "Cancel" dialog.cancel_button_text = "Cancel"
@ -594,7 +643,7 @@ func _show_existing_root_choice(path: String) -> void:
dialog.custom_action.connect(func(action: StringName) -> void: dialog.custom_action.connect(func(action: StringName) -> void:
if action != &"replace": if action != &"replace":
return return
var result := PortableDataMigration.replace_existing_with_legacy( var result: Dictionary = PortableDataMigration.replace_existing_with_legacy(
_data_root, _pending_existing_root_path _data_root, _pending_existing_root_path
) )
if bool(result.get("ok", false)): if bool(result.get("ok", false)):
@ -605,6 +654,7 @@ func _show_existing_root_choice(path: String) -> void:
else: else:
_show_data_error(str(result.get("message", "Migration failed."))) _show_data_error(str(result.get("message", "Migration failed.")))
) )
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog) add_child(dialog)
dialog.popup_centered(Vector2i(680, 360)) dialog.popup_centered(Vector2i(680, 360))
@ -616,7 +666,8 @@ func _show_data_error(message: String) -> void:
func _on_portable_conflict(message: String, _path: String) -> void: func _on_portable_conflict(message: String, _path: String) -> void:
var dialog := AcceptDialog.new() var dialog: AcceptDialog = AcceptDialog.new()
_interface_fonts.apply_utility_theme(dialog)
dialog.title = "Player data conflict" dialog.title = "Player data conflict"
dialog.dialog_text = ( dialog.dialog_text = (
message message
@ -635,7 +686,8 @@ func _on_portable_conflict(message: String, _path: String) -> void:
func _on_data_root_status(message: String) -> void: func _on_data_root_status(message: String) -> void:
if "Syncthing conflict" not in message: if "Syncthing conflict" not in message:
return return
var dialog := AcceptDialog.new() var dialog: AcceptDialog = AcceptDialog.new()
_interface_fonts.apply_utility_theme(dialog)
dialog.title = "Synced data needs review" dialog.title = "Synced data needs review"
dialog.dialog_text = message dialog.dialog_text = message
dialog.add_button("Open Data Folder", false, "open") dialog.add_button("Open Data Folder", false, "open")
@ -771,6 +823,9 @@ func _process(_delta: float) -> void:
func _apply_runtime_settings(settings: PlayerSettingsType) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void:
if settings == null: if settings == null:
return return
_interface_fonts.set_readable_font_enabled(
settings.use_readable_interface_font
)
_apply_world_pixelation(settings.world_pixel_size) _apply_world_pixelation(settings.world_pixel_size)
_ui_pixelation.set_pixel_size(settings.ui_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size)
_game_ui.get_title_screen().set_world_pixelation( _game_ui.get_title_screen().set_world_pixelation(

View file

@ -1,4 +1,4 @@
[gd_scene load_steps=39 format=3] [gd_scene load_steps=40 format=3]
[ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"] [ext_resource type="PackedScene" path="res://world/test_world.tscn" id="1_world"]
[ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"] [ext_resource type="PackedScene" path="res://player/player.tscn" id="2_player"]
@ -38,6 +38,7 @@
[ext_resource type="Script" path="res://network/network_player_list_service.gd" id="36_player_list"] [ext_resource type="Script" path="res://network/network_player_list_service.gd" id="36_player_list"]
[ext_resource type="Script" path="res://network/player_data_root.gd" id="37_data_root"] [ext_resource type="Script" path="res://network/player_data_root.gd" id="37_data_root"]
[ext_resource type="Script" path="res://network/identity_backup_service.gd" id="38_identity_backup"] [ext_resource type="Script" path="res://network/identity_backup_service.gd" id="38_identity_backup"]
[ext_resource type="Script" path="res://ui/interface_font_controller.gd" id="39_fonts"]
[node name="Main" type="Node3D"] [node name="Main" type="Node3D"]
script = ExtResource("3_main") script = ExtResource("3_main")
@ -46,6 +47,10 @@ pelican_buyer_profile = ExtResource("7_pelicans")
main_shop_buyer_profile = ExtResource("12_main_shop") main_shop_buyer_profile = ExtResource("12_main_shop")
item_catalog = ExtResource("11_items") item_catalog = ExtResource("11_items")
[node name="InterfaceFontController" type="Node" parent="."]
unique_name_in_owner = true
script = ExtResource("39_fonts")
[node name="PlayerDataRoot" type="Node" parent="."] [node name="PlayerDataRoot" type="Node" parent="."]
unique_name_in_owner = true unique_name_in_owner = true
script = ExtResource("37_data_root") script = ExtResource("37_data_root")

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) 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 ( if (
not NetworkIdentityCrypto.valid_fingerprint(host_fingerprint) not NetworkIdentityCrypto.valid_fingerprint(host_fingerprint)
or not NetworkIdentityCrypto.valid_fingerprint(target_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 return false
_ensure_loaded() _ensure_loaded()
@ -41,7 +45,7 @@ func ban(host_fingerprint: String, target_fingerprint: String, name: String) ->
records[target_fingerprint] = { records[target_fingerprint] = {
"host_fingerprint": host_fingerprint, "host_fingerprint": host_fingerprint,
"target_fingerprint": target_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()), "banned_unix": int(Time.get_unix_time_from_system()),
"reason": "host_ban", "reason": "host_ban",
} }

View file

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

View file

@ -245,10 +245,10 @@ func _on_peer_removed(peer_id: int) -> void:
_rate_times.erase(peer_id) _rate_times.erase(peer_id)
if not _session.is_host(): if not _session.is_host():
return 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) _peer_names.erase(peer_id)
_broadcast(_make_message( _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() var normalized := str(choice["name"]).to_lower()
counts[normalized] = int(counts.get(normalized, 0)) + 1 counts[normalized] = int(counts.get(normalized, 0)) + 1
for choice: Dictionary in choices: for choice: Dictionary in choices:
var name: String = choice["name"] var display_name: String = choice["name"]
if int(counts.get(name.to_lower(), 0)) > 1: if int(counts.get(display_name.to_lower(), 0)) > 1:
choice["label"] = "%s · %s" % [ choice["label"] = "%s · %s" % [
name, display_name,
NetworkIdentityCrypto.compact_suffix(choice["fingerprint"]), NetworkIdentityCrypto.compact_suffix(choice["fingerprint"]),
] ]
else: else:
choice["label"] = name choice["label"] = display_name
return choices return choices
@ -531,8 +531,8 @@ func prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
func _prepare_recipient(transfer_id: String, letter: Dictionary) -> void: func _prepare_recipient(transfer_id: String, letter: Dictionary) -> void:
var ready := _can_receive(letter["attachment"]) var recipient_ready: bool = _can_receive(letter["attachment"])
_send_phase_ack("recipient_prepared", transfer_id, ready) _send_phase_ack("recipient_prepared", transfer_id, recipient_ready)
func _request_sender_commit(peer_id: int, transfer_id: String, letter: Dictionary) -> void: 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 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): if not _valid_moderation_target(peer_id, fingerprint, revision):
moderation_finished.emit(false, "That player is no longer connected.") moderation_finished.emit(false, "That player is no longer connected.")
return false return false
var host_fingerprint := _session.get_host_identity_fingerprint() 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.") moderation_finished.emit(false, "Ban could not be saved.")
return false return false
var ok := _session.kick_authenticated_peer(peer_id, fingerprint, true) 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: func _process_apply_request(peer_id: int, data: Dictionary) -> void:
var name := str(data["display_name"]).strip_edges() var display_name: String = str(data["display_name"]).strip_edges()
var conflict := _name_conflicts(peer_id, name) var conflict: bool = _name_conflicts(peer_id, display_name)
var accepted := not conflict or bool(data["use_anyway"]) var accepted: bool = not conflict or bool(data["use_anyway"])
var suggestions := _make_suggestions(peer_id, name) if conflict else PackedStringArray() var suggestions: PackedStringArray = (
_make_suggestions(peer_id, display_name)
if conflict
else PackedStringArray()
)
if peer_id == _session.get_local_peer_id(): if peer_id == _session.get_local_peer_id():
_apply_local_result( _apply_local_result(
str(data["request_id"]), accepted, "", conflict, suggestions str(data["request_id"]), accepted, "", conflict, suggestions
@ -184,7 +188,7 @@ func _process_apply_request(peer_id: int, data: Dictionary) -> void:
if accepted: if accepted:
_host_pending_apply[str(data["request_id"])] = { _host_pending_apply[str(data["request_id"])] = {
"peer_id": peer_id, "peer_id": peer_id,
"display_name": name, "display_name": display_name,
"appearance": Dictionary(data["appearance"]).duplicate(true), "appearance": Dictionary(data["appearance"]).duplicate(true),
"authorization": { "authorization": {
"request_id": data["request_id"], "request_id": data["request_id"],
@ -214,10 +218,10 @@ func confirm_profile_saved(request_id: String) -> void:
): ):
return return
_host_pending_apply.erase(request_id) _host_pending_apply.erase(request_id)
var name := str(pending["display_name"]) var display_name: String = str(pending["display_name"])
var appearance := Dictionary(pending["appearance"]) var appearance: Dictionary = Dictionary(pending["appearance"])
_session.apply_canonical_profile(sender_id, name, appearance) _session.apply_canonical_profile(sender_id, display_name, appearance)
var record := _session.get_peer_record(sender_id) var record: PeerRegistry.PeerRecord = _session.get_peer_record(sender_id)
if record != null: if record != null:
record.profile_authorization = pending.get("authorization", {}).duplicate(true) record.profile_authorization = pending.get("authorization", {}).duplicate(true)
_apply_to_avatar(sender_id, appearance) _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) _registry.update_appearance(peer_id, appearance)
var transform: Transform3D = _spawn_service.get_spawn_transform_for_index(0) var transform: Transform3D = _spawn_service.get_spawn_transform_for_index(0)
var position: Array = entry["position"] var spawn_position: Array = entry["position"]
if position.size() != 3: if spawn_position.size() != 3:
return return
transform.origin = Vector3( transform.origin = Vector3(
float(position[0]), float(spawn_position[0]),
float(position[1]), float(spawn_position[1]),
float(position[2]) float(spawn_position[2])
) )
transform.basis = Basis(Vector3.UP, float(entry["yaw"])) transform.basis = Basis(Vector3.UP, float(entry["yaw"]))
var avatar := _spawn_service.spawn_remote_player(peer_id, transform, false) var avatar := _spawn_service.spawn_remote_player(peer_id, transform, false)

View file

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

View file

@ -78,10 +78,10 @@ func get_avatar(peer_id: int) -> Player:
return _avatars.get(peer_id) 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) var avatar: Player = _avatars.get(peer_id)
if avatar != null and is_instance_valid(avatar): 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]: func get_peer_ids() -> Array[int]:

View file

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

View file

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

View file

@ -425,19 +425,19 @@ func apply_network_teleport(snapshot: Dictionary) -> void:
func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary: func _parse_network_snapshot(snapshot: Dictionary) -> Dictionary:
var position: Variant = snapshot.get("position") var snapshot_position: Variant = snapshot.get("position")
var network_velocity: Variant = snapshot.get("velocity") var network_velocity: Variant = snapshot.get("velocity")
if ( if (
typeof(position) != TYPE_ARRAY typeof(snapshot_position) != TYPE_ARRAY
or typeof(network_velocity) != TYPE_ARRAY or typeof(network_velocity) != TYPE_ARRAY
or position.size() != 3 or snapshot_position.size() != 3
or network_velocity.size() != 3 or network_velocity.size() != 3
): ):
return {} return {}
var parsed_position := Vector3( var parsed_position := Vector3(
float(position[0]), float(snapshot_position[0]),
float(position[1]), float(snapshot_position[1]),
float(position[2]) float(snapshot_position[2])
) )
var parsed_velocity := Vector3( var parsed_velocity := Vector3(
float(network_velocity[0]), float(network_velocity[0]),

View file

@ -20,6 +20,7 @@ const UI_DESKTOP_RENDER_HEIGHTS: Array[int] = [0, 612, 504, 396, 288]
const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192] const UI_COMPACT_RENDER_HEIGHTS: Array[int] = [0, 408, 336, 264, 192]
@export var auto_click_enabled: bool = false @export var auto_click_enabled: bool = false
@export var use_readable_interface_font: bool = false
@export_range(0.10, 0.50, 0.01) var auto_click_interval: float = 0.20 @export_range(0.10, 0.50, 0.01) var auto_click_interval: float = 0.20
@export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005 @export_range(0.001, 0.012, 0.0005) var mouse_camera_sensitivity: float = 0.005
@export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5 @export_range(0.5, 5.0, 0.1) var controller_camera_sensitivity: float = 2.5
@ -49,6 +50,7 @@ func is_valid() -> bool:
func copy() -> PlayerSettings: func copy() -> PlayerSettings:
var result := PlayerSettings.new() var result := PlayerSettings.new()
result.auto_click_enabled = auto_click_enabled result.auto_click_enabled = auto_click_enabled
result.use_readable_interface_font = use_readable_interface_font
result.auto_click_interval = auto_click_interval result.auto_click_interval = auto_click_interval
result.mouse_camera_sensitivity = mouse_camera_sensitivity result.mouse_camera_sensitivity = mouse_camera_sensitivity
result.controller_camera_sensitivity = controller_camera_sensitivity result.controller_camera_sensitivity = controller_camera_sensitivity

View file

@ -45,10 +45,17 @@ func load_settings() -> bool:
if ( if (
typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL typeof(accessibility.get("auto_click_enabled")) != TYPE_BOOL
or typeof(camera.get("invert_vertical")) != TYPE_BOOL or typeof(camera.get("invert_vertical")) != TYPE_BOOL
or (
accessibility.has("use_readable_interface_font")
and typeof(accessibility["use_readable_interface_font"]) != TYPE_BOOL
)
): ):
return _use_defaults_after_corruption("Player settings values are invalid.") return _use_defaults_after_corruption("Player settings values are invalid.")
var loaded := PlayerSettings.new() var loaded := PlayerSettings.new()
loaded.auto_click_enabled = accessibility["auto_click_enabled"] loaded.auto_click_enabled = accessibility["auto_click_enabled"]
loaded.use_readable_interface_font = bool(
accessibility.get("use_readable_interface_font", false)
)
loaded.auto_click_interval = _read_float( loaded.auto_click_interval = _read_float(
accessibility.get("auto_click_interval"), accessibility.get("auto_click_interval"),
-1.0 -1.0
@ -124,6 +131,9 @@ func save_now() -> bool:
"accessibility": { "accessibility": {
"auto_click_enabled": current_settings.auto_click_enabled, "auto_click_enabled": current_settings.auto_click_enabled,
"auto_click_interval": current_settings.auto_click_interval, "auto_click_interval": current_settings.auto_click_interval,
"use_readable_interface_font": (
current_settings.use_readable_interface_font
),
}, },
"camera": { "camera": {
"mouse_sensitivity": current_settings.mouse_camera_sensitivity, "mouse_sensitivity": current_settings.mouse_camera_sensitivity,

View file

@ -0,0 +1,11 @@
We, the copyright holders of this work, hereby release it into the
public domain. This applies worldwide.
In case this is not legally possible,
We grant any entity the right to use this work for any purpose, without
any conditions, unless such conditions are required by law.
Thatcher Ulrich <tu@tulrich.com> http://tulrich.com
Karoly Barta bartakarcsi@gmail.com
Michael Evans http://www.evertype.com

BIN
ui/fonts/Tuffy_Bold.otf Normal file

Binary file not shown.

View file

@ -0,0 +1,36 @@
[remap]
importer="font_data_dynamic"
type="FontFile"
uid="uid://xhdms1j01k2q"
path="res://.godot/imported/Tuffy_Bold.otf-1ed903e9af3906035b0cf09ea141ce25.fontdata"
[deps]
source_file="res://ui/fonts/Tuffy_Bold.otf"
dest_files=["res://.godot/imported/Tuffy_Bold.otf-1ed903e9af3906035b0cf09ea141ce25.fontdata"]
[params]
Rendering=null
antialiasing=1
generate_mipmaps=false
disable_embedded_bitmaps=true
multichannel_signed_distance_field=false
msdf_pixel_range=8
msdf_size=48
allow_system_fallback=true
force_autohinter=false
modulate_color_glyphs=false
hinting=3
subpixel_positioning=4
keep_rounding_remainders=true
oversampling=0.0
Fallbacks=null
fallbacks=[]
Compress=null
compress=true
preload=[]
language_support={}
script_support={}
opentype_features={}

View file

@ -184,6 +184,7 @@ func setup_data_and_identity(
player_identity: PlayerIdentityStore, player_identity: PlayerIdentityStore,
host_identity: HostIdentityStore, host_identity: HostIdentityStore,
network_session: NetworkSession, network_session: NetworkSession,
interface_fonts: InterfaceFontController,
) -> void: ) -> void:
for panel: SettingsPanelType in [ for panel: SettingsPanelType in [
_title_settings_panel, _pause_settings_panel _title_settings_panel, _pause_settings_panel
@ -194,6 +195,7 @@ func setup_data_and_identity(
player_identity, player_identity,
host_identity, host_identity,
network_session, network_session,
interface_fonts,
) )
@ -219,7 +221,7 @@ func _process(_delta: float) -> void:
}.get(item_id, "") }.get(item_id, "")
parts.append( parts.append(
"%s %d:%02d" "%s %d:%02d"
% [label, int(remaining) / 60, int(remaining) % 60] % [label, floori(remaining / 60.0), int(remaining) % 60]
) )
_effect_status.text = " ".join(parts) _effect_status.text = " ".join(parts)
_effect_status.visible = not parts.is_empty() _effect_status.visible = not parts.is_empty()

View file

@ -0,0 +1,55 @@
class_name InterfaceFontController
extends Node
signal font_mode_changed(use_readable_font: bool)
const READABLE_FONT_PATH: String = "res://ui/fonts/Tuffy_Bold.otf"
var _game_theme: Theme = preload("res://ui/game_theme.tres")
var _game_font: Font = preload("res://ui/fonts/seattle_avenue.otf")
var _readable_font: Font
var _utility_theme: Theme
var _use_readable_font: bool = false
func _ready() -> void:
if ResourceLoader.exists(READABLE_FONT_PATH):
_readable_font = load(READABLE_FONT_PATH) as Font
if _readable_font == null:
push_error("Readable interface font resource is unavailable.")
else:
_readable_font.fallbacks = [_game_font]
_utility_theme = _game_theme.duplicate(true)
_utility_theme.default_font = readable_font()
func set_readable_font_enabled(enabled: bool) -> void:
_use_readable_font = enabled
_game_theme.default_font = (
_readable_font
if enabled and _readable_font != null
else _game_font
)
font_mode_changed.emit(_use_readable_font)
func is_readable_font_enabled() -> bool:
return _use_readable_font
func apply_utility_theme(themed_node: Node) -> void:
if themed_node == null:
return
if _utility_theme == null:
_utility_theme = _game_theme.duplicate(true)
_utility_theme.default_font = (
_readable_font if _readable_font != null else _game_font
)
if themed_node is Control:
(themed_node as Control).theme = _utility_theme
elif themed_node is Window:
(themed_node as Window).theme = _utility_theme
func readable_font() -> Font:
return _readable_font if _readable_font != null else _game_font

View file

@ -0,0 +1 @@
uid://djmxy6jibi5y3

View file

@ -194,8 +194,6 @@ enum CloseReason {
@onready var _selection_status: BubbleStatusBubbleType = %SelectionStatus @onready var _selection_status: BubbleStatusBubbleType = %SelectionStatus
@onready var _offer_status: BubbleStatusBubbleType = %OfferStatus @onready var _offer_status: BubbleStatusBubbleType = %OfferStatus
@onready var _inventory_section: Control = %InventorySection @onready var _inventory_section: Control = %InventorySection
@onready var _inventory_body: BoxContainer = %InventoryBody
@onready var _inventory_scroll: ScrollContainer = %InventoryScroll
@onready var _bag_section: Control = %BagSection @onready var _bag_section: Control = %BagSection
@onready var _logbook_section: Control = %LogbookSection @onready var _logbook_section: Control = %LogbookSection
@onready var _bag_empty: Label = %BagEmpty @onready var _bag_empty: Label = %BagEmpty
@ -210,9 +208,6 @@ enum CloseReason {
@onready var _held_value: Label = %HeldValue @onready var _held_value: Label = %HeldValue
@onready var _cooler_count: Label = %CoolerCount @onready var _cooler_count: Label = %CoolerCount
@onready var _inventory_empty: Label = %InventoryEmpty @onready var _inventory_empty: Label = %InventoryEmpty
@onready var _inventory_grid: GridContainer = %InventoryGrid
@onready var _inventory_list: Control = %InventoryList
@onready var _detail_panel: Control = %DetailPanel
@onready var _detail_texture: TextureRect = %DetailTexture @onready var _detail_texture: TextureRect = %DetailTexture
@onready var _detail_name: Label = %DetailName @onready var _detail_name: Label = %DetailName
@onready var _detail_data: Label = %DetailData @onready var _detail_data: Label = %DetailData
@ -227,8 +222,6 @@ enum CloseReason {
@onready var _cancel_sale_button: Button = %CancelSaleButton @onready var _cancel_sale_button: Button = %CancelSaleButton
@onready var _logbook_empty: Label = %LogbookEmpty @onready var _logbook_empty: Label = %LogbookEmpty
@onready var _logbook_grid: GridContainer = %LogbookGrid @onready var _logbook_grid: GridContainer = %LogbookGrid
@onready var _status_shoal: HBoxContainer = %StatusShoal
@onready var _sort_bar: HBoxContainer = %SortBar
var _compact_layout: bool = false var _compact_layout: bool = false
var _player: PlayerType var _player: PlayerType
@ -1181,6 +1174,7 @@ func _update_shell_layout() -> void:
_logbook_page.size = reference_size _logbook_page.size = reference_size
_logbook_page.position = Vector2.ZERO _logbook_page.position = Vector2.ZERO
_logbook_rest_position = Vector2.ZERO _logbook_rest_position = Vector2.ZERO
_mail_page.set_anchors_preset(Control.PRESET_TOP_LEFT)
_mail_page.size = reference_size _mail_page.size = reference_size
_mail_page.position = Vector2.ZERO _mail_page.position = Vector2.ZERO
_mail_rest_position = Vector2.ZERO _mail_rest_position = Vector2.ZERO
@ -1732,7 +1726,10 @@ func _reset_page_transition_visuals() -> void:
func _on_sort_selected(index: int, source: OptionButton) -> void: func _on_sort_selected(index: int, source: OptionButton) -> void:
_sort_mode = source.get_item_id(index) var selected_id: int = source.get_item_id(index)
if selected_id < SortMode.CATCH_ORDER or selected_id > SortMode.RARITY:
return
_sort_mode = selected_id as SortMode
_sort_option.select(_sort_mode) _sort_option.select(_sort_mode)
_cooler_sort_option.select(_sort_mode) _cooler_sort_option.select(_sort_mode)
_refresh_inventory() _refresh_inventory()
@ -1891,7 +1888,7 @@ func _layout_bag_items() -> void:
var row: int = floori(float(index) / float(columns)) var row: int = floori(float(index) / float(columns))
var stable_offset := Vector2( var stable_offset := Vector2(
float(identity_hash % 13) - 6.0, float(identity_hash % 13) - 6.0,
float((identity_hash / 19) % 11) - 5.0, float(floori(float(identity_hash) / 19.0) % 11) - 5.0,
) )
var lane_offset: float = 18.0 if row % 2 == 1 else 0.0 var lane_offset: float = 18.0 if row % 2 == 1 else 0.0
item_node.custom_minimum_size = item_size item_node.custom_minimum_size = item_size
@ -2225,7 +2222,7 @@ func _layout_cooler_fish(animate: bool = true) -> void:
var offset_center: float = 2.0 if _compact_layout else 3.0 var offset_center: float = 2.0 if _compact_layout else 3.0
var stable_offset := Vector2( var stable_offset := Vector2(
float(identity_hash % offset_span) - offset_center, float(identity_hash % offset_span) - offset_center,
float((identity_hash / 17) % offset_span) - offset_center, float(floori(float(identity_hash) / 17.0) % offset_span) - offset_center,
) )
var lane_offset: float = 5.0 if row % 2 == 1 else 0.0 var lane_offset: float = 5.0 if row % 2 == 1 else 0.0
var target := Vector2( var target := Vector2(

View file

@ -48,6 +48,7 @@ enum PresentationMode {
@onready var _invert_y_toggle: BubbleButton = %InvertYToggle @onready var _invert_y_toggle: BubbleButton = %InvertYToggle
@onready var _auto_click_toggle: BubbleButton = %AutoClickToggle @onready var _auto_click_toggle: BubbleButton = %AutoClickToggle
@onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue @onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue
@onready var _readable_font_toggle: BubbleButton = %ReadableFontToggle
@onready var _world_options: Array[BubbleButton] = [ @onready var _world_options: Array[BubbleButton] = [
%WorldLegible, %WorldLegible,
@ -83,6 +84,7 @@ var _data_root: PlayerDataRoot
var _identity_backups: IdentityBackupService var _identity_backups: IdentityBackupService
var _player_identity: PlayerIdentityStore var _player_identity: PlayerIdentityStore
var _host_identity: HostIdentityStore var _host_identity: HostIdentityStore
var _interface_fonts: InterfaceFontController
var _data_folder_dialog: FileDialog var _data_folder_dialog: FileDialog
var _backup_file_dialog: FileDialog var _backup_file_dialog: FileDialog
var _export_file_dialog: FileDialog var _export_file_dialog: FileDialog
@ -148,6 +150,7 @@ func _ready() -> void:
) )
_invert_y_toggle.pressed.connect(_toggle_invert_y) _invert_y_toggle.pressed.connect(_toggle_invert_y)
_auto_click_toggle.pressed.connect(_toggle_auto_click) _auto_click_toggle.pressed.connect(_toggle_auto_click)
_readable_font_toggle.pressed.connect(_toggle_readable_font)
%IntervalDecrease.pressed.connect(_adjust_auto_click_interval.bind(-1)) %IntervalDecrease.pressed.connect(_adjust_auto_click_interval.bind(-1))
%IntervalIncrease.pressed.connect(_adjust_auto_click_interval.bind(1)) %IntervalIncrease.pressed.connect(_adjust_auto_click_interval.bind(1))
_auto_click_interval.pressed.connect( _auto_click_interval.pressed.connect(
@ -185,13 +188,16 @@ func setup_data_and_identity(
player_identity: PlayerIdentityStore, player_identity: PlayerIdentityStore,
host_identity: HostIdentityStore, host_identity: HostIdentityStore,
session: NetworkSession, session: NetworkSession,
interface_fonts: InterfaceFontController,
) -> void: ) -> void:
_data_root = data_root _data_root = data_root
_identity_backups = identity_backups _identity_backups = identity_backups
_player_identity = player_identity _player_identity = player_identity
_host_identity = host_identity _host_identity = host_identity
_network_session = session _network_session = session
_interface_fonts = interface_fonts
_identity_backups.operation_finished.connect(_on_identity_operation_finished) _identity_backups.operation_finished.connect(_on_identity_operation_finished)
_interface_fonts.apply_utility_theme(_data_page)
_refresh_data_page() _refresh_data_page()
@ -410,14 +416,18 @@ func _choose_data_folder() -> void:
_data_folder_dialog = FileDialog.new() _data_folder_dialog = FileDialog.new()
_data_folder_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR _data_folder_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
_data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM _data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM
_data_folder_dialog.use_native_dialog = false
_data_folder_dialog.dir_selected.connect(_change_data_folder) _data_folder_dialog.dir_selected.connect(_change_data_folder)
_interface_fonts.apply_utility_theme(_data_folder_dialog)
add_child(_data_folder_dialog) add_child(_data_folder_dialog)
_data_folder_dialog.current_dir = _data_root.root_path.get_base_dir() _data_folder_dialog.current_dir = _data_root.root_path.get_base_dir()
_data_folder_dialog.popup_centered_ratio(0.75) _data_folder_dialog.popup_centered_ratio(0.75)
func _change_data_folder(path: String) -> void: func _change_data_folder(path: String) -> void:
var result := PortableDataMigration.migrate_active_to(_data_root, path) var result: Dictionary = PortableDataMigration.migrate_active_to(
_data_root, path
)
if bool(result.get("requires_existing_root_decision", false)): if bool(result.get("requires_existing_root_decision", false)):
_show_existing_data_folder_choice(path) _show_existing_data_folder_choice(path)
return return
@ -430,7 +440,7 @@ func _change_data_folder(path: String) -> void:
func _show_existing_data_folder_choice(path: String) -> void: func _show_existing_data_folder_choice(path: String) -> void:
var dialog := ConfirmationDialog.new() var dialog: ConfirmationDialog = ConfirmationDialog.new()
dialog.title = "Existing NETFISHING data" dialog.title = "Existing NETFISHING data"
dialog.ok_button_text = "Use Selected Data" dialog.ok_button_text = "Use Selected Data"
dialog.dialog_text = ( dialog.dialog_text = (
@ -449,7 +459,7 @@ func _show_existing_data_folder_choice(path: String) -> void:
dialog.custom_action.connect(func(action: StringName) -> void: dialog.custom_action.connect(func(action: StringName) -> void:
if action != &"replace": if action != &"replace":
return return
var result := PortableDataMigration.replace_existing_with_active( var result: Dictionary = PortableDataMigration.replace_existing_with_active(
_data_root, path _data_root, path
) )
_feedback.text = str(result.get("message", "Could not replace selected data.")) _feedback.text = str(result.get("message", "Could not replace selected data."))
@ -457,6 +467,7 @@ func _show_existing_data_folder_choice(path: String) -> void:
get_tree().call_deferred("quit") get_tree().call_deferred("quit")
dialog.queue_free() dialog.queue_free()
) )
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog) add_child(dialog)
dialog.popup_centered(Vector2i(620, 340)) dialog.popup_centered(Vector2i(620, 340))
@ -466,13 +477,15 @@ func _choose_identity_export(identity_type: String) -> void:
return return
_pending_identity_operation = "export" _pending_identity_operation = "export"
_pending_identity_type = identity_type _pending_identity_type = identity_type
var suggested := _identity_backups.default_export_path(identity_type) var suggested: String = _identity_backups.default_export_path(identity_type)
if _export_file_dialog == null: if _export_file_dialog == null:
_export_file_dialog = FileDialog.new() _export_file_dialog = FileDialog.new()
_export_file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE _export_file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
_export_file_dialog.access = FileDialog.ACCESS_FILESYSTEM _export_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_export_file_dialog.use_native_dialog = false
_export_file_dialog.filters = PackedStringArray(["*.nfidentity ; NETFISHING identity backup"]) _export_file_dialog.filters = PackedStringArray(["*.nfidentity ; NETFISHING identity backup"])
_export_file_dialog.file_selected.connect(_identity_export_file_selected) _export_file_dialog.file_selected.connect(_identity_export_file_selected)
_interface_fonts.apply_utility_theme(_export_file_dialog)
add_child(_export_file_dialog) add_child(_export_file_dialog)
_export_file_dialog.current_dir = suggested.get_base_dir() _export_file_dialog.current_dir = suggested.get_base_dir()
_export_file_dialog.current_file = suggested.get_file() _export_file_dialog.current_file = suggested.get_file()
@ -495,8 +508,10 @@ func _choose_identity_import(identity_type: String) -> void:
_backup_file_dialog = FileDialog.new() _backup_file_dialog = FileDialog.new()
_backup_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE _backup_file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_backup_file_dialog.access = FileDialog.ACCESS_FILESYSTEM _backup_file_dialog.access = FileDialog.ACCESS_FILESYSTEM
_backup_file_dialog.use_native_dialog = false
_backup_file_dialog.filters = PackedStringArray(["*.nfidentity ; NETFISHING identity backup"]) _backup_file_dialog.filters = PackedStringArray(["*.nfidentity ; NETFISHING identity backup"])
_backup_file_dialog.file_selected.connect(_identity_import_file_selected) _backup_file_dialog.file_selected.connect(_identity_import_file_selected)
_interface_fonts.apply_utility_theme(_backup_file_dialog)
add_child(_backup_file_dialog) add_child(_backup_file_dialog)
_backup_file_dialog.current_dir = _data_root.identity_backup_directory() _backup_file_dialog.current_dir = _data_root.identity_backup_directory()
_backup_file_dialog.popup_centered_ratio(0.75) _backup_file_dialog.popup_centered_ratio(0.75)
@ -512,8 +527,8 @@ func _show_passphrase_dialog(exporting: bool) -> void:
_passphrase_dialog = ConfirmationDialog.new() _passphrase_dialog = ConfirmationDialog.new()
_passphrase_dialog.title = "Encrypted identity backup" _passphrase_dialog.title = "Encrypted identity backup"
_passphrase_dialog.confirmed.connect(_submit_identity_passphrase) _passphrase_dialog.confirmed.connect(_submit_identity_passphrase)
var fields := VBoxContainer.new() var fields: VBoxContainer = VBoxContainer.new()
var warning := Label.new() var warning: Label = Label.new()
warning.text = ( warning.text = (
"Anyone with this backup and passphrase can use your identity." "Anyone with this backup and passphrase can use your identity."
) )
@ -528,6 +543,7 @@ func _show_passphrase_dialog(exporting: bool) -> void:
_passphrase_confirm.secret = true _passphrase_confirm.secret = true
fields.add_child(_passphrase_confirm) fields.add_child(_passphrase_confirm)
_passphrase_dialog.add_child(fields) _passphrase_dialog.add_child(fields)
_interface_fonts.apply_utility_theme(_passphrase_dialog)
add_child(_passphrase_dialog) add_child(_passphrase_dialog)
_passphrase_confirm.visible = exporting _passphrase_confirm.visible = exporting
_passphrase_entry.clear() _passphrase_entry.clear()
@ -541,7 +557,7 @@ func _show_passphrase_dialog(exporting: bool) -> void:
func _submit_identity_passphrase() -> void: func _submit_identity_passphrase() -> void:
var passphrase := _passphrase_entry.text var passphrase: String = _passphrase_entry.text
if _pending_identity_operation == "export": if _pending_identity_operation == "export":
_identity_backups.export_backup( _identity_backups.export_backup(
_pending_identity_type, _pending_identity_type,
@ -550,7 +566,7 @@ func _submit_identity_passphrase() -> void:
_passphrase_confirm.text, _passphrase_confirm.text,
) )
return return
var inspected := _identity_backups.import_backup( var inspected: Dictionary = _identity_backups.import_backup(
_pending_identity_type, _pending_identity_type,
_pending_identity_path, _pending_identity_path,
passphrase, passphrase,
@ -566,7 +582,7 @@ func _submit_identity_passphrase() -> void:
func _show_identity_replacement_confirmation() -> void: func _show_identity_replacement_confirmation() -> void:
var dialog := ConfirmationDialog.new() var dialog: ConfirmationDialog = ConfirmationDialog.new()
dialog.title = "Replace active identity?" dialog.title = "Replace active identity?"
dialog.ok_button_text = "Review Replacement" dialog.ok_button_text = "Review Replacement"
dialog.dialog_text = ( dialog.dialog_text = (
@ -579,6 +595,7 @@ func _show_identity_replacement_confirmation() -> void:
dialog.set_meta("confirmation_step", 1) dialog.set_meta("confirmation_step", 1)
dialog.confirmed.connect(_advance_identity_replacement.bind(dialog)) dialog.confirmed.connect(_advance_identity_replacement.bind(dialog))
dialog.canceled.connect(dialog.queue_free) dialog.canceled.connect(dialog.queue_free)
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog) add_child(dialog)
dialog.popup_centered(Vector2i(620, 360)) dialog.popup_centered(Vector2i(620, 360))
@ -659,6 +676,9 @@ func _load_controls() -> void:
_mouse_sensitivity = settings.mouse_camera_sensitivity _mouse_sensitivity = settings.mouse_camera_sensitivity
_controller_sensitivity = settings.controller_camera_sensitivity _controller_sensitivity = settings.controller_camera_sensitivity
_invert_camera_y = settings.invert_camera_y _invert_camera_y = settings.invert_camera_y
_readable_font_toggle.button_pressed = (
settings.use_readable_interface_font
)
_refresh_value_labels() _refresh_value_labels()
@ -686,6 +706,14 @@ func _refresh_value_labels() -> void:
"accessibility\nauto-click\n" "accessibility\nauto-click\n"
+ ("on" if _auto_click_enabled else "off") + ("on" if _auto_click_enabled else "off")
) )
_readable_font_toggle.text = (
"readable\ninterface font\n"
+ (
"on"
if settings.use_readable_interface_font
else "off"
)
)
_auto_click_interval.text = ( _auto_click_interval.text = (
"auto-click\ninterval\n%.2f s" % _auto_click_interval_value "auto-click\ninterval\n%.2f s" % _auto_click_interval_value
) )
@ -709,6 +737,19 @@ func _set_world_pixelation(pixel_size: int) -> void:
_refresh_value_labels() _refresh_value_labels()
func _toggle_readable_font() -> void:
if _settings_manager == null:
return
var edited: PlayerSettings = _settings_manager.current_settings.copy()
edited.use_readable_interface_font = (
not edited.use_readable_interface_font
)
if not _settings_manager.apply_settings(edited):
_feedback.text = "failed to save accessibility settings."
return
_load_controls()
func _set_ui_pixelation(pixel_size: int) -> void: func _set_ui_pixelation(pixel_size: int) -> void:
if _settings_manager == null: if _settings_manager == null:
return return

View file

@ -486,8 +486,8 @@ grow_horizontal = 2
grow_vertical = 2 grow_vertical = 2
script = ExtResource("3_page") script = ExtResource("3_page")
page_id = &"accessibility" page_id = &"accessibility"
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/IntervalHelp"), NodePath("BubbleCluster/AccessibilityBackButton")]) bubble_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/ReadableFontToggle"), NodePath("BubbleCluster/IntervalHelp"), NodePath("BubbleCluster/AccessibilityBackButton")])
focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/AccessibilityBackButton")]) focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), NodePath("BubbleCluster/ReadableFontToggle"), NodePath("BubbleCluster/AccessibilityBackButton")])
initial_focus_path = NodePath("BubbleCluster/AutoClickToggle") initial_focus_path = NodePath("BubbleCluster/AutoClickToggle")
back_focus_path = NodePath("BubbleCluster/AccessibilityBackButton") back_focus_path = NodePath("BubbleCluster/AccessibilityBackButton")
@ -562,6 +562,41 @@ minimum_font_size = 13
maximum_font_size = 22 maximum_font_size = 22
motion_phase = 4.1 motion_phase = 4.1
[node name="ReadableFontToggle" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0)
toggle_mode = true
text = "readable\ninterface font\noff"
neutral_size = Vector2(180, 132)
desktop_anchor = Vector2(92, 405)
compact_anchor = Vector2(102, 374)
compact_minimum_size = Vector2(156, 118)
minimum_font_size = 13
maximum_font_size = 21
motion_phase = 4.6
[node name="ReadableFontSample" type="Label" parent="AccessibilityPage/BubbleCluster"]
layout_mode = 0
offset_left = 250.0
offset_top = 20.0
offset_right = 700.0
offset_bottom = 82.0
mouse_filter = 2
text = "The quick brown fox jumps over the lazy dog.\n0123456789"
horizontal_alignment = 1
vertical_alignment = 1
autowrap_mode = 2
[node name="ReadableFontHelper" type="Label" parent="AccessibilityPage/BubbleCluster"]
layout_mode = 0
offset_left = 250.0
offset_top = 82.0
offset_right = 700.0
offset_bottom = 112.0
mouse_filter = 2
text = "Uses a clearer font for menus and interface text."
horizontal_alignment = 1
[node name="AccessibilityBackButton" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")] [node name="AccessibilityBackButton" parent="AccessibilityPage/BubbleCluster" instance=ExtResource("4_bubble")]
unique_name_in_owner = true unique_name_in_owner = true
custom_minimum_size = Vector2(0, 0) custom_minimum_size = Vector2(0, 0)