Add portable player data and identity backups
This commit is contained in:
parent
141b54261d
commit
ba4476511f
23 changed files with 2132 additions and 237 deletions
|
|
@ -178,6 +178,25 @@ func setup(
|
|||
_fishing_shop.menu_visibility_changed.connect(_on_shop_visibility_changed)
|
||||
|
||||
|
||||
func setup_data_and_identity(
|
||||
data_root: PlayerDataRoot,
|
||||
identity_backups: IdentityBackupService,
|
||||
player_identity: PlayerIdentityStore,
|
||||
host_identity: HostIdentityStore,
|
||||
network_session: NetworkSession,
|
||||
) -> void:
|
||||
for panel: SettingsPanelType in [
|
||||
_title_settings_panel, _pause_settings_panel
|
||||
]:
|
||||
panel.setup_data_and_identity(
|
||||
data_root,
|
||||
identity_backups,
|
||||
player_identity,
|
||||
host_identity,
|
||||
network_session,
|
||||
)
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _item_effects == null or not _gameplay_ui_enabled:
|
||||
_effect_status.hide()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ const PAGE_ROOT: StringName = &"root"
|
|||
const PAGE_DISPLAY: StringName = &"display"
|
||||
const PAGE_CONTROLS: StringName = &"controls"
|
||||
const PAGE_ACCESSIBILITY: StringName = &"accessibility"
|
||||
const PAGE_DATA: StringName = &"data"
|
||||
const TITLE_HOST_OUTGOING_DURATION: float = 1.70
|
||||
const TITLE_HOST_INCOMING_DURATION: float = 1.80
|
||||
const PIXELATION_NAMES: PackedStringArray = [
|
||||
|
|
@ -37,6 +38,7 @@ enum PresentationMode {
|
|||
@onready var _display_page: SettingsBubblePage = %DisplayPage
|
||||
@onready var _controls_page: SettingsBubblePage = %ControlsPage
|
||||
@onready var _accessibility_page: SettingsBubblePage = %AccessibilityPage
|
||||
@onready var _data_page: SettingsBubblePage = %DataPage
|
||||
@onready var _feedback: Label = %SettingsFeedback
|
||||
|
||||
@onready var _world_value: BubbleButton = %WorldValue
|
||||
|
|
@ -77,6 +79,20 @@ var _controller_sensitivity: float = 2.5
|
|||
var _invert_camera_y: bool = false
|
||||
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 _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:
|
||||
|
|
@ -85,21 +101,31 @@ func _ready() -> void:
|
|||
PAGE_DISPLAY: _display_page,
|
||||
PAGE_CONTROLS: _controls_page,
|
||||
PAGE_ACCESSIBILITY: _accessibility_page,
|
||||
PAGE_DATA: _data_page,
|
||||
}
|
||||
%DisplayCategory.pressed.connect(_push_page.bind(PAGE_DISPLAY))
|
||||
%ControlsCategory.pressed.connect(_push_page.bind(PAGE_CONTROLS))
|
||||
%AccessibilityCategory.pressed.connect(
|
||||
_push_page.bind(PAGE_ACCESSIBILITY)
|
||||
)
|
||||
%DataCategory.pressed.connect(_push_page.bind(PAGE_DATA))
|
||||
%ApplySettingsButton.pressed.connect(_apply_settings)
|
||||
%RootBackButton.pressed.connect(close_panel)
|
||||
%DisplayBackButton.pressed.connect(handle_back)
|
||||
%ControlsBackButton.pressed.connect(handle_back)
|
||||
%AccessibilityBackButton.pressed.connect(handle_back)
|
||||
%DataBackButton.pressed.connect(handle_back)
|
||||
%RootBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
%DisplayBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
%ControlsBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
%AccessibilityBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
%DataBackButton.gui_input.connect(_on_back_bubble_gui_input)
|
||||
%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"))
|
||||
%ExportHostIdentity.pressed.connect(_choose_identity_export.bind("host"))
|
||||
%ImportHostIdentity.pressed.connect(_choose_identity_import.bind("host"))
|
||||
for index: int in _world_options.size():
|
||||
_world_options[index].pressed.connect(
|
||||
_set_world_pixelation.bind(index + 1)
|
||||
|
|
@ -153,6 +179,22 @@ func setup_network_profile(
|
|||
_network_session = session
|
||||
|
||||
|
||||
func setup_data_and_identity(
|
||||
data_root: PlayerDataRoot,
|
||||
identity_backups: IdentityBackupService,
|
||||
player_identity: PlayerIdentityStore,
|
||||
host_identity: HostIdentityStore,
|
||||
session: NetworkSession,
|
||||
) -> void:
|
||||
_data_root = data_root
|
||||
_identity_backups = identity_backups
|
||||
_player_identity = player_identity
|
||||
_host_identity = host_identity
|
||||
_network_session = session
|
||||
_identity_backups.operation_finished.connect(_on_identity_operation_finished)
|
||||
_refresh_data_page()
|
||||
|
||||
|
||||
func open_panel(
|
||||
settings_manager: SettingsManagerType,
|
||||
presentation_mode: PresentationMode = PresentationMode.GAMEPLAY_MODAL,
|
||||
|
|
@ -169,6 +211,7 @@ func open_panel(
|
|||
_settings_manager = settings_manager
|
||||
_load_controls()
|
||||
_feedback.text = ""
|
||||
_refresh_data_page()
|
||||
show()
|
||||
_page_stack.clear()
|
||||
_page_stack.append(PAGE_ROOT)
|
||||
|
|
@ -334,6 +377,247 @@ func _show_active_page(should_focus: bool) -> void:
|
|||
page.show_page(should_focus)
|
||||
else:
|
||||
page.hide_page()
|
||||
if get_active_page_id() == PAGE_DATA:
|
||||
_refresh_data_page()
|
||||
if should_focus:
|
||||
%OpenDataFolder.call_deferred("grab_focus")
|
||||
|
||||
|
||||
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())
|
||||
)
|
||||
|
||||
|
||||
func _open_data_folder() -> void:
|
||||
if _data_root == null or not _data_root.open_folder():
|
||||
_feedback.text = "Could not open the data folder."
|
||||
|
||||
|
||||
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.dir_selected.connect(_change_data_folder)
|
||||
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)
|
||||
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 := 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()
|
||||
)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(620, 340))
|
||||
|
||||
|
||||
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 := _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.filters = PackedStringArray(["*.nfidentity ; NETFISHING identity backup"])
|
||||
_export_file_dialog.file_selected.connect(_identity_export_file_selected)
|
||||
add_child(_export_file_dialog)
|
||||
_export_file_dialog.current_dir = suggested.get_base_dir()
|
||||
_export_file_dialog.current_file = suggested.get_file()
|
||||
_export_file_dialog.popup_centered_ratio(0.75)
|
||||
|
||||
|
||||
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.filters = PackedStringArray(["*.nfidentity ; NETFISHING identity backup"])
|
||||
_backup_file_dialog.file_selected.connect(_identity_import_file_selected)
|
||||
add_child(_backup_file_dialog)
|
||||
_backup_file_dialog.current_dir = _data_root.identity_backup_directory()
|
||||
_backup_file_dialog.popup_centered_ratio(0.75)
|
||||
|
||||
|
||||
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)
|
||||
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))
|
||||
_passphrase_entry.grab_focus()
|
||||
|
||||
|
||||
func _submit_identity_passphrase() -> void:
|
||||
var passphrase := _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 := _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)
|
||||
add_child(dialog)
|
||||
dialog.popup_centered(Vector2i(620, 360))
|
||||
|
||||
|
||||
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))
|
||||
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 _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 _get_active_page() -> SettingsBubblePage:
|
||||
|
|
|
|||
|
|
@ -49,8 +49,8 @@ grow_horizontal = 2
|
|||
grow_vertical = 2
|
||||
script = ExtResource("3_page")
|
||||
page_id = &"root"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")])
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/DataCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/DisplayCategory"), NodePath("BubbleCluster/ControlsCategory"), NodePath("BubbleCluster/AccessibilityCategory"), NodePath("BubbleCluster/DataCategory"), NodePath("BubbleCluster/ApplySettingsButton"), NodePath("BubbleCluster/RootBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/DisplayCategory")
|
||||
back_focus_path = NodePath("BubbleCluster/RootBackButton")
|
||||
compact_maximum_layout_size = Vector2(544, 400)
|
||||
|
|
@ -128,6 +128,18 @@ minimum_font_size = 15
|
|||
maximum_font_size = 19
|
||||
motion_phase = 3.7
|
||||
|
||||
[node name="DataCategory" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
text = "data &\nidentity"
|
||||
neutral_size = Vector2(140, 132)
|
||||
desktop_anchor = Vector2(610, 220)
|
||||
compact_anchor = Vector2(540, 205)
|
||||
compact_minimum_size = Vector2(116, 108)
|
||||
minimum_font_size = 14
|
||||
maximum_font_size = 21
|
||||
motion_phase = 5.4
|
||||
|
||||
[node name="RootBackButton" parent="RootPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
custom_minimum_size = Vector2(0, 0)
|
||||
|
|
@ -561,3 +573,118 @@ compact_minimum_size = Vector2(82, 78)
|
|||
minimum_font_size = 13
|
||||
maximum_font_size = 16
|
||||
motion_phase = 5.0
|
||||
|
||||
[node name="DataPage" type="Control" parent="."]
|
||||
unique_name_in_owner = true
|
||||
visible = false
|
||||
layout_mode = 1
|
||||
anchors_preset = 15
|
||||
anchor_right = 1.0
|
||||
anchor_bottom = 1.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
script = ExtResource("3_page")
|
||||
page_id = &"data"
|
||||
bubble_paths = Array[NodePath]([NodePath("BubbleCluster/DataBackButton")])
|
||||
focus_paths = Array[NodePath]([NodePath("BubbleCluster/DataBackButton")])
|
||||
initial_focus_path = NodePath("BubbleCluster/DataBackButton")
|
||||
back_focus_path = NodePath("BubbleCluster/DataBackButton")
|
||||
|
||||
[node name="BubbleCluster" type="Control" parent="DataPage"]
|
||||
layout_mode = 0
|
||||
offset_right = 720.0
|
||||
offset_bottom = 520.0
|
||||
script = ExtResource("5_cluster")
|
||||
profile = ExtResource("6_profile")
|
||||
desktop_reference_size = Vector2(720, 520)
|
||||
compact_reference_size = Vector2(608, 448)
|
||||
|
||||
[node name="Paper" type="PanelContainer" parent="DataPage"]
|
||||
layout_mode = 1
|
||||
anchors_preset = 8
|
||||
anchor_left = 0.5
|
||||
anchor_top = 0.5
|
||||
anchor_right = 0.5
|
||||
anchor_bottom = 0.5
|
||||
offset_left = -330.0
|
||||
offset_top = -255.0
|
||||
offset_right = 330.0
|
||||
offset_bottom = 210.0
|
||||
grow_horizontal = 2
|
||||
grow_vertical = 2
|
||||
|
||||
[node name="Content" type="VBoxContainer" parent="DataPage/Paper"]
|
||||
layout_mode = 2
|
||||
theme_override_constants/separation = 10
|
||||
|
||||
[node name="Heading" type="Label" parent="DataPage/Paper/Content"]
|
||||
layout_mode = 2
|
||||
theme_override_font_sizes/font_size = 28
|
||||
text = "data & identity"
|
||||
horizontal_alignment = 1
|
||||
|
||||
[node name="DataFolderLabel" type="Label" parent="DataPage/Paper/Content"]
|
||||
layout_mode = 2
|
||||
text = "Data Folder"
|
||||
|
||||
[node name="DataFolderPath" type="Label" parent="DataPage/Paper/Content"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Unavailable"
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="DataStorageMode" type="Label" parent="DataPage/Paper/Content"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "storage mode: unavailable"
|
||||
|
||||
[node name="OpenDataFolder" type="Button" parent="DataPage/Paper/Content"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Open Data Folder"
|
||||
|
||||
[node name="ChangeDataFolder" type="Button" parent="DataPage/Paper/Content"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Change Data Folder"
|
||||
|
||||
[node name="IdentityHelper" type="Label" parent="DataPage/Paper/Content"]
|
||||
layout_mode = 2
|
||||
text = "Active identity keys stay on this device.\nEncrypted backups can be stored in your synced data folder.\nDo not play the same profile on two devices at the same time."
|
||||
autowrap_mode = 2
|
||||
|
||||
[node name="IdentityGrid" type="GridContainer" parent="DataPage/Paper/Content"]
|
||||
layout_mode = 2
|
||||
columns = 2
|
||||
|
||||
[node name="ExportPlayerIdentity" type="Button" parent="DataPage/Paper/Content/IdentityGrid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Export Player Identity"
|
||||
|
||||
[node name="ImportPlayerIdentity" type="Button" parent="DataPage/Paper/Content/IdentityGrid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Import Player Identity"
|
||||
|
||||
[node name="ExportHostIdentity" type="Button" parent="DataPage/Paper/Content/IdentityGrid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Export Host Identity"
|
||||
|
||||
[node name="ImportHostIdentity" type="Button" parent="DataPage/Paper/Content/IdentityGrid"]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 2
|
||||
text = "Import Host Identity"
|
||||
|
||||
[node name="DataBackButton" parent="DataPage/BubbleCluster" instance=ExtResource("4_bubble")]
|
||||
unique_name_in_owner = true
|
||||
layout_mode = 0
|
||||
text = "back"
|
||||
neutral_size = Vector2(96, 90)
|
||||
desktop_anchor = Vector2(135, 470)
|
||||
compact_anchor = Vector2(115, 420)
|
||||
compact_minimum_size = Vector2(82, 78)
|
||||
minimum_font_size = 13
|
||||
maximum_font_size = 16
|
||||
motion_phase = 5.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue