class_name SettingsPanel extends Control const PANEL_SIZE: Vector2 = Vector2(880.0, 660.0) const PANEL_EDGE_MARGIN: float = 16.0 const PAGE_DISPLAY: StringName = &"display" const PAGE_SOUND: StringName = &"sound" const PAGE_CONTROLS: StringName = &"controls" const PAGE_ACCESSIBILITY: StringName = &"accessibility" const PAGE_DATA: StringName = &"data" const PIXELATION_NAMES: PackedStringArray = [ "legible", "cute", "retro", "hardcore", "wtf", ] const SettingsManagerType = preload( "res://settings/player_settings_manager.gd" ) const ControllerMappingManagerType = preload( "res://settings/controller_mapping_manager.gd" ) const ControllerMappingPanelType = preload( "res://ui/controller_mapping_panel.gd" ) const KeyboardMouseMappingManagerType = preload( "res://settings/keyboard_mouse_mapping_manager.gd" ) const KeyboardMouseMappingPanelType = preload( "res://ui/keyboard_mouse_mapping_panel.gd" ) const DialogControllerNavigationType = preload( "res://ui/file_dialog_controller_navigation.gd" ) signal applied signal closed signal closing signal opened signal crisp_reset_focus_requested signal panel_visibility_changed(is_visible: bool) signal navigation_transition_started enum PresentationMode { TITLE_EMBEDDED, GAMEPLAY_MODAL, } @onready var _main_panel: PanelContainer = %MainPanel @onready var _content_panel: PanelContainer = %ContentPanel @onready var _feedback: Label = %SettingsFeedback @onready var _display_page: Control = %DisplayPage @onready var _sound_page: Control = %SoundPage @onready var _controls_page: Control = %ControlsPage @onready var _accessibility_page: Control = %AccessibilityPage @onready var _data_page: Control = %DataPage @onready var _display_tab: OrganizerTab = %DisplayTab @onready var _sound_tab: OrganizerTab = %SoundTab @onready var _controls_tab: OrganizerTab = %ControlsTab @onready var _accessibility_tab: OrganizerTab = %AccessibilityTab @onready var _data_tab: OrganizerTab = %DataTab @onready var _world_pixelation: OptionButton = %WorldPixelationSelector @onready var _ui_pixelation: OptionButton = %UIPixelationSelector @onready var _chat_dock: OptionButton = %ChatDockSelector @onready var _chat_mode: OptionButton = %ChatModeSelector @onready var _paint_dock: OptionButton = %PaintDockSelector @onready var _fullscreen_toggle: Button = %FullscreenToggle @onready var _master_volume_slider: HSlider = %MasterVolumeSlider @onready var _music_volume_slider: HSlider = %MusicVolumeSlider @onready var _effects_volume_slider: HSlider = %EffectsVolumeSlider @onready var _environment_volume_slider: HSlider = %EnvironmentVolumeSlider @onready var _master_volume_value: Label = %MasterVolumeValue @onready var _music_volume_value: Label = %MusicVolumeValue @onready var _effects_volume_value: Label = %EffectsVolumeValue @onready var _environment_volume_value: Label = %EnvironmentVolumeValue @onready var _mouse_sensitivity_slider: HSlider = %MouseSensitivitySlider @onready var _controller_sensitivity_slider: HSlider = ( %ControllerSensitivitySlider ) @onready var _mouse_sensitivity_value: Label = %MouseSensitivityValue @onready var _controller_sensitivity_value: Label = ( %ControllerSensitivityValue ) @onready var _invert_y_toggle: Button = %InvertYToggle @onready var _on_screen_keyboard_toggle: Button = %OnScreenKeyboardToggle @onready var _auto_click_toggle: Button = %AutoClickToggle @onready var _auto_click_interval_slider: HSlider = ( %AutoClickIntervalSlider ) @onready var _auto_click_interval_value_label: Label = ( %AutoClickIntervalValue ) @onready var _apply_button: Button = %ApplySettingsButton @onready var _back_button: Button = %SettingsBackButton var _settings_manager: SettingsManagerType var _effective_ui_pixel_size: int = PlayerSettings.DEFAULT_UI_PIXEL_SIZE var _pages: Dictionary[StringName, Control] = {} var _tabs: Dictionary[StringName, OrganizerTab] = {} var _active_page_id: StringName = PAGE_DISPLAY var _presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL var _animate_gameplay_host_transitions: bool = false var _panel_tween: Tween var _transition_generation: int = 0 var _transition_active: bool = false var _auto_click_enabled: bool = false var _auto_click_interval_value: float = 0.20 var _mouse_sensitivity: float = 0.005 var _controller_sensitivity: float = 2.5 var _invert_camera_y: bool = false var _on_screen_keyboard_enabled: bool = false var _chat_dock_right: bool = false var _chat_mobile_mode: bool = false var _paint_dock_right: bool = true var _fullscreen_enabled: bool = false var _master_volume: float = 1.0 var _music_volume: float = 1.0 var _effects_volume: float = 1.0 var _environment_volume: float = 1.0 var _network_profile: NetworkProfilePreferences var _network_session: NetworkSession var _data_root: PlayerDataRoot var _identity_backups: IdentityBackupService var _player_identity: PlayerIdentityStore var _host_identity: HostIdentityStore var _interface_fonts: InterfaceFontController var _controller_mapping_manager: ControllerMappingManagerType var _controller_mapping_panel: ControllerMappingPanelType var _keyboard_mouse_mapping_manager: KeyboardMouseMappingManagerType var _keyboard_mouse_mapping_panel: KeyboardMouseMappingPanelType var _data_folder_dialog: FileDialog var _backup_file_dialog: FileDialog var _export_file_dialog: FileDialog var _passphrase_dialog: ConfirmationDialog var _passphrase_entry: LineEdit var _passphrase_confirm: LineEdit var _pending_identity_operation := "" var _pending_identity_type := "" var _pending_identity_path := "" var _pending_import_data: Dictionary = {} func _notification(what: int) -> void: if what == NOTIFICATION_VISIBILITY_CHANGED and not visible: _release_owned_focus() func _ready() -> void: _pages = { PAGE_DISPLAY: _display_page, PAGE_SOUND: _sound_page, PAGE_CONTROLS: _controls_page, PAGE_ACCESSIBILITY: _accessibility_page, PAGE_DATA: _data_page, } _tabs = { PAGE_DISPLAY: _display_tab, PAGE_SOUND: _sound_tab, PAGE_CONTROLS: _controls_tab, PAGE_ACCESSIBILITY: _accessibility_tab, PAGE_DATA: _data_tab, } for page_id: StringName in _tabs: _tabs[page_id].pressed.connect(_select_page.bind(page_id, true)) _populate_option_buttons() _connect_controls() _configure_style() _controller_mapping_panel = ControllerMappingPanelType.new() add_child(_controller_mapping_panel) _controller_mapping_panel.closed.connect( _on_controller_mapping_panel_closed ) _keyboard_mouse_mapping_panel = KeyboardMouseMappingPanelType.new() add_child(_keyboard_mouse_mapping_panel) _keyboard_mouse_mapping_panel.closed.connect( _on_keyboard_mouse_mapping_panel_closed ) resized.connect(_refresh_panel_size) var parent_control := get_parent() as Control if parent_control != null: parent_control.resized.connect(_refresh_panel_size) _select_page(PAGE_DISPLAY, false) call_deferred("_refresh_panel_size") func _populate_option_buttons() -> void: for selector: OptionButton in [_world_pixelation, _ui_pixelation]: selector.clear() for index: int in PIXELATION_NAMES.size(): selector.add_item(PIXELATION_NAMES[index], index + 1) _chat_dock.clear() _chat_dock.add_item("left", 0) _chat_dock.add_item("right", 1) _chat_mode.clear() _chat_mode.add_item("desktop", 0) _chat_mode.add_item("mobile", 1) _paint_dock.clear() _paint_dock.add_item("left", 0) _paint_dock.add_item("right", 1) func _connect_controls() -> void: _apply_button.pressed.connect(_apply_settings) _back_button.pressed.connect(close_panel) _world_pixelation.item_selected.connect( _on_world_pixelation_selected ) _ui_pixelation.item_selected.connect(_on_ui_pixelation_selected) _chat_dock.item_selected.connect(_on_chat_dock_selected) _chat_mode.item_selected.connect(_on_chat_mode_selected) _paint_dock.item_selected.connect(_on_paint_dock_selected) _fullscreen_toggle.toggled.connect(_set_fullscreen) _master_volume_slider.value_changed.connect( _on_audio_volume_changed.bind(&"master") ) _music_volume_slider.value_changed.connect( _on_audio_volume_changed.bind(&"music") ) _effects_volume_slider.value_changed.connect( _on_audio_volume_changed.bind(&"effects") ) _environment_volume_slider.value_changed.connect( _on_audio_volume_changed.bind(&"environment") ) _mouse_sensitivity_slider.value_changed.connect( _on_mouse_sensitivity_changed ) _controller_sensitivity_slider.value_changed.connect( _on_controller_sensitivity_changed ) _invert_y_toggle.toggled.connect(_set_invert_y) _on_screen_keyboard_toggle.toggled.connect(_set_on_screen_keyboard) _auto_click_toggle.toggled.connect(_set_auto_click) _auto_click_interval_slider.value_changed.connect( _on_auto_click_interval_changed ) %ControllerMapping.pressed.connect(_open_controller_mapping) %KeyboardMapping.pressed.connect(_open_keyboard_mouse_mapping) %OpenDataFolder.pressed.connect(_open_data_folder) %ChangeDataFolder.pressed.connect(_choose_data_folder) %ExportPlayerIdentity.pressed.connect( _choose_identity_export.bind("player") ) %ImportPlayerIdentity.pressed.connect( _choose_identity_import.bind("player") ) %CopyPlayerFingerprint.pressed.connect(_copy_player_fingerprint) %ExportHostIdentity.pressed.connect( _choose_identity_export.bind("host") ) %ImportHostIdentity.pressed.connect( _choose_identity_import.bind("host") ) func _configure_style() -> void: UtilityPageStyle.apply_page(self) _main_panel.add_theme_stylebox_override( "panel", UtilityPageStyle.rounded_style( UtilityPageStyle.OCEAN_PANEL_MID, 28, ), ) _content_panel.add_theme_stylebox_override( "panel", UtilityPageStyle.rounded_style( UtilityPageStyle.OCEAN_FIELD, 20, ), ) for node: Node in find_children("*", "Label", true, false): var label := node as Label label.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_PRIMARY, ) for node: Node in find_children("*", "Button", true, false): if node is OrganizerTab: continue UtilityPageStyle.apply_ocean_button(node as BaseButton) for node: Node in find_children("*", "OptionButton", true, false): UtilityPageStyle.apply_ocean_button(node as BaseButton) _apply_button.add_theme_stylebox_override( "normal", UtilityPageStyle.ocean_button_style(Color("31594d")), ) _back_button.add_theme_stylebox_override( "normal", UtilityPageStyle.ocean_button_style( UtilityPageStyle.OCEAN_PANEL_DEEP, ), ) _feedback.add_theme_color_override( "font_color", UtilityPageStyle.OCEAN_TEXT_SECONDARY, ) func setup_network_profile( profile: NetworkProfilePreferences, session: NetworkSession, ) -> void: _network_profile = profile _network_session = session func setup_controller_mapping( mapping_manager: ControllerMappingManagerType, ) -> void: _controller_mapping_manager = mapping_manager _controller_mapping_panel.setup(_controller_mapping_manager) func setup_keyboard_mouse_mapping( mapping_manager: KeyboardMouseMappingManagerType, ) -> void: _keyboard_mouse_mapping_manager = mapping_manager _keyboard_mouse_mapping_panel.setup(_keyboard_mouse_mapping_manager) func setup_data_and_identity( data_root: PlayerDataRoot, identity_backups: IdentityBackupService, 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 if not _identity_backups.operation_finished.is_connected( _on_identity_operation_finished ): _identity_backups.operation_finished.connect( _on_identity_operation_finished ) _interface_fonts.apply_utility_theme(_data_page) _refresh_data_page() func open_panel( settings_manager: SettingsManagerType, presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL, animate_gameplay_host_transitions: bool = false, ) -> void: _cancel_panel_transition() _release_owned_focus() _presentation_mode = presentation_mode _animate_gameplay_host_transitions = animate_gameplay_host_transitions _settings_manager = settings_manager _load_controls() _feedback.text = "" _refresh_data_page() _select_page(PAGE_DISPLAY, false) show() panel_visibility_changed.emit(true) _main_panel.modulate.a = 1.0 _main_panel.scale = Vector2.ONE call_deferred("_focus_active_tab") if ( presentation_mode == PresentationMode.TITLE_EMBEDDED or _animate_gameplay_host_transitions ): _animate_panel_open() else: opened.emit() func close_panel(immediate: bool = false) -> void: if not visible: return if immediate: _finish_panel_close(false) return if ( _presentation_mode == PresentationMode.TITLE_EMBEDDED or _animate_gameplay_host_transitions ): _animate_panel_close(false) return _finish_panel_close(false) func _animate_panel_open() -> void: _transition_generation += 1 _transition_active = true var generation: int = _transition_generation _main_panel.pivot_offset = _main_panel.size * 0.5 _main_panel.scale = Vector2.ONE * UIMotion.UTILITY_ENTER_SCALE _main_panel.modulate.a = 0.0 _panel_tween = create_tween() _panel_tween.set_parallel(true) _panel_tween.tween_property( _main_panel, "scale", Vector2.ONE, UIMotion.UTILITY_ENTER_DURATION, ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT) _panel_tween.tween_property( _main_panel, "modulate:a", 1.0, UIMotion.UTILITY_ENTER_DURATION, ) _panel_tween.finished.connect( _finish_panel_open.bind(generation), CONNECT_ONE_SHOT, ) func _finish_panel_open(generation: int) -> void: if generation != _transition_generation or not visible: return _transition_active = false _panel_tween = null opened.emit() func _animate_panel_close(applied_result: bool) -> void: if _transition_active: return _transition_generation += 1 _transition_active = true var generation: int = _transition_generation navigation_transition_started.emit() closing.emit() _main_panel.pivot_offset = _main_panel.size * 0.5 _panel_tween = create_tween() _panel_tween.set_parallel(true) _panel_tween.tween_property( _main_panel, "scale", Vector2.ONE * UIMotion.UTILITY_ENTER_SCALE, UIMotion.UTILITY_EXIT_DURATION, ).set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_IN) _panel_tween.tween_property( _main_panel, "modulate:a", 0.0, UIMotion.UTILITY_EXIT_DURATION, ) _panel_tween.finished.connect( _finish_animated_close.bind(generation, applied_result), CONNECT_ONE_SHOT, ) func _finish_animated_close( generation: int, applied_result: bool, ) -> void: if generation != _transition_generation or not visible: return _finish_panel_close(applied_result) func _finish_panel_close(applied_result: bool) -> void: _cancel_panel_transition() if ( _controller_mapping_panel != null and _controller_mapping_panel.is_open() ): _controller_mapping_panel.close_panel() if ( _keyboard_mouse_mapping_panel != null and _keyboard_mouse_mapping_panel.is_open() ): _keyboard_mouse_mapping_panel.close_panel() _main_panel.modulate.a = 1.0 _main_panel.scale = Vector2.ONE _release_owned_focus() hide() panel_visibility_changed.emit(false) if applied_result: applied.emit() else: _load_controls() closed.emit() func _cancel_panel_transition() -> void: _transition_generation += 1 _transition_active = false if _panel_tween != null: _panel_tween.kill() _panel_tween = null func handle_back() -> void: if ( _keyboard_mouse_mapping_panel != null and _keyboard_mouse_mapping_panel.is_open() ): _keyboard_mouse_mapping_panel.request_back() return if ( _controller_mapping_panel != null and _controller_mapping_panel.is_open() ): _controller_mapping_panel.request_back() return for selector: OptionButton in [ _world_pixelation, _ui_pixelation, _chat_dock, _chat_mode, _paint_dock, ]: if selector.get_popup().visible: selector.get_popup().hide() selector.grab_focus() return if _transition_active: return close_panel() func _select_page(page_id: StringName, focus_tab: bool = true) -> void: if not _pages.has(page_id): return var target_tab: OrganizerTab = _tabs.get(page_id) _active_page_id = page_id for candidate_id: StringName in _pages: _pages[candidate_id].visible = candidate_id == page_id _tabs[candidate_id].set_selected( candidate_id == page_id, is_inside_tree(), ) if page_id == PAGE_DATA: _refresh_data_page() if focus_tab and target_tab != null: target_tab.grab_focus() func _focus_active_tab() -> void: var tab: OrganizerTab = _tabs.get(_active_page_id) if tab != null: tab.grab_focus() func _open_controller_mapping() -> void: if _controller_mapping_manager == null: _feedback.text = "no controller mapping service is available" return _controller_mapping_panel.open_panel() func _on_controller_mapping_panel_closed() -> void: %ControllerMapping.grab_focus() func _open_keyboard_mouse_mapping() -> void: if _keyboard_mouse_mapping_manager == null: _feedback.text = "no keyboard binding service is available" return _keyboard_mouse_mapping_panel.open_panel() func _on_keyboard_mouse_mapping_panel_closed() -> void: %KeyboardMapping.grab_focus() func get_active_page_id() -> StringName: return _active_page_id if visible else StringName() func is_controller_mapping_capturing() -> bool: return ( _controller_mapping_panel != null and _controller_mapping_panel.is_capturing() ) func is_input_mapping_capturing() -> bool: return ( is_controller_mapping_capturing() or ( _keyboard_mouse_mapping_panel != null and _keyboard_mouse_mapping_panel.is_capturing() ) ) func _refresh_data_page() -> void: if not is_node_ready() or _data_root == null: return %DataFolderPath.text = _data_root.root_path %DataStorageMode.text = "storage mode: " + _data_root.storage_mode_text() %ChangeDataFolder.disabled = ( _data_root.override_active or (_network_session != null and _network_session.is_session_active()) ) var fingerprint: String = ( _player_identity.fingerprint if _player_identity != null else "" ) %CopyPlayerFingerprint.disabled = not NetworkIdentityCrypto.valid_fingerprint( fingerprint ) %CopyPlayerFingerprint.text = ( "copy player fingerprint ยท %s" % NetworkIdentityCrypto.compact_suffix(fingerprint) ) func _open_data_folder() -> void: if ( not is_visible_in_tree() or _active_page_id != PAGE_DATA or not _data_page.is_visible_in_tree() or not %OpenDataFolder.is_visible_in_tree() or get_viewport().gui_get_focus_owner() != %OpenDataFolder ): return if _data_root == null: _feedback.text = "the data folder is unavailable." return if ( _data_root.root_path.is_empty() or OS.shell_open(_data_root.root_path) != OK ): _feedback.text = "could not open the data folder." func _release_owned_focus() -> void: if not is_inside_tree(): return var focus_owner: Control = get_viewport().gui_get_focus_owner() if focus_owner != null and is_ancestor_of(focus_owner): focus_owner.release_focus() func _copy_player_fingerprint() -> void: var fingerprint: String = ( _player_identity.fingerprint if _player_identity != null else "" ) if not NetworkIdentityCrypto.valid_fingerprint(fingerprint): _feedback.text = "player identity is unavailable." return DisplayServer.clipboard_set(fingerprint) _feedback.text = "full player fingerprint copied for server operator setup." func _choose_data_folder() -> void: if _data_root == null or _data_root.override_active: _feedback.text = "the data folder is externally managed." return if _network_session != null and _network_session.is_session_active(): _feedback.text = "return to title to change the data folder." return if _data_folder_dialog == null: _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() _interface_fonts.popup_file_dialog(_data_folder_dialog) func _change_data_folder(path: String) -> void: 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 if bool(result.get("ok", false)): _feedback.text = "data folder changed. NETfishing will close safely." _refresh_data_page() get_tree().call_deferred("quit") else: _feedback.text = str( result.get("message", "could not change the data folder.") ) func _show_existing_data_folder_choice(path: String) -> void: var dialog := ConfirmationDialog.new() dialog.title = "existing NETfishing data" dialog.ok_button_text = "use selected data" dialog.dialog_text = ( "the selected folder contains different NETfishing data.\n\n" + "choose one complete data set. data will not be merged." ) dialog.add_button("replace selected data", false, "replace") dialog.confirmed.connect(func() -> void: if _data_root.use_existing_root(path): _feedback.text = "data folder changed. NETfishing will close safely." get_tree().call_deferred("quit") else: _feedback.text = _data_root.error_message dialog.queue_free() ) dialog.custom_action.connect(func(action: StringName) -> void: if action != &"replace": return var result: Dictionary = PortableDataMigration.replace_existing_with_active( _data_root, path ) _feedback.text = str( result.get("message", "could not replace selected data.") ) if bool(result.get("ok", false)): get_tree().call_deferred("quit") dialog.queue_free() ) _interface_fonts.apply_utility_theme(dialog) add_child(dialog) dialog.popup_centered(Vector2i(620, 340)) _configure_confirmation_dialog.call_deferred( dialog, dialog.get_cancel_button() ) func _choose_identity_export(identity_type: String) -> void: if not _identity_operation_allowed(): return _pending_identity_operation = "export" _pending_identity_type = 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() _interface_fonts.popup_file_dialog(_export_file_dialog) func _identity_export_file_selected(path: String) -> void: _pending_identity_path = ( path if path.ends_with(".nfidentity") else path + ".nfidentity" ) _show_passphrase_dialog(true) func _choose_identity_import(identity_type: String) -> void: if not _identity_operation_allowed(): return _pending_identity_operation = "import" _pending_identity_type = identity_type if _backup_file_dialog == null: _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() _interface_fonts.popup_file_dialog(_backup_file_dialog) func _identity_import_file_selected(path: String) -> void: _pending_identity_path = path _show_passphrase_dialog(false) func _show_passphrase_dialog(exporting: bool) -> void: if _passphrase_dialog == null: _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() warning.text = ( "anyone with this backup and passphrase can use your identity." ) warning.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART fields.add_child(warning) _passphrase_entry = LineEdit.new() _passphrase_entry.placeholder_text = "passphrase (12 characters minimum)" _passphrase_entry.secret = true fields.add_child(_passphrase_entry) _passphrase_confirm = LineEdit.new() _passphrase_confirm.placeholder_text = "confirm passphrase" _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() _passphrase_confirm.clear() _passphrase_dialog.dialog_text = ( "create a passphrase-encrypted backup." if exporting else "enter the backup passphrase." ) _passphrase_dialog.popup_centered(Vector2i(560, 300)) _configure_confirmation_dialog.call_deferred( _passphrase_dialog, _passphrase_entry ) func _submit_identity_passphrase() -> void: var passphrase: String = _passphrase_entry.text if _pending_identity_operation == "export": _identity_backups.export_backup( _pending_identity_type, _pending_identity_path, passphrase, _passphrase_confirm.text, ) return var inspected: Dictionary = _identity_backups.import_backup( _pending_identity_type, _pending_identity_path, passphrase, false, ) if bool(inspected.get("requires_confirmation", false)): _pending_import_data = { "passphrase": passphrase, "current": inspected["current_fingerprint"], "incoming": inspected["incoming_fingerprint"], } _show_identity_replacement_confirmation() func _show_identity_replacement_confirmation() -> void: var dialog := ConfirmationDialog.new() dialog.title = "replace active identity?" dialog.ok_button_text = "review replacement" dialog.dialog_text = ( "current:\n%s\n\nincoming:\n%s\n\n" + "other players will recognize this device as the imported identity." ) % [ NetworkIdentityCrypto.format_fingerprint(_pending_import_data["current"]), NetworkIdentityCrypto.format_fingerprint(_pending_import_data["incoming"]), ] 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)) _configure_confirmation_dialog.call_deferred( dialog, dialog.get_cancel_button() ) func _advance_identity_replacement(dialog: ConfirmationDialog) -> void: if int(dialog.get_meta("confirmation_step", 1)) == 1: dialog.set_meta("confirmation_step", 2) dialog.ok_button_text = "replace identity" dialog.dialog_text = ( "replace the active identity and archive the current key locally?" ) dialog.call_deferred("popup_centered", Vector2i(560, 280)) _configure_confirmation_dialog.call_deferred( dialog, dialog.get_cancel_button() ) return _confirm_identity_replacement(dialog) func _confirm_identity_replacement(dialog: ConfirmationDialog) -> void: _identity_backups.import_backup( _pending_identity_type, _pending_identity_path, str(_pending_import_data["passphrase"]), true, ) _pending_import_data.clear() dialog.queue_free() func _configure_confirmation_dialog( dialog: ConfirmationDialog, preferred_control: Control, ) -> void: if dialog != null and is_instance_valid(dialog) and dialog.visible: DialogControllerNavigationType.configure_scope( dialog, preferred_control ) func _identity_operation_allowed() -> bool: if _identity_backups == null: _feedback.text = "identity backup is unavailable." return false if _network_session != null and _network_session.is_session_active(): _feedback.text = "return to title to change an identity." return false return true func _on_identity_operation_finished(_success: bool, message: String) -> void: _feedback.text = message func _apply_settings() -> void: if _transition_active: return if _settings_manager == null: _feedback.text = "settings are unavailable." return var edited: PlayerSettings = _settings_manager.current_settings.copy() edited.auto_click_enabled = _auto_click_enabled edited.auto_click_interval = _auto_click_interval_value edited.mouse_camera_sensitivity = _mouse_sensitivity edited.controller_camera_sensitivity = _controller_sensitivity edited.invert_camera_y = _invert_camera_y edited.on_screen_keyboard_enabled = _on_screen_keyboard_enabled edited.chat_dock_right = _chat_dock_right edited.chat_mobile_mode = _chat_mobile_mode edited.paint_dock_right = _paint_dock_right edited.fullscreen_enabled = _fullscreen_enabled edited.master_volume = _master_volume edited.music_volume = _music_volume edited.effects_volume = _effects_volume edited.environment_volume = _environment_volume if _settings_manager.apply_settings(edited): if ( _presentation_mode == PresentationMode.TITLE_EMBEDDED or _animate_gameplay_host_transitions ): _animate_panel_close(true) else: _finish_panel_close(true) else: _feedback.text = "failed to save settings." func _load_controls() -> void: if _settings_manager == null or not is_node_ready(): return var settings: PlayerSettings = _settings_manager.current_settings _auto_click_enabled = settings.auto_click_enabled _auto_click_interval_value = settings.auto_click_interval _mouse_sensitivity = settings.mouse_camera_sensitivity _controller_sensitivity = settings.controller_camera_sensitivity _invert_camera_y = settings.invert_camera_y _on_screen_keyboard_enabled = settings.on_screen_keyboard_enabled _chat_dock_right = settings.chat_dock_right _chat_mobile_mode = settings.chat_mobile_mode _paint_dock_right = settings.paint_dock_right _fullscreen_enabled = settings.fullscreen_enabled _master_volume = settings.master_volume _music_volume = settings.music_volume _effects_volume = settings.effects_volume _environment_volume = settings.environment_volume _world_pixelation.select(settings.world_pixel_size - 1) _ui_pixelation.select(settings.ui_pixel_size - 1) _chat_dock.select(1 if _chat_dock_right else 0) _chat_mode.select(1 if _chat_mobile_mode else 0) _paint_dock.select(1 if _paint_dock_right else 0) _fullscreen_toggle.set_pressed_no_signal(_fullscreen_enabled) _master_volume_slider.set_value_no_signal(_master_volume * 100.0) _music_volume_slider.set_value_no_signal(_music_volume * 100.0) _effects_volume_slider.set_value_no_signal(_effects_volume * 100.0) _environment_volume_slider.set_value_no_signal( _environment_volume * 100.0 ) _mouse_sensitivity_slider.set_value_no_signal(_mouse_sensitivity) _controller_sensitivity_slider.set_value_no_signal( _controller_sensitivity ) _invert_y_toggle.set_pressed_no_signal(_invert_camera_y) _on_screen_keyboard_toggle.set_pressed_no_signal( _on_screen_keyboard_enabled ) _auto_click_toggle.set_pressed_no_signal(_auto_click_enabled) _auto_click_interval_slider.set_value_no_signal( _auto_click_interval_value ) _settings_manager.restore_audio_levels() _refresh_value_labels() func _refresh_value_labels() -> void: _fullscreen_toggle.text = ( "on" if _fullscreen_toggle.button_pressed else "off" ) _invert_y_toggle.text = "on" if _invert_camera_y else "off" _on_screen_keyboard_toggle.text = ( "on" if _on_screen_keyboard_enabled else "off" ) _auto_click_toggle.text = "on" if _auto_click_enabled else "off" _mouse_sensitivity_value.text = "%.4f" % _mouse_sensitivity _controller_sensitivity_value.text = "%.1f" % _controller_sensitivity _auto_click_interval_value_label.text = ( "%.2f s" % _auto_click_interval_value ) _refresh_audio_value_labels() func _on_audio_volume_changed(value: float, channel: StringName) -> void: var linear_volume: float = clampf(value / 100.0, 0.0, 1.0) match channel: &"master": _master_volume = linear_volume &"music": _music_volume = linear_volume &"effects": _effects_volume = linear_volume &"environment": _environment_volume = linear_volume _refresh_audio_value_labels() if _settings_manager != null: _settings_manager.preview_audio_levels( _master_volume, _music_volume, _effects_volume, _environment_volume, ) func _refresh_audio_value_labels() -> void: _master_volume_value.text = "%d%%" % roundi(_master_volume * 100.0) _music_volume_value.text = "%d%%" % roundi(_music_volume * 100.0) _effects_volume_value.text = "%d%%" % roundi(_effects_volume * 100.0) _environment_volume_value.text = ( "%d%%" % roundi(_environment_volume * 100.0) ) func _on_world_pixelation_selected(index: int) -> void: _set_world_pixelation(_world_pixelation.get_item_id(index)) func _on_ui_pixelation_selected(index: int) -> void: _set_ui_pixelation(_ui_pixelation.get_item_id(index)) func _set_world_pixelation(pixel_size: int) -> void: if _settings_manager == null: return if not _settings_manager.apply_pixelation( pixel_size, _settings_manager.current_settings.ui_pixel_size, ): _feedback.text = "failed to save pixelation settings." func _set_ui_pixelation(pixel_size: int) -> void: if _settings_manager == null: return if not _settings_manager.apply_pixelation( _settings_manager.current_settings.world_pixel_size, pixel_size, ): _feedback.text = "failed to save pixelation settings." func _on_chat_dock_selected(index: int) -> void: _chat_dock_right = _chat_dock.get_item_id(index) == 1 func _on_chat_mode_selected(index: int) -> void: _chat_mobile_mode = _chat_mode.get_item_id(index) == 1 func _on_paint_dock_selected(index: int) -> void: _paint_dock_right = _paint_dock.get_item_id(index) == 1 func _set_fullscreen(enabled: bool) -> void: _fullscreen_enabled = enabled _refresh_value_labels() func _on_mouse_sensitivity_changed(value: float) -> void: _mouse_sensitivity = clampf( value, PlayerSettings.MIN_MOUSE_SENSITIVITY, PlayerSettings.MAX_MOUSE_SENSITIVITY, ) _refresh_value_labels() func _on_controller_sensitivity_changed(value: float) -> void: _controller_sensitivity = clampf( value, PlayerSettings.MIN_CONTROLLER_SENSITIVITY, PlayerSettings.MAX_CONTROLLER_SENSITIVITY, ) _refresh_value_labels() func _set_invert_y(enabled: bool) -> void: _invert_camera_y = enabled _refresh_value_labels() func _set_on_screen_keyboard(enabled: bool) -> void: _on_screen_keyboard_enabled = enabled _refresh_value_labels() func _set_auto_click(enabled: bool) -> void: _auto_click_enabled = enabled _refresh_value_labels() func _on_auto_click_interval_changed(value: float) -> void: _auto_click_interval_value = clampf( value, PlayerSettings.MIN_AUTO_CLICK_INTERVAL, PlayerSettings.MAX_AUTO_CLICK_INTERVAL, ) _refresh_value_labels() func set_effective_ui_pixel_size(pixel_size: int) -> void: _effective_ui_pixel_size = clampi( pixel_size, PlayerSettings.MIN_UI_PIXEL_SIZE, PlayerSettings.MAX_UI_PIXEL_SIZE, ) func focus_back_button() -> void: _back_button.grab_focus() func refresh_open_panel() -> void: if visible: _load_controls() func _refresh_panel_size() -> void: if not is_node_ready(): return custom_minimum_size = PANEL_SIZE var parent_control := get_parent() as Control if parent_control == null or parent_control is CenterContainer: return var available_size: Vector2 = parent_control.size - Vector2.ONE * ( PANEL_EDGE_MARGIN * 2.0 ) var scale_factor: float = minf( 1.0, minf( available_size.x / PANEL_SIZE.x, available_size.y / PANEL_SIZE.y, ), ) var target_size: Vector2 = PANEL_SIZE * maxf(scale_factor, 0.1) set_anchors_preset(Control.PRESET_CENTER) position = (parent_control.size - target_size) * 0.5 size = target_size