From 3734d4a168d2b874b1c3c313bd68a2b9b945dbc6 Mon Sep 17 00:00:00 2001 From: Voyager Date: Thu, 30 Jul 2026 00:54:53 -0400 Subject: [PATCH] Improve interface accessibility and UI reliability --- fishing/fishing_presentation.gd | 16 ++-- fishing/fishing_spot.gd | 2 +- fishing/remote_fishing_presentation.gd | 12 +-- main/main.gd | 83 +++++++++++++++++---- main/main.tscn | 7 +- network/host_ban_store.gd | 10 ++- network/identity_backup_service.gd | 50 +++++++------ network/network_chat_service.gd | 4 +- network/network_mail_service.gd | 12 +-- network/network_player_list_service.gd | 9 ++- network/network_profile_service.gd | 22 +++--- network/network_session.gd | 10 +-- network/player_data_root.gd | 96 ++++++++++++------------ network/player_spawn_service.gd | 4 +- network/portable_data_migration.gd | 98 ++++++++++++++----------- network/portable_file_guard.gd | 50 +++++++------ network/saved_server_store.gd | 7 +- player/player.gd | 12 +-- settings/player_settings.gd | 2 + settings/player_settings_manager.gd | 10 +++ ui/fonts/Tuffy-LICENSE.txt | 11 +++ ui/fonts/Tuffy_Bold.otf | Bin 0 -> 97308 bytes ui/fonts/Tuffy_Bold.otf.import | 36 +++++++++ ui/game_ui.gd | 4 +- ui/interface_font_controller.gd | 55 ++++++++++++++ ui/interface_font_controller.gd.uid | 1 + ui/player_menu.gd | 17 ++--- ui/settings_panel.gd | 59 ++++++++++++--- ui/settings_panel.tscn | 39 +++++++++- 29 files changed, 514 insertions(+), 224 deletions(-) create mode 100644 ui/fonts/Tuffy-LICENSE.txt create mode 100644 ui/fonts/Tuffy_Bold.otf create mode 100644 ui/fonts/Tuffy_Bold.otf.import create mode 100644 ui/interface_font_controller.gd create mode 100644 ui/interface_font_controller.gd.uid diff --git a/fishing/fishing_presentation.gd b/fishing/fishing_presentation.gd index a44ed7f..62501e2 100644 --- a/fishing/fishing_presentation.gd +++ b/fishing/fishing_presentation.gd @@ -216,12 +216,12 @@ func show_bite() -> void: _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: return _kill_active_tween() - _bobber.global_position = _with_water_height(position) + _bobber.global_position = _with_water_height(world_position) func begin_reeling() -> void: @@ -232,12 +232,12 @@ func begin_reeling() -> void: _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: return _kill_active_tween() - _bobber.global_position = _with_water_height(position) + _bobber.global_position = _with_water_height(world_position) func play_outcome(outcome: StringName) -> void: @@ -371,8 +371,12 @@ func _calculate_pickup_position(target: Vector3) -> Vector3: return player_on_water + outward * pickup_distance -func _with_water_height(position: Vector3) -> Vector3: - return Vector3(position.x, get_water_surface_height(), position.z) +func _with_water_height(world_position: Vector3) -> Vector3: + return Vector3( + world_position.x, + get_water_surface_height(), + world_position.z, + ) func _update_line(delta: float) -> void: diff --git a/fishing/fishing_spot.gd b/fishing/fishing_spot.gd index fd2e01c..1655c6d 100644 --- a/fishing/fishing_spot.gd +++ b/fishing/fishing_spot.gd @@ -1082,7 +1082,7 @@ func get_fishable_water_region( query, 32 ) - var selected_region: FishableWaterRegionType + var selected_region: FishableWaterRegionType = null for result: Dictionary in results: var collider: Object = result.get("collider") var region: FishableWaterRegionType = collider as FishableWaterRegionType diff --git a/fishing/remote_fishing_presentation.gd b/fishing/remote_fishing_presentation.gd index 98e5ea8..3d20ee9 100644 --- a/fishing/remote_fishing_presentation.gd +++ b/fishing/remote_fishing_presentation.gd @@ -9,8 +9,8 @@ var _target: Vector3 var _active: bool = false -func setup(owner: Player) -> void: - _owner = owner +func setup(owning_player: Player) -> void: + _owner = owning_player _bobber = MeshInstance3D.new() var bobber_mesh := SphereMesh.new() bobber_mesh.radius = 0.12 @@ -43,11 +43,11 @@ func show_cast(target: Vector3) -> void: _redraw_line() -func update_bobber(position: Vector3) -> void: - if not _active or not position.is_finite(): +func update_bobber(world_position: Vector3) -> void: + if not _active or not world_position.is_finite(): return - _target = position - _bobber.global_position = position + _target = world_position + _bobber.global_position = world_position _redraw_line() diff --git a/main/main.gd b/main/main.gd index 590aca9..d8e86e8 100644 --- a/main/main.gd +++ b/main/main.gd @@ -88,6 +88,7 @@ const TITLE_MUSIC_SILENCE_DB: float = -80.0 @onready var _water_recovery: WaterRecoveryControllerType = %WaterRecovery @onready var _save_manager: PlayerSaveManagerType = %PlayerSaveManager @onready var _settings_manager: PlayerSettingsManagerType = %PlayerSettingsManager +@onready var _interface_fonts: InterfaceFontController = %InterfaceFontController @onready var _title_music: AudioStreamPlayer = %TitleMusic @onready var _ui_pixelation: UIPixelationPresenterType = %UIPresentation @onready var _pixelation_reset: PixelationResetOverlayType = ( @@ -146,11 +147,21 @@ var _pending_trust_changed: bool = false var _identity_notice_dialog: AcceptDialog var _data_setup_dialog: ConfirmationDialog 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 _pending_existing_root_path := "" 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(): _configure_portable_stores() _initialize_after_data_root() @@ -377,6 +388,7 @@ func _initialize_after_data_root() -> void: _player_identity, _host_identity, _network_session, + _interface_fonts, ) _data_root.conflict_detected.connect(_on_portable_conflict) _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_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( _on_effective_ui_pixel_size_changed ) @@ -420,7 +428,6 @@ func _initialize_after_data_root() -> void: _game_ui.focus_open_settings_back_button ) _pixelation_reset.reset_requested.connect(_reset_pixelation) - _settings_manager.load_settings() _apply_runtime_settings(_settings_manager.current_settings) _set_gameplay_active(false) var title_screen: TitleScreenType = _game_ui.get_title_screen() @@ -491,9 +498,10 @@ func _show_data_root_setup() -> void: "Keep Current Location", false, "current" ) _data_setup_dialog.custom_action.connect(_on_data_setup_action) + _interface_fonts.apply_utility_theme(_data_setup_dialog) add_child(_data_setup_dialog) - var default_path := _data_root.default_visible_path() - var legacy := PortableDataMigration.legacy_files_present() + var default_path: String = _data_root.default_visible_path() + var legacy: bool = PortableDataMigration.legacy_files_present() _data_setup_dialog.dialog_text = ( ("Move your existing NETFISHING data to an easy-to-find folder." 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: - var path := _data_root.default_visible_path() + var path: String = _data_root.default_visible_path() if path.is_empty(): _show_folder_picker() return @@ -535,7 +543,29 @@ func _show_folder_picker() -> void: _data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM _data_folder_dialog.use_native_dialog = false _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) + 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_root.default_visible_path().get_base_dir() 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) +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: - var ok := false + _restore_data_setup_after_picker = false + var ok: bool = false if PortableDataMigration.legacy_files_present(): - var result := ( + var result: Dictionary = ( PortableDataMigration.adopt_legacy_app_data(_data_root) if app_data 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: _pending_existing_root_path = path - var dialog := ConfirmationDialog.new() + var dialog: ConfirmationDialog = ConfirmationDialog.new() dialog.title = "Existing NETFISHING data" dialog.ok_button_text = "Use Data Already in Selected Folder" 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: if action != &"replace": return - var result := PortableDataMigration.replace_existing_with_legacy( + var result: Dictionary = PortableDataMigration.replace_existing_with_legacy( _data_root, _pending_existing_root_path ) if bool(result.get("ok", false)): @@ -605,6 +654,7 @@ func _show_existing_root_choice(path: String) -> void: else: _show_data_error(str(result.get("message", "Migration failed."))) ) + _interface_fonts.apply_utility_theme(dialog) add_child(dialog) 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: - var dialog := AcceptDialog.new() + var dialog: AcceptDialog = AcceptDialog.new() + _interface_fonts.apply_utility_theme(dialog) dialog.title = "Player data conflict" dialog.dialog_text = ( message @@ -635,7 +686,8 @@ func _on_portable_conflict(message: String, _path: String) -> void: func _on_data_root_status(message: String) -> void: if "Syncthing conflict" not in message: return - var dialog := AcceptDialog.new() + var dialog: AcceptDialog = AcceptDialog.new() + _interface_fonts.apply_utility_theme(dialog) dialog.title = "Synced data needs review" dialog.dialog_text = message dialog.add_button("Open Data Folder", false, "open") @@ -771,6 +823,9 @@ func _process(_delta: float) -> void: func _apply_runtime_settings(settings: PlayerSettingsType) -> void: if settings == null: return + _interface_fonts.set_readable_font_enabled( + settings.use_readable_interface_font + ) _apply_world_pixelation(settings.world_pixel_size) _ui_pixelation.set_pixel_size(settings.ui_pixel_size) _game_ui.get_title_screen().set_world_pixelation( diff --git a/main/main.tscn b/main/main.tscn index 576dc80..89c988f 100644 --- a/main/main.tscn +++ b/main/main.tscn @@ -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://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/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://ui/interface_font_controller.gd" id="39_fonts"] [node name="Main" type="Node3D"] script = ExtResource("3_main") @@ -46,6 +47,10 @@ pelican_buyer_profile = ExtResource("7_pelicans") main_shop_buyer_profile = ExtResource("12_main_shop") 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="."] unique_name_in_owner = true script = ExtResource("37_data_root") diff --git a/network/host_ban_store.gd b/network/host_ban_store.gd index 716a60e..f8c4bf9 100644 --- a/network/host_ban_store.gd +++ b/network/host_ban_store.gd @@ -24,11 +24,15 @@ func is_banned(host_fingerprint: String, target_fingerprint: String) -> bool: return Dictionary(_namespaces.get(host_fingerprint, {})).has(target_fingerprint) -func ban(host_fingerprint: String, target_fingerprint: String, name: String) -> bool: +func ban( + host_fingerprint: String, + target_fingerprint: String, + display_name: String, +) -> bool: if ( not NetworkIdentityCrypto.valid_fingerprint(host_fingerprint) or not NetworkIdentityCrypto.valid_fingerprint(target_fingerprint) - or not NetworkProfilePreferences.is_valid_display_name(name) + or not NetworkProfilePreferences.is_valid_display_name(display_name) ): return false _ensure_loaded() @@ -41,7 +45,7 @@ func ban(host_fingerprint: String, target_fingerprint: String, name: String) -> records[target_fingerprint] = { "host_fingerprint": host_fingerprint, "target_fingerprint": target_fingerprint, - "last_known_display_name": name, + "last_known_display_name": display_name, "banned_unix": int(Time.get_unix_time_from_system()), "reason": "host_ban", } diff --git a/network/identity_backup_service.gd b/network/identity_backup_service.gd index 98f6106..bceb4bb 100644 --- a/network/identity_backup_service.gd +++ b/network/identity_backup_service.gd @@ -25,12 +25,12 @@ func setup( func default_export_path(identity_type: String) -> String: - var store := _store(identity_type) + var store: LocalSigningIdentityStore = _store(identity_type) if store == null: return "" if not store.is_ready() and not store.load_or_create(): return "" - var timestamp := Time.get_datetime_string_from_system().replace(":", "-") + var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-") return _data_root.identity_backup_directory().path_join( "%s-%s-%s.nfidentity" % [ identity_type, @@ -50,14 +50,14 @@ func export_backup( return _finish(false, "Passphrases must match and contain at least 12 characters.") if FileAccess.file_exists(path): return _finish(false, "Choose a new backup filename.") - var store := _store(identity_type) + var store: LocalSigningIdentityStore = _store(identity_type) if store == null or (not store.is_ready() and not store.load_or_create()): return _finish(false, "The active identity is unavailable.") - var material := store.export_identity_material() - var proof := store.sign("identity_backup_self_test", [ + var material: Dictionary = store.export_identity_material() + var proof: PackedByteArray = store.sign("identity_backup_self_test", [ identity_type, material["fingerprint"], ]) - var envelope := { + var envelope: Dictionary = { "magic": MAGIC, "format_version": FORMAT_VERSION, "identity_type": identity_type, @@ -70,16 +70,18 @@ func export_backup( "self_signature": Marshalls.raw_to_base64(proof), } DirAccess.make_dir_recursive_absolute(path.get_base_dir()) - var file := FileAccess.open_encrypted_with_pass(path, FileAccess.WRITE, passphrase) + var file: FileAccess = FileAccess.open_encrypted_with_pass( + path, FileAccess.WRITE, passphrase + ) if file == null: return _finish(false, "Could not write this identity backup.") file.store_string(JSON.stringify(envelope)) file.flush() - var ok := file.get_error() == OK + var ok: bool = file.get_error() == OK file.close() if not ok: return _finish(false, "Could not write this identity backup.") - var verified := inspect_backup(path, passphrase, identity_type) + var verified: Dictionary = inspect_backup(path, passphrase, identity_type) if not bool(verified.get("ok", false)): DirAccess.remove_absolute(path) return _finish(false, "Could not verify this identity backup.") @@ -97,12 +99,14 @@ func inspect_backup( or FileAccess.get_size(path) > MAX_BACKUP_BYTES ): return {"ok": false} - var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, passphrase) + var file: FileAccess = FileAccess.open_encrypted_with_pass( + path, FileAccess.READ, passphrase + ) if file == null: return {"ok": false} - var text := file.get_as_text() + var text: String = file.get_as_text() file.close() - var json := JSON.new() + var json: JSON = JSON.new() if json.parse(text) != OK or typeof(json.data) != TYPE_DICTIONARY: return {"ok": false} var data: Dictionary = json.data @@ -113,21 +117,23 @@ func inspect_backup( or data.get("algorithm") != NetworkIdentityCrypto.ALGORITHM ): return {"ok": false} - var private_pem := str(data.get("private_pem", "")) - var public_pem := NetworkIdentityCrypto.normalize_public_pem( + var private_pem: String = str(data.get("private_pem", "")) + var public_pem: String = NetworkIdentityCrypto.normalize_public_pem( str(data.get("public_pem", "")) ) - var fingerprint := str(data.get("fingerprint", "")) + var fingerprint: String = str(data.get("fingerprint", "")) if ( private_pem.length() > 128 * 1024 or public_pem.length() > 32 * 1024 or NetworkIdentityCrypto.fingerprint_public_pem(public_pem) != fingerprint ): return {"ok": false} - var key := CryptoKey.new() + var key: CryptoKey = CryptoKey.new() if key.load_from_string(private_pem) != OK: return {"ok": false} - var signature := Marshalls.base64_to_raw(str(data.get("self_signature", ""))) + var signature: PackedByteArray = Marshalls.base64_to_raw( + str(data.get("self_signature", "")) + ) if not NetworkIdentityCrypto.verify_fields( NetworkIdentityCrypto.load_public_key(public_pem), "identity_backup_self_test", @@ -135,7 +141,7 @@ func inspect_backup( signature, ): return {"ok": false} - var fresh := NetworkIdentityCrypto.sign_fields( + var fresh: PackedByteArray = NetworkIdentityCrypto.sign_fields( key, "identity_import_self_test", [fingerprint] ) if not NetworkIdentityCrypto.verify_fields( @@ -160,12 +166,12 @@ func import_backup( passphrase: String, confirmed_replacement: bool, ) -> Dictionary: - var inspected := inspect_backup(path, passphrase, identity_type) + var inspected: Dictionary = inspect_backup(path, passphrase, identity_type) if not bool(inspected.get("ok", false)): _finish(false, "Could not open this identity backup.") return {"ok": false} - var store := _store(identity_type) - var incoming := str(inspected["fingerprint"]) + var store: LocalSigningIdentityStore = _store(identity_type) + var incoming: String = str(inspected["fingerprint"]) if store.fingerprint == incoming: _finish(true, "This identity is already active.") return {"ok": true, "same": true} @@ -176,7 +182,7 @@ func import_backup( "current_fingerprint": store.fingerprint, "incoming_fingerprint": incoming, } - var result := store.install_identity_material( + var result: Dictionary = store.install_identity_material( str(inspected["private_pem"]), str(inspected["public_pem"]), incoming, diff --git a/network/network_chat_service.gd b/network/network_chat_service.gd index fcf3ff7..36f26b6 100644 --- a/network/network_chat_service.gd +++ b/network/network_chat_service.gd @@ -245,10 +245,10 @@ func _on_peer_removed(peer_id: int) -> void: _rate_times.erase(peer_id) if not _session.is_host(): return - var name: String = _peer_names.get(peer_id, "Player") + var display_name: String = _peer_names.get(peer_id, "Player") _peer_names.erase(peer_id) _broadcast(_make_message( - NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s left." % name + NetworkChatProtocol.Kind.SYSTEM, 0, "", "%s left." % display_name )) diff --git a/network/network_mail_service.gd b/network/network_mail_service.gd index f4f3456..b2fa672 100644 --- a/network/network_mail_service.gd +++ b/network/network_mail_service.gd @@ -144,14 +144,14 @@ func get_recipient_choices() -> Array[Dictionary]: var normalized := str(choice["name"]).to_lower() counts[normalized] = int(counts.get(normalized, 0)) + 1 for choice: Dictionary in choices: - var name: String = choice["name"] - if int(counts.get(name.to_lower(), 0)) > 1: + var display_name: String = choice["name"] + if int(counts.get(display_name.to_lower(), 0)) > 1: choice["label"] = "%s ยท %s" % [ - name, + display_name, NetworkIdentityCrypto.compact_suffix(choice["fingerprint"]), ] else: - choice["label"] = name + choice["label"] = display_name return choices @@ -531,8 +531,8 @@ func prepare_recipient(transfer_id: String, letter: Dictionary) -> void: func _prepare_recipient(transfer_id: String, letter: Dictionary) -> void: - var ready := _can_receive(letter["attachment"]) - _send_phase_ack("recipient_prepared", transfer_id, ready) + var recipient_ready: bool = _can_receive(letter["attachment"]) + _send_phase_ack("recipient_prepared", transfer_id, recipient_ready) func _request_sender_commit(peer_id: int, transfer_id: String, letter: Dictionary) -> void: diff --git a/network/network_player_list_service.gd b/network/network_player_list_service.gd index 2681a14..9145cfa 100644 --- a/network/network_player_list_service.gd +++ b/network/network_player_list_service.gd @@ -117,12 +117,17 @@ func kick(peer_id: int, fingerprint: String, revision: int) -> bool: return ok -func ban(peer_id: int, fingerprint: String, name: String, revision: int) -> bool: +func ban( + peer_id: int, + fingerprint: String, + display_name: String, + revision: int, +) -> bool: if not _valid_moderation_target(peer_id, fingerprint, revision): moderation_finished.emit(false, "That player is no longer connected.") return false var host_fingerprint := _session.get_host_identity_fingerprint() - if not _bans.ban(host_fingerprint, fingerprint, name): + if not _bans.ban(host_fingerprint, fingerprint, display_name): moderation_finished.emit(false, "Ban could not be saved.") return false var ok := _session.kick_authenticated_peer(peer_id, fingerprint, true) diff --git a/network/network_profile_service.gd b/network/network_profile_service.gd index 40b15f4..2666d26 100644 --- a/network/network_profile_service.gd +++ b/network/network_profile_service.gd @@ -172,10 +172,14 @@ func submit_profile_apply(data: Dictionary) -> void: func _process_apply_request(peer_id: int, data: Dictionary) -> void: - var name := str(data["display_name"]).strip_edges() - var conflict := _name_conflicts(peer_id, name) - var accepted := not conflict or bool(data["use_anyway"]) - var suggestions := _make_suggestions(peer_id, name) if conflict else PackedStringArray() + var display_name: String = str(data["display_name"]).strip_edges() + var conflict: bool = _name_conflicts(peer_id, display_name) + var accepted: bool = not conflict or bool(data["use_anyway"]) + var suggestions: PackedStringArray = ( + _make_suggestions(peer_id, display_name) + if conflict + else PackedStringArray() + ) if peer_id == _session.get_local_peer_id(): _apply_local_result( str(data["request_id"]), accepted, "", conflict, suggestions @@ -184,7 +188,7 @@ func _process_apply_request(peer_id: int, data: Dictionary) -> void: if accepted: _host_pending_apply[str(data["request_id"])] = { "peer_id": peer_id, - "display_name": name, + "display_name": display_name, "appearance": Dictionary(data["appearance"]).duplicate(true), "authorization": { "request_id": data["request_id"], @@ -214,10 +218,10 @@ func confirm_profile_saved(request_id: String) -> void: ): return _host_pending_apply.erase(request_id) - var name := str(pending["display_name"]) - var appearance := Dictionary(pending["appearance"]) - _session.apply_canonical_profile(sender_id, name, appearance) - var record := _session.get_peer_record(sender_id) + var display_name: String = str(pending["display_name"]) + var appearance: Dictionary = Dictionary(pending["appearance"]) + _session.apply_canonical_profile(sender_id, display_name, appearance) + var record: PeerRegistry.PeerRecord = _session.get_peer_record(sender_id) if record != null: record.profile_authorization = pending.get("authorization", {}).duplicate(true) _apply_to_avatar(sender_id, appearance) diff --git a/network/network_session.gd b/network/network_session.gd index 3844ea3..1538550 100644 --- a/network/network_session.gd +++ b/network/network_session.gd @@ -1145,13 +1145,13 @@ func _apply_spawn_entry(entry: Dictionary) -> void: ) _registry.update_appearance(peer_id, appearance) var transform: Transform3D = _spawn_service.get_spawn_transform_for_index(0) - var position: Array = entry["position"] - if position.size() != 3: + var spawn_position: Array = entry["position"] + if spawn_position.size() != 3: return transform.origin = Vector3( - float(position[0]), - float(position[1]), - float(position[2]) + float(spawn_position[0]), + float(spawn_position[1]), + float(spawn_position[2]) ) transform.basis = Basis(Vector3.UP, float(entry["yaw"])) var avatar := _spawn_service.spawn_remote_player(peer_id, transform, false) diff --git a/network/player_data_root.gd b/network/player_data_root.gd index b2f8a65..9e379d2 100644 --- a/network/player_data_root.gd +++ b/network/player_data_root.gd @@ -34,7 +34,7 @@ var override_active := false func resolve() -> bool: _load_bootstrap_identity() - var command_line := _command_line_override() + var command_line: String = _command_line_override() if not command_line.is_empty(): override_active = true mode = Mode.COMMAND_LINE_OVERRIDE @@ -42,18 +42,18 @@ func resolve() -> bool: if OS.has_environment(ENVIRONMENT_VARIABLE): override_active = true mode = Mode.ENVIRONMENT_OVERRIDE - var environment_path := OS.get_environment(ENVIRONMENT_VARIABLE) + var environment_path: String = OS.get_environment(ENVIRONMENT_VARIABLE) if not environment_path.is_absolute_path(): return _fail("NETFISHING_DATA_DIR must be an absolute path.") return _activate_existing(environment_path, "", false) - var bootstrap := _read_json(BOOTSTRAP_PATH, 64 * 1024) + var bootstrap: Dictionary = _read_json(BOOTSTRAP_PATH, 64 * 1024) if bootstrap.is_empty() and FileAccess.file_exists(BOOTSTRAP_PATH + ".backup"): bootstrap = _read_json(BOOTSTRAP_PATH + ".backup", 64 * 1024) if not bootstrap.is_empty(): if bootstrap.get("format_version") != BOOTSTRAP_VERSION: return _fail("The data-folder pointer uses an unsupported version.") - var selected := str(bootstrap.get("selected_absolute_path", "")) - var expected := str(bootstrap.get("expected_root_id", "")) + var selected: String = str(bootstrap.get("selected_absolute_path", "")) + var expected: String = str(bootstrap.get("expected_root_id", "")) if selected.is_empty(): return _fail("The data-folder pointer is incomplete.") mode = ( @@ -68,7 +68,7 @@ func resolve() -> bool: func default_visible_path() -> String: - var documents := OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) + var documents: String = OS.get_system_dir(OS.SYSTEM_DIR_DOCUMENTS) return documents.path_join("NETFISHING") if not documents.is_empty() else "" @@ -77,14 +77,14 @@ func select_new_root(path: String, app_data: bool = false) -> bool: return _fail("The data folder is controlled by a process override.") if device_id.length() != 32: device_id = Crypto.new().generate_random_bytes(16).hex_encode() - var normalized := _normalize(path) + var normalized: String = _normalize(path) if app_data: normalized = ProjectSettings.globalize_path(APP_DATA_PORTABLE_PATH) if not _validate_candidate(normalized, true): return false - var manifest_path := normalized.path_join(MANIFEST_FILENAME) + var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) if FileAccess.file_exists(manifest_path): - var manifest := _read_json(manifest_path) + var manifest: Dictionary = _read_json(manifest_path) if not _valid_manifest(manifest): return _fail("The selected folder has a malformed NETFISHING manifest.") root_id = str(manifest["root_id"]) @@ -107,30 +107,30 @@ func select_new_root(path: String, app_data: bool = false) -> bool: func create_unbound_root(path: String) -> Dictionary: - var normalized := _normalize(path) + var normalized: String = _normalize(path) if not _validate_candidate(normalized, true): return {"ok": false, "message": error_message} if not _directory_is_empty(normalized): return {"ok": false, "message": "The destination staging folder is not empty."} - var id := Crypto.new().generate_random_bytes(16).hex_encode() + var id: String = Crypto.new().generate_random_bytes(16).hex_encode() if not _create_layout(normalized, id): return {"ok": false, "message": "The portable layout could not be created."} return {"ok": true, "root_id": id} func create_app_data_layout_for_migration(path: String) -> Dictionary: - var normalized := _normalize(path) + var normalized: String = _normalize(path) if not _validate_candidate(normalized, false): return {"ok": false, "message": error_message} - var manifest_path := normalized.path_join(MANIFEST_FILENAME) + var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) if FileAccess.file_exists(manifest_path): - var manifest := _read_json(manifest_path) + var manifest: Dictionary = _read_json(manifest_path) return ( {"ok": true, "root_id": str(manifest.get("root_id", ""))} if _valid_manifest(manifest) else {"ok": false, "message": "The app-data manifest is malformed."} ) - var id := Crypto.new().generate_random_bytes(16).hex_encode() + var id: String = Crypto.new().generate_random_bytes(16).hex_encode() if not _create_layout(normalized, id): return {"ok": false, "message": "The app-data layout could not be created."} return {"ok": true, "root_id": id} @@ -141,7 +141,7 @@ func use_existing_root(path: String) -> bool: return _fail("The data folder is controlled by a process override.") if device_id.length() != 32: device_id = Crypto.new().generate_random_bytes(16).hex_encode() - var normalized := _normalize(path) + var normalized: String = _normalize(path) if not _activate_existing(normalized, "", true): return false if not _write_bootstrap(root_path, root_id): @@ -156,7 +156,7 @@ func use_existing_root(path: String) -> bool: return true -func path_for(owner: StringName) -> String: +func path_for(store_owner: StringName) -> String: var relative: String = { &"player_save": "player/player_save.json", &"network_profile": "player/network_profile.json", @@ -166,7 +166,7 @@ func path_for(owner: StringName) -> String: &"player_relationships": "social/player_relationships.json", &"server_trust": "social/server_trust.json", &"host_bans": "social/host_bans.json", - }.get(owner, "") + }.get(store_owner, "") return root_path.path_join(relative) if not relative.is_empty() else "" @@ -202,16 +202,16 @@ func report_conflict(message: String, conflict_path: String) -> void: func _activate_existing(path: String, expected_id: String, permit_creation: bool) -> bool: - var normalized := _normalize(path) + var normalized: String = _normalize(path) if not _validate_candidate(normalized, permit_creation): return false - var manifest_path := normalized.path_join(MANIFEST_FILENAME) + var manifest_path: String = normalized.path_join(MANIFEST_FILENAME) if not FileAccess.file_exists(manifest_path): return _fail("The selected folder is not a NETFISHING data folder.") - var manifest := _read_json(manifest_path) + var manifest: Dictionary = _read_json(manifest_path) if not _valid_manifest(manifest): return _fail("The NETFISHING data-folder manifest is malformed.") - var found_id := str(manifest["root_id"]) + var found_id: String = str(manifest["root_id"]) if not expected_id.is_empty() and expected_id != found_id: return _fail("The selected data folder does not match this device pointer.") if not _test_writable(normalized): @@ -228,7 +228,7 @@ func _activate_existing(path: String, expected_id: String, permit_creation: bool func _validate_candidate(path: String, create: bool) -> bool: if path.is_empty() or not path.is_absolute_path(): return _fail("Choose an absolute filesystem folder.") - var project := ProjectSettings.globalize_path("res://").trim_suffix("/") + var project: String = ProjectSettings.globalize_path("res://").trim_suffix("/") if path == project or path.begins_with(project + "/"): return _fail("The project folder cannot be used as the player data folder.") if not DirAccess.dir_exists_absolute(path): @@ -238,8 +238,10 @@ func _validate_candidate(path: String, create: bool) -> bool: func _test_writable(path: String) -> bool: - var probe := path.path_join(".netfishing-write-%s.tmp" % device_id.left(12)) - var file := FileAccess.open(probe, FileAccess.WRITE) + var probe: String = path.path_join( + ".netfishing-write-%s.tmp" % device_id.left(12) + ) + var file: FileAccess = FileAccess.open(probe, FileAccess.WRITE) if file == null: return false file.store_string("probe") @@ -254,8 +256,8 @@ func _create_layout(path: String, id: String) -> bool: ]: if DirAccess.make_dir_recursive_absolute(path.path_join(relative)) != OK: return false - var now := int(Time.get_unix_time_from_system()) - var manifest := { + var now: int = int(Time.get_unix_time_from_system()) + var manifest: Dictionary = { "format_version": MANIFEST_VERSION, "layout_version": LAYOUT_VERSION, "application": APPLICATION_ID, @@ -267,7 +269,7 @@ func _create_layout(path: String, id: String) -> bool: path.path_join(MANIFEST_FILENAME), JSON.stringify(manifest, "\t") ): return false - var readme := ( + var readme: String = ( "NETFISHING player data\n\n" + "This folder is safe to synchronize with tools such as Syncthing.\n" + "Active private identity keys remain device-local.\n" @@ -280,8 +282,8 @@ func _create_layout(path: String, id: String) -> bool: func _write_bootstrap(path: String, id: String) -> bool: - var now := int(Time.get_unix_time_from_system()) - var data := { + var now: int = int(Time.get_unix_time_from_system()) + var data: Dictionary = { "format_version": BOOTSTRAP_VERSION, "selected_absolute_path": path, "expected_root_id": id, @@ -296,7 +298,7 @@ func _write_bootstrap(path: String, id: String) -> bool: func _load_bootstrap_identity() -> void: - var data := _read_json(BOOTSTRAP_PATH, 64 * 1024) + var data: Dictionary = _read_json(BOOTSTRAP_PATH, 64 * 1024) if data.is_empty(): data = _read_json(BOOTSTRAP_PATH + ".backup", 64 * 1024) device_id = str(data.get("device_id", "")) @@ -305,9 +307,9 @@ func _load_bootstrap_identity() -> void: func _command_line_override() -> String: - var args := OS.get_cmdline_user_args() + var args: PackedStringArray = OS.get_cmdline_user_args() for index: int in args.size(): - var value := args[index] + var value: String = args[index] if value.begins_with("--data-dir="): return value.trim_prefix("--data-dir=") if value == "--data-dir" and index + 1 < args.size(): @@ -326,15 +328,15 @@ func _valid_manifest(data: Dictionary) -> bool: func _directory_is_empty(path: String) -> bool: - var access := DirAccess.open(path) + var access: DirAccess = DirAccess.open(path) if access == null: return true access.list_dir_begin() - var name := access.get_next() - while name in [".", ".."]: - name = access.get_next() + var entry_name: String = access.get_next() + while entry_name in [".", ".."]: + entry_name = access.get_next() access.list_dir_end() - return name.is_empty() + return entry_name.is_empty() func _normalize(path: String) -> String: @@ -344,37 +346,37 @@ func _normalize(path: String) -> String: func _read_json(path: String, maximum := 1024 * 1024) -> Dictionary: if not FileAccess.file_exists(path): return {} - var file := FileAccess.open(path, FileAccess.READ) + var file: FileAccess = FileAccess.open(path, FileAccess.READ) if file == null or file.get_length() > maximum: return {} - var json := JSON.new() - var error := json.parse(file.get_as_text()) + var json: JSON = JSON.new() + var error: Error = json.parse(file.get_as_text()) file.close() return json.data if error == OK and typeof(json.data) == TYPE_DICTIONARY else {} func _write_text_atomic(path: String, text: String) -> bool: - var absolute := ( + var absolute: String = ( ProjectSettings.globalize_path(path) if path.begins_with("user://") else path ) if DirAccess.make_dir_recursive_absolute(absolute.get_base_dir()) != OK: return false - var temporary := absolute + ".tmp" - var file := FileAccess.open(temporary, FileAccess.WRITE) + var temporary: String = absolute + ".tmp" + var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE) if file == null: return false file.store_string(text) file.flush() - var ok := file.get_error() == OK + var ok: bool = file.get_error() == OK file.close() if not ok: return false - var backup := absolute + ".backup" + var backup: String = absolute + ".backup" if FileAccess.file_exists(backup): DirAccess.remove_absolute(backup) - var had_primary := FileAccess.file_exists(absolute) + var had_primary: bool = FileAccess.file_exists(absolute) if had_primary and DirAccess.rename_absolute(absolute, backup) != OK: DirAccess.remove_absolute(temporary) return false diff --git a/network/player_spawn_service.gd b/network/player_spawn_service.gd index c31feb6..1352758 100644 --- a/network/player_spawn_service.gd +++ b/network/player_spawn_service.gd @@ -78,10 +78,10 @@ func get_avatar(peer_id: int) -> Player: return _avatars.get(peer_id) -func set_peer_presentation_visible(peer_id: int, visible: bool) -> void: +func set_peer_presentation_visible(peer_id: int, should_be_visible: bool) -> void: var avatar: Player = _avatars.get(peer_id) if avatar != null and is_instance_valid(avatar): - avatar.set_remote_presentation_visible(visible) + avatar.set_remote_presentation_visible(should_be_visible) func get_peer_ids() -> Array[int]: diff --git a/network/portable_data_migration.gd b/network/portable_data_migration.gd index 7ab2b94..45ce7fb 100644 --- a/network/portable_data_migration.gd +++ b/network/portable_data_migration.gd @@ -24,9 +24,9 @@ static func migrate_legacy_to( data_root: PlayerDataRoot, destination: String, ) -> Dictionary: - var normalized := destination.simplify_path().trim_suffix("/") + var normalized: String = destination.simplify_path().trim_suffix("/") if DirAccess.dir_exists_absolute(normalized): - var existing_manifest := normalized.path_join( + var existing_manifest: String = normalized.path_join( PlayerDataRoot.MANIFEST_FILENAME ) if FileAccess.file_exists(existing_manifest): @@ -40,24 +40,24 @@ static func migrate_legacy_to( "ok": false, "message": "Choose an empty folder or create a NETFISHING subfolder.", } - var staging := "%s.migration-%s" % [ + var staging: String = "%s.migration-%s" % [ normalized, Crypto.new().generate_random_bytes(8).hex_encode() ] if DirAccess.make_dir_recursive_absolute(staging) != OK: return {"ok": false, "message": "Migration staging could not be created."} - var created := data_root.create_unbound_root(staging) + var created: Dictionary = data_root.create_unbound_root(staging) if not bool(created.get("ok", false)): _remove_tree(staging) return {"ok": false, "message": str(created.get("message", ""))} var copied: Array[String] = [] for source_name: String in LEGACY_FILES: - var source := ProjectSettings.globalize_path( + var source: String = ProjectSettings.globalize_path( "user://".path_join(source_name) ) if not FileAccess.file_exists(source): continue - var target := staging.path_join(LEGACY_FILES[source_name]) - var result := _copy_verified_json(source, target) + var target: String = staging.path_join(str(LEGACY_FILES[source_name])) + var result: Dictionary = _copy_verified_json(source, target) if not bool(result.get("ok", false)): _remove_tree(staging) return { @@ -70,11 +70,15 @@ static func migrate_legacy_to( if DirAccess.rename_absolute(staging, normalized) != OK: _remove_tree(staging) return {"ok": false, "message": "Migration could not activate its destination."} - var manifest := _read_json(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)) - var migrated_root_id := str(manifest.get("root_id", "")) + var manifest: Dictionary = _read_json( + normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME) + ) + var migrated_root_id: String = str(manifest.get("root_id", "")) if migrated_root_id.is_empty() or not data_root.use_existing_root(normalized): return {"ok": false, "message": "Migration completed but could not switch roots."} - var recovery := ProjectSettings.globalize_path("user://migration-recovery").path_join( + var recovery: String = ProjectSettings.globalize_path( + "user://migration-recovery" + ).path_join( Time.get_datetime_string_from_system().replace(":", "-") ) DirAccess.make_dir_recursive_absolute(recovery) @@ -94,7 +98,7 @@ static func migrate_active_to( data_root: PlayerDataRoot, destination: String, ) -> Dictionary: - var normalized := destination.simplify_path().trim_suffix("/") + var normalized: String = destination.simplify_path().trim_suffix("/") if normalized == data_root.root_path: return {"ok": true, "message": "This data folder is already active."} if DirAccess.dir_exists_absolute(normalized) and not _directory_empty(normalized): @@ -108,12 +112,12 @@ static func migrate_active_to( ), } return {"ok": false, "message": "Choose an empty folder or a NETFISHING subfolder."} - var staging := "%s.migration-%s" % [ + var staging: String = "%s.migration-%s" % [ normalized, Crypto.new().generate_random_bytes(8).hex_encode() ] if DirAccess.make_dir_recursive_absolute(staging) != OK: return {"ok": false, "message": "Migration staging could not be created."} - var created := data_root.create_unbound_root(staging) + var created: Dictionary = data_root.create_unbound_root(staging) if not bool(created.get("ok", false)): _remove_tree(staging) return {"ok": false, "message": str(created.get("message", ""))} @@ -127,7 +131,7 @@ static func migrate_active_to( "social/server_trust.json", "social/host_bans.json", ]: - var source := data_root.root_path.path_join(relative) + var source: String = data_root.root_path.path_join(relative) if not FileAccess.file_exists(source): continue if not bool(_copy_verified_json(source, staging.path_join(relative)).get("ok", false)): @@ -138,7 +142,7 @@ static func migrate_active_to( if DirAccess.rename_absolute(staging, normalized) != OK: _remove_tree(staging) return {"ok": false, "message": "Migration could not activate its destination."} - var old_root := data_root.root_path + var old_root: String = data_root.root_path if not data_root.use_existing_root(normalized): return {"ok": false, "message": "Migration copied data but did not change the pointer."} return { @@ -149,18 +153,20 @@ static func migrate_active_to( static func adopt_legacy_app_data(data_root: PlayerDataRoot) -> Dictionary: - var source_root := ProjectSettings.globalize_path("user://").trim_suffix("/") - var root := ProjectSettings.globalize_path(PlayerDataRoot.APP_DATA_PORTABLE_PATH) + var source_root: String = ProjectSettings.globalize_path("user://").trim_suffix("/") + var root: String = ProjectSettings.globalize_path( + PlayerDataRoot.APP_DATA_PORTABLE_PATH + ) if DirAccess.make_dir_recursive_absolute(root) != OK: return {"ok": false, "message": "The app-data folder could not be created."} - var created := data_root.create_app_data_layout_for_migration(root) + var created: Dictionary = data_root.create_app_data_layout_for_migration(root) if not bool(created.get("ok", false)): return created for source_name: String in LEGACY_FILES: - var source := source_root.path_join(source_name) + var source: String = source_root.path_join(source_name) if not FileAccess.file_exists(source): continue - var target := root.path_join(LEGACY_FILES[source_name]) + var target: String = root.path_join(str(LEGACY_FILES[source_name])) if not bool(_copy_verified_json(source, target).get("ok", false)): return { "ok": false, @@ -179,17 +185,19 @@ static func replace_existing_with_legacy( data_root: PlayerDataRoot, destination: String, ) -> Dictionary: - var normalized := destination.simplify_path().trim_suffix("/") - var manifest := normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME) + var normalized: String = destination.simplify_path().trim_suffix("/") + var manifest: String = normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME) if not FileAccess.file_exists(manifest): return {"ok": false, "message": "The selected folder is not a NETFISHING data root."} - var recovery := ProjectSettings.globalize_path("user://migration-recovery").path_join( + var recovery: String = ProjectSettings.globalize_path( + "user://migration-recovery" + ).path_join( "replaced-root-" + Time.get_datetime_string_from_system().replace(":", "-") ) if not _copy_tree(normalized, recovery): return {"ok": false, "message": "The selected data could not be backed up."} _remove_tree(normalized) - var result := migrate_legacy_to(data_root, normalized) + var result: Dictionary = migrate_legacy_to(data_root, normalized) if bool(result.get("ok", false)): result["replaced_root_backup"] = recovery return result @@ -199,26 +207,28 @@ static func replace_existing_with_active( data_root: PlayerDataRoot, destination: String, ) -> Dictionary: - var normalized := destination.simplify_path().trim_suffix("/") + var normalized: String = destination.simplify_path().trim_suffix("/") if not FileAccess.file_exists(normalized.path_join(PlayerDataRoot.MANIFEST_FILENAME)): return {"ok": false, "message": "The selected folder is not a NETFISHING data root."} - var recovery := ProjectSettings.globalize_path("user://migration-recovery").path_join( + var recovery: String = ProjectSettings.globalize_path( + "user://migration-recovery" + ).path_join( "replaced-root-" + Time.get_datetime_string_from_system().replace(":", "-") ) if not _copy_tree(normalized, recovery): return {"ok": false, "message": "The selected data could not be backed up."} _remove_tree(normalized) - var result := migrate_active_to(data_root, normalized) + var result: Dictionary = migrate_active_to(data_root, normalized) if bool(result.get("ok", false)): result["replaced_root_backup"] = recovery return result static func _copy_verified_json(source: String, destination: String) -> Dictionary: - var bytes := PortableFileGuard.read_bytes(source) + var bytes: PackedByteArray = PortableFileGuard.read_bytes(source) if bytes.is_empty() and FileAccess.get_open_error() != OK: return {"ok": false} - var json := JSON.new() + var json: JSON = JSON.new() if ( json.parse(bytes.get_string_from_utf8()) != OK or typeof(json.data) != TYPE_DICTIONARY @@ -229,7 +239,7 @@ static func _copy_verified_json(source: String, destination: String) -> Dictiona return {"ok": false} if not _write_bytes(destination, bytes): return {"ok": false} - var copied := PortableFileGuard.read_bytes(destination) + var copied: PackedByteArray = PortableFileGuard.read_bytes(destination) return { "ok": ( PortableFileGuard.hash_bytes(copied) @@ -240,7 +250,7 @@ static func _copy_verified_json(source: String, destination: String) -> Dictiona static func _valid_owned_data(filename: String, data: Dictionary) -> bool: if filename == "player_save.json": - var version := int(data.get("save_version", -1)) + var version: int = int(data.get("save_version", -1)) return version >= 1 and version <= PlayerSaveManager.SAVE_VERSION return ( filename not in LEGACY_FILES @@ -255,14 +265,14 @@ static func _copy_bytes(source: String, destination: String) -> bool: static func _copy_tree(source: String, destination: String) -> bool: if DirAccess.make_dir_recursive_absolute(destination) != OK: return false - var access := DirAccess.open(source) + var access: DirAccess = DirAccess.open(source) if access == null: return false access.list_dir_begin() - var name := access.get_next() + var name: String = access.get_next() while not name.is_empty(): - var from := source.path_join(name) - var to := destination.path_join(name) + var from: String = source.path_join(name) + var to: String = destination.path_join(name) if access.current_is_dir(): if not _copy_tree(from, to): access.list_dir_end() @@ -277,19 +287,19 @@ static func _copy_tree(source: String, destination: String) -> bool: static func _write_bytes(path: String, bytes: PackedByteArray) -> bool: DirAccess.make_dir_recursive_absolute(path.get_base_dir()) - var file := FileAccess.open(path, FileAccess.WRITE) + var file: FileAccess = FileAccess.open(path, FileAccess.WRITE) if file == null: return false file.store_buffer(bytes) file.flush() - var ok := file.get_error() == OK + var ok: bool = file.get_error() == OK file.close() return ok static func _read_json(path: String) -> Dictionary: - var bytes := PortableFileGuard.read_bytes(path) - var json := JSON.new() + var bytes: PackedByteArray = PortableFileGuard.read_bytes(path) + var json: JSON = JSON.new() return ( json.data if json.parse(bytes.get_string_from_utf8()) == OK @@ -299,23 +309,23 @@ static func _read_json(path: String) -> Dictionary: static func _directory_empty(path: String) -> bool: - var access := DirAccess.open(path) + var access: DirAccess = DirAccess.open(path) if access == null: return true access.list_dir_begin() - var name := access.get_next() + var name: String = access.get_next() access.list_dir_end() return name.is_empty() static func _remove_tree(path: String) -> void: - var access := DirAccess.open(path) + var access: DirAccess = DirAccess.open(path) if access == null: return access.list_dir_begin() - var name := access.get_next() + var name: String = access.get_next() while not name.is_empty(): - var child := path.path_join(name) + var child: String = path.path_join(name) if access.current_is_dir(): _remove_tree(child) else: diff --git a/network/portable_file_guard.gd b/network/portable_file_guard.gd index 95f21aa..cb99ae1 100644 --- a/network/portable_file_guard.gd +++ b/network/portable_file_guard.gd @@ -7,19 +7,21 @@ const MAX_PORTABLE_FILE_BYTES := 16 * 1024 * 1024 static func hash_file(path: String) -> String: if not FileAccess.file_exists(path): return "" - var file := FileAccess.open(path, FileAccess.READ) + var file: FileAccess = FileAccess.open(path, FileAccess.READ) if file == null or file.get_length() > MAX_PORTABLE_FILE_BYTES: return "" - var bytes := file.get_buffer(file.get_length()) + var bytes: PackedByteArray = file.get_buffer(file.get_length()) file.close() return hash_bytes(bytes) static func hash_bytes(bytes: PackedByteArray) -> String: - var context := HashingContext.new() - if context.start(HashingContext.HASH_SHA256) != OK: + var context: HashingContext = HashingContext.new() + var start_error: Error = context.start(HashingContext.HASH_SHA256) + if start_error != OK: return "" - if context.update(bytes) != OK: + var update_error: Error = context.update(bytes) + if update_error != OK: return "" return context.finish().hex_encode() @@ -27,10 +29,10 @@ static func hash_bytes(bytes: PackedByteArray) -> String: static func read_bytes(path: String, maximum_bytes: int = MAX_PORTABLE_FILE_BYTES) -> PackedByteArray: if not FileAccess.file_exists(path): return PackedByteArray() - var file := FileAccess.open(path, FileAccess.READ) + var file: FileAccess = FileAccess.open(path, FileAccess.READ) if file == null or file.get_length() > maximum_bytes: return PackedByteArray() - var bytes := file.get_buffer(file.get_length()) + var bytes: PackedByteArray = file.get_buffer(file.get_length()) file.close() return bytes @@ -42,10 +44,10 @@ static func write_guarded( conflict_directory: String, device_id: String, ) -> Dictionary: - var current_exists := FileAccess.file_exists(path) - var current_hash := hash_file(path) if current_exists else "" + var current_exists: bool = FileAccess.file_exists(path) + var current_hash: String = hash_file(path) if current_exists else "" if current_hash != expected_hash: - var conflict_path := _write_conflict_copy( + var conflict_path: String = _write_conflict_copy( path, bytes, conflict_directory, device_id ) return { @@ -60,14 +62,14 @@ static func write_guarded( } if not _ensure_parent(path): return {"ok": false, "conflict": false, "hash": expected_hash} - var temporary := path + ".tmp" - var backup := path + ".backup" - var file := FileAccess.open(temporary, FileAccess.WRITE) + var temporary: String = path + ".tmp" + var backup: String = path + ".backup" + var file: FileAccess = FileAccess.open(temporary, FileAccess.WRITE) if file == null: return {"ok": false, "conflict": false, "hash": expected_hash} file.store_buffer(bytes) file.flush() - var write_error := file.get_error() + var write_error: Error = file.get_error() file.close() if write_error != OK: _remove(temporary) @@ -90,13 +92,13 @@ static func write_guarded( static func has_syncthing_conflict(directory: String) -> bool: - var access := DirAccess.open(directory) + var access: DirAccess = DirAccess.open(directory) if access == null: return false access.list_dir_begin() - var name := access.get_next() + var name: String = access.get_next() while not name.is_empty(): - var lower := name.to_lower() + var lower: String = name.to_lower() if "sync-conflict" in lower or ".syncthing." in lower: access.list_dir_end() return true @@ -113,24 +115,24 @@ static func _write_conflict_copy( ) -> String: if DirAccess.make_dir_recursive_absolute(conflict_directory) != OK: return "" - var filename := canonical_path.get_file() - var safe_device := device_id.left(16) - var timestamp := Time.get_datetime_string_from_system().replace(":", "-") - var destination := conflict_directory.path_join( + var filename: String = canonical_path.get_file() + var safe_device: String = device_id.left(16) + var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-") + var destination: String = conflict_directory.path_join( "%s.local-%s-%s" % [filename, safe_device, timestamp] ) - var file := FileAccess.open(destination, FileAccess.WRITE) + var file: FileAccess = FileAccess.open(destination, FileAccess.WRITE) if file == null: return "" file.store_buffer(bytes) file.flush() - var ok := file.get_error() == OK + var ok: bool = file.get_error() == OK file.close() return destination if ok else "" static func _ensure_parent(path: String) -> bool: - var parent := path.get_base_dir() + var parent: String = path.get_base_dir() return ( DirAccess.dir_exists_absolute(parent) or DirAccess.make_dir_recursive_absolute(parent) == OK diff --git a/network/saved_server_store.gd b/network/saved_server_store.gd index eadc281..de48c2c 100644 --- a/network/saved_server_store.gd +++ b/network/saved_server_store.gd @@ -323,9 +323,12 @@ func _validate_entry(value: Dictionary, is_saved: bool) -> Dictionary: var endpoint := EndpointParser.parse(endpoint_text) if not endpoint.is_valid(): return {} - var name: String = str(value.get("display_name", "")).strip_edges() + var display_name: String = str( + value.get("display_name", "") + ).strip_edges() if is_saved and ( - name.is_empty() or name.length() > MAX_DISPLAY_NAME_LENGTH + display_name.is_empty() + or display_name.length() > MAX_DISPLAY_NAME_LENGTH ): return {} var result: Dictionary = _make_entry( diff --git a/player/player.gd b/player/player.gd index 36a0ae9..e735557 100644 --- a/player/player.gd +++ b/player/player.gd @@ -425,19 +425,19 @@ func apply_network_teleport(snapshot: Dictionary) -> void: 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") if ( - typeof(position) != TYPE_ARRAY + typeof(snapshot_position) != TYPE_ARRAY or typeof(network_velocity) != TYPE_ARRAY - or position.size() != 3 + or snapshot_position.size() != 3 or network_velocity.size() != 3 ): return {} var parsed_position := Vector3( - float(position[0]), - float(position[1]), - float(position[2]) + float(snapshot_position[0]), + float(snapshot_position[1]), + float(snapshot_position[2]) ) var parsed_velocity := Vector3( float(network_velocity[0]), diff --git a/settings/player_settings.gd b/settings/player_settings.gd index 44c00a3..70cceb1 100644 --- a/settings/player_settings.gd +++ b/settings/player_settings.gd @@ -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] @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.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 @@ -49,6 +50,7 @@ func is_valid() -> bool: func copy() -> PlayerSettings: var result := PlayerSettings.new() result.auto_click_enabled = auto_click_enabled + result.use_readable_interface_font = use_readable_interface_font result.auto_click_interval = auto_click_interval result.mouse_camera_sensitivity = mouse_camera_sensitivity result.controller_camera_sensitivity = controller_camera_sensitivity diff --git a/settings/player_settings_manager.gd b/settings/player_settings_manager.gd index 7e989a7..3cf737e 100644 --- a/settings/player_settings_manager.gd +++ b/settings/player_settings_manager.gd @@ -45,10 +45,17 @@ func load_settings() -> bool: if ( typeof(accessibility.get("auto_click_enabled")) != 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.") var loaded := PlayerSettings.new() 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( accessibility.get("auto_click_interval"), -1.0 @@ -124,6 +131,9 @@ func save_now() -> bool: "accessibility": { "auto_click_enabled": current_settings.auto_click_enabled, "auto_click_interval": current_settings.auto_click_interval, + "use_readable_interface_font": ( + current_settings.use_readable_interface_font + ), }, "camera": { "mouse_sensitivity": current_settings.mouse_camera_sensitivity, diff --git a/ui/fonts/Tuffy-LICENSE.txt b/ui/fonts/Tuffy-LICENSE.txt new file mode 100644 index 0000000..defced0 --- /dev/null +++ b/ui/fonts/Tuffy-LICENSE.txt @@ -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 http://tulrich.com +Karoly Barta bartakarcsi@gmail.com +Michael Evans http://www.evertype.com diff --git a/ui/fonts/Tuffy_Bold.otf b/ui/fonts/Tuffy_Bold.otf new file mode 100644 index 0000000000000000000000000000000000000000..805ef2e20ea2a4b1b4336f70b94b4c1ba86b3f71 GIT binary patch literal 97308 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 literal 0 HcmV?d00001 diff --git a/ui/fonts/Tuffy_Bold.otf.import b/ui/fonts/Tuffy_Bold.otf.import new file mode 100644 index 0000000..04a768a --- /dev/null +++ b/ui/fonts/Tuffy_Bold.otf.import @@ -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={} diff --git a/ui/game_ui.gd b/ui/game_ui.gd index 4f3ec51..acd54c4 100644 --- a/ui/game_ui.gd +++ b/ui/game_ui.gd @@ -184,6 +184,7 @@ func setup_data_and_identity( player_identity: PlayerIdentityStore, host_identity: HostIdentityStore, network_session: NetworkSession, + interface_fonts: InterfaceFontController, ) -> void: for panel: SettingsPanelType in [ _title_settings_panel, _pause_settings_panel @@ -194,6 +195,7 @@ func setup_data_and_identity( player_identity, host_identity, network_session, + interface_fonts, ) @@ -219,7 +221,7 @@ func _process(_delta: float) -> void: }.get(item_id, "") parts.append( "%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.visible = not parts.is_empty() diff --git a/ui/interface_font_controller.gd b/ui/interface_font_controller.gd new file mode 100644 index 0000000..88562e0 --- /dev/null +++ b/ui/interface_font_controller.gd @@ -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 diff --git a/ui/interface_font_controller.gd.uid b/ui/interface_font_controller.gd.uid new file mode 100644 index 0000000..f6b4897 --- /dev/null +++ b/ui/interface_font_controller.gd.uid @@ -0,0 +1 @@ +uid://djmxy6jibi5y3 diff --git a/ui/player_menu.gd b/ui/player_menu.gd index 943ac39..243a858 100644 --- a/ui/player_menu.gd +++ b/ui/player_menu.gd @@ -194,8 +194,6 @@ enum CloseReason { @onready var _selection_status: BubbleStatusBubbleType = %SelectionStatus @onready var _offer_status: BubbleStatusBubbleType = %OfferStatus @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 _logbook_section: Control = %LogbookSection @onready var _bag_empty: Label = %BagEmpty @@ -210,9 +208,6 @@ enum CloseReason { @onready var _held_value: Label = %HeldValue @onready var _cooler_count: Label = %CoolerCount @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_name: Label = %DetailName @onready var _detail_data: Label = %DetailData @@ -227,8 +222,6 @@ enum CloseReason { @onready var _cancel_sale_button: Button = %CancelSaleButton @onready var _logbook_empty: Label = %LogbookEmpty @onready var _logbook_grid: GridContainer = %LogbookGrid -@onready var _status_shoal: HBoxContainer = %StatusShoal -@onready var _sort_bar: HBoxContainer = %SortBar var _compact_layout: bool = false var _player: PlayerType @@ -1181,6 +1174,7 @@ func _update_shell_layout() -> void: _logbook_page.size = reference_size _logbook_page.position = Vector2.ZERO _logbook_rest_position = Vector2.ZERO + _mail_page.set_anchors_preset(Control.PRESET_TOP_LEFT) _mail_page.size = reference_size _mail_page.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: - _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) _cooler_sort_option.select(_sort_mode) _refresh_inventory() @@ -1891,7 +1888,7 @@ func _layout_bag_items() -> void: var row: int = floori(float(index) / float(columns)) var stable_offset := Vector2( 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 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 stable_offset := Vector2( 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 target := Vector2( diff --git a/ui/settings_panel.gd b/ui/settings_panel.gd index e7a6875..830f2b4 100644 --- a/ui/settings_panel.gd +++ b/ui/settings_panel.gd @@ -48,6 +48,7 @@ enum PresentationMode { @onready var _invert_y_toggle: BubbleButton = %InvertYToggle @onready var _auto_click_toggle: BubbleButton = %AutoClickToggle @onready var _auto_click_interval: BubbleButton = %AutoClickIntervalValue +@onready var _readable_font_toggle: BubbleButton = %ReadableFontToggle @onready var _world_options: Array[BubbleButton] = [ %WorldLegible, @@ -83,6 +84,7 @@ var _data_root: PlayerDataRoot var _identity_backups: IdentityBackupService var _player_identity: PlayerIdentityStore var _host_identity: HostIdentityStore +var _interface_fonts: InterfaceFontController var _data_folder_dialog: FileDialog var _backup_file_dialog: FileDialog var _export_file_dialog: FileDialog @@ -148,6 +150,7 @@ func _ready() -> void: ) _invert_y_toggle.pressed.connect(_toggle_invert_y) _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)) %IntervalIncrease.pressed.connect(_adjust_auto_click_interval.bind(1)) _auto_click_interval.pressed.connect( @@ -185,13 +188,16 @@ func setup_data_and_identity( player_identity: PlayerIdentityStore, host_identity: HostIdentityStore, session: NetworkSession, + interface_fonts: InterfaceFontController, ) -> void: _data_root = data_root _identity_backups = identity_backups _player_identity = player_identity _host_identity = host_identity _network_session = session + _interface_fonts = interface_fonts _identity_backups.operation_finished.connect(_on_identity_operation_finished) + _interface_fonts.apply_utility_theme(_data_page) _refresh_data_page() @@ -410,14 +416,18 @@ func _choose_data_folder() -> void: _data_folder_dialog = FileDialog.new() _data_folder_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR _data_folder_dialog.access = FileDialog.ACCESS_FILESYSTEM + _data_folder_dialog.use_native_dialog = false _data_folder_dialog.dir_selected.connect(_change_data_folder) + _interface_fonts.apply_utility_theme(_data_folder_dialog) add_child(_data_folder_dialog) _data_folder_dialog.current_dir = _data_root.root_path.get_base_dir() _data_folder_dialog.popup_centered_ratio(0.75) 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)): _show_existing_data_folder_choice(path) return @@ -430,7 +440,7 @@ func _change_data_folder(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.ok_button_text = "Use Selected Data" dialog.dialog_text = ( @@ -449,7 +459,7 @@ func _show_existing_data_folder_choice(path: String) -> void: dialog.custom_action.connect(func(action: StringName) -> void: if action != &"replace": return - var result := PortableDataMigration.replace_existing_with_active( + var result: Dictionary = PortableDataMigration.replace_existing_with_active( _data_root, path ) _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") dialog.queue_free() ) + _interface_fonts.apply_utility_theme(dialog) add_child(dialog) dialog.popup_centered(Vector2i(620, 340)) @@ -466,13 +477,15 @@ func _choose_identity_export(identity_type: String) -> void: return _pending_identity_operation = "export" _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: _export_file_dialog = FileDialog.new() _export_file_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE _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.file_selected.connect(_identity_export_file_selected) + _interface_fonts.apply_utility_theme(_export_file_dialog) add_child(_export_file_dialog) _export_file_dialog.current_dir = suggested.get_base_dir() _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.file_mode = FileDialog.FILE_MODE_OPEN_FILE _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.file_selected.connect(_identity_import_file_selected) + _interface_fonts.apply_utility_theme(_backup_file_dialog) add_child(_backup_file_dialog) _backup_file_dialog.current_dir = _data_root.identity_backup_directory() _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.title = "Encrypted identity backup" _passphrase_dialog.confirmed.connect(_submit_identity_passphrase) - var fields := VBoxContainer.new() - var warning := Label.new() + var fields: VBoxContainer = VBoxContainer.new() + var warning: Label = Label.new() warning.text = ( "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 fields.add_child(_passphrase_confirm) _passphrase_dialog.add_child(fields) + _interface_fonts.apply_utility_theme(_passphrase_dialog) add_child(_passphrase_dialog) _passphrase_confirm.visible = exporting _passphrase_entry.clear() @@ -541,7 +557,7 @@ func _show_passphrase_dialog(exporting: bool) -> void: func _submit_identity_passphrase() -> void: - var passphrase := _passphrase_entry.text + var passphrase: String = _passphrase_entry.text if _pending_identity_operation == "export": _identity_backups.export_backup( _pending_identity_type, @@ -550,7 +566,7 @@ func _submit_identity_passphrase() -> void: _passphrase_confirm.text, ) return - var inspected := _identity_backups.import_backup( + var inspected: Dictionary = _identity_backups.import_backup( _pending_identity_type, _pending_identity_path, passphrase, @@ -566,7 +582,7 @@ func _submit_identity_passphrase() -> void: func _show_identity_replacement_confirmation() -> void: - var dialog := ConfirmationDialog.new() + var dialog: ConfirmationDialog = ConfirmationDialog.new() dialog.title = "Replace active identity?" dialog.ok_button_text = "Review Replacement" dialog.dialog_text = ( @@ -579,6 +595,7 @@ func _show_identity_replacement_confirmation() -> void: dialog.set_meta("confirmation_step", 1) dialog.confirmed.connect(_advance_identity_replacement.bind(dialog)) dialog.canceled.connect(dialog.queue_free) + _interface_fonts.apply_utility_theme(dialog) add_child(dialog) dialog.popup_centered(Vector2i(620, 360)) @@ -659,6 +676,9 @@ func _load_controls() -> void: _mouse_sensitivity = settings.mouse_camera_sensitivity _controller_sensitivity = settings.controller_camera_sensitivity _invert_camera_y = settings.invert_camera_y + _readable_font_toggle.button_pressed = ( + settings.use_readable_interface_font + ) _refresh_value_labels() @@ -686,6 +706,14 @@ func _refresh_value_labels() -> void: "accessibility\nauto-click\n" + ("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\ninterval\n%.2f s" % _auto_click_interval_value ) @@ -709,6 +737,19 @@ func _set_world_pixelation(pixel_size: int) -> void: _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: if _settings_manager == null: return diff --git a/ui/settings_panel.tscn b/ui/settings_panel.tscn index 3055d3f..f41cba8 100644 --- a/ui/settings_panel.tscn +++ b/ui/settings_panel.tscn @@ -486,8 +486,8 @@ grow_horizontal = 2 grow_vertical = 2 script = ExtResource("3_page") 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")]) -focus_paths = Array[NodePath]([NodePath("BubbleCluster/AutoClickToggle"), NodePath("BubbleCluster/IntervalDecrease"), NodePath("BubbleCluster/AutoClickIntervalValue"), NodePath("BubbleCluster/IntervalIncrease"), 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/ReadableFontToggle"), NodePath("BubbleCluster/AccessibilityBackButton")]) initial_focus_path = NodePath("BubbleCluster/AutoClickToggle") back_focus_path = NodePath("BubbleCluster/AccessibilityBackButton") @@ -562,6 +562,41 @@ minimum_font_size = 13 maximum_font_size = 22 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")] unique_name_in_owner = true custom_minimum_size = Vector2(0, 0)