netfishing/ui/settings_panel.gd

1203 lines
36 KiB
GDScript

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"
)
const ControllerFocusNavigationType = preload(
"res://ui/controller_focus_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 _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 _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(_confirm_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()
_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("_configure_controller_navigation")
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
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
_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()
call_deferred("_configure_controller_navigation")
if focus_tab:
_tabs[page_id].call_deferred("grab_focus")
func _focus_active_tab() -> void:
var tab: OrganizerTab = _tabs.get(_active_page_id)
if tab != null:
tab.grab_focus()
func _configure_controller_navigation() -> void:
if not is_node_ready() or not visible:
return
var controls: Array[Control] = []
_collect_focusable_controls(_main_panel, controls)
ControllerFocusNavigationType.configure_spatial_neighbors(controls)
func _collect_focusable_controls(
node: Node,
controls: Array[Control],
) -> void:
for child: Node in node.get_children():
var control := child as Control
if control != null and not control.is_visible_in_tree():
continue
if (
control != null
and ControllerFocusNavigationType.is_focusable(control)
):
controls.append(control)
_collect_focusable_controls(child, controls)
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)
)
call_deferred("_configure_controller_navigation")
func _open_data_folder() -> void:
if _data_root == null or not _data_root.open_folder():
_feedback.text = "could not open the data folder."
func _confirm_open_data_folder() -> void:
if _data_root == null:
_feedback.text = "the data folder is unavailable."
return
var dialog := ConfirmationDialog.new()
dialog.title = "open data folder?"
dialog.ok_button_text = "open folder"
dialog.cancel_button_text = "cancel"
dialog.dialog_text = (
"open this folder outside NETfishing?\n\n" + _data_root.root_path
)
dialog.confirmed.connect(func() -> void:
_open_data_folder()
dialog.queue_free()
)
dialog.canceled.connect(dialog.queue_free)
if _interface_fonts != null:
_interface_fonts.apply_utility_theme(dialog)
add_child(dialog)
dialog.popup_centered(Vector2i(620, 300))
_configure_confirmation_dialog.call_deferred(
dialog,
dialog.get_cancel_button(),
)
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.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
_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 settings.chat_dock_right else 0)
_chat_mode.select(1 if settings.chat_mobile_mode else 0)
_paint_dock.select(1 if settings.paint_dock_right else 0)
_fullscreen_toggle.set_pressed_no_signal(settings.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:
_set_presentation_toggle(
&"chat_dock_right",
_chat_dock.get_item_id(index) == 1,
)
func _on_chat_mode_selected(index: int) -> void:
_set_presentation_toggle(
&"chat_mobile_mode",
_chat_mode.get_item_id(index) == 1,
)
func _on_paint_dock_selected(index: int) -> void:
_set_presentation_toggle(
&"paint_dock_right",
_paint_dock.get_item_id(index) == 1,
)
func _set_fullscreen(enabled: bool) -> void:
_set_presentation_toggle(&"fullscreen_enabled", enabled)
_refresh_value_labels()
func _set_presentation_toggle(
property_name: StringName,
enabled: bool,
) -> void:
if _settings_manager == null:
return
var edited: PlayerSettings = _settings_manager.current_settings.copy()
edited.set(property_name, enabled)
if not _settings_manager.apply_settings(edited):
_feedback.text = "failed to save display setting."
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