straywild/ui/save_slots_page.gd

737 lines
24 KiB
GDScript

class_name SaveSlotsPage
extends Control
const PAGE_SAVES: StringName = &"saves"
const PAGE_NEW: StringName = &"new"
const WorldLayoutType = preload("res://world/world_layout.gd")
const SaveManagerType = preload("res://save/player_save_manager.gd")
const NewGameSetupPageType = preload("res://ui/new_game_setup_page.gd")
const DialogControllerNavigationType = preload(
"res://ui/file_dialog_controller_navigation.gd"
)
signal back_requested
signal play_requested(slot_id: String)
signal create_requested(
display_name: String,
world_layout: StringName,
world_seed: int,
duplicate_source_slot_id: String,
)
enum SeedMode {
RANDOM,
CUSTOM,
}
@onready var _main_panel: PanelContainer = %MainPanel
@onready var _content_panel: PanelContainer = %ContentPanel
@onready var _saves_tab: OrganizerTab = %SavesTab
@onready var _new_slot_tab: OrganizerTab = %NewSlotTab
@onready var _saves_page: Control = %SavesPage
@onready var _new_slot_page: Control = %NewSlotPage
@onready var _slot_list: VBoxContainer = %SlotList
@onready var _empty_slots_label: Label = %EmptySlotsLabel
@onready var _slot_name_edit: LineEdit = %SelectedSlotName
@onready var _slot_summary: RichTextLabel = %SlotSummary
@onready var _play_button: Button = %PlaySlotButton
@onready var _rename_button: Button = %RenameSlotButton
@onready var _duplicate_button: Button = %DuplicateSlotButton
@onready var _export_button: Button = %ExportSlotButton
@onready var _delete_button: Button = %DeleteSlotButton
@onready var _import_button: Button = %ImportSlotButton
@onready var _new_slot_heading: Label = %NewSlotHeading
@onready var _new_slot_name: LineEdit = %NewSlotName
@onready var _generated_button: Button = %GeneratedButton
@onready var _starter_button: Button = %StarterButton
@onready var _world_description: Label = %WorldDescription
@onready var _seed_section: VBoxContainer = %SeedSection
@onready var _random_seed_button: Button = %RandomSeedButton
@onready var _custom_seed_button: Button = %CustomSeedButton
@onready var _seed_edit: LineEdit = %SeedEdit
@onready var _seed_help: Label = %SeedHelp
@onready var _create_button: Button = %CreateSlotButton
@onready var _status: Label = %Status
@onready var _back_button: Button = %BackButton
var _catalog: PlayerSaveSlotCatalog
var _data_root: PlayerDataRoot
var _interface_fonts: InterfaceFontController
var _active_page_id: StringName = PAGE_SAVES
var _selected_slot_id := ""
var _duplicate_source_slot_id := ""
var _world_layout: StringName = WorldLayoutType.GENERATED
var _seed_mode: SeedMode = SeedMode.RANDOM
var _random_seed: int = SaveManagerType.DEFAULT_WORLD_SEED
var _slot_buttons: Array[Button] = []
var _import_dialog: FileDialog
var _export_dialog: FileDialog
var _delete_dialog: ConfirmationDialog
var _busy: bool = false
func _ready() -> void:
_configure_style()
_saves_tab.pressed.connect(_select_page.bind(PAGE_SAVES, true))
_new_slot_tab.pressed.connect(_open_fresh_slot_page)
_play_button.pressed.connect(_request_play)
_rename_button.pressed.connect(_rename_selected_slot)
_duplicate_button.pressed.connect(_prepare_duplicate)
_export_button.pressed.connect(_choose_export_path)
_delete_button.pressed.connect(_confirm_delete_selected_slot)
_import_button.pressed.connect(_choose_import_path)
_generated_button.pressed.connect(
_set_world_layout.bind(WorldLayoutType.GENERATED)
)
_starter_button.pressed.connect(
_set_world_layout.bind(WorldLayoutType.STARTER_ISLAND)
)
_random_seed_button.pressed.connect(_choose_random_seed)
_custom_seed_button.pressed.connect(_choose_custom_seed)
_new_slot_name.text_changed.connect(_on_new_slot_name_changed)
_seed_edit.text_changed.connect(_on_seed_text_changed)
_seed_edit.text_submitted.connect(_on_seed_submitted)
_new_slot_name.text_submitted.connect(_on_new_slot_name_submitted)
_create_button.pressed.connect(_request_create)
_back_button.pressed.connect(request_back)
visibility_changed.connect(_on_visibility_changed)
hide()
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)
_play_button.add_theme_stylebox_override(
"normal",
UtilityPageStyle.ocean_button_style(
UtilityPageStyle.GREEN,
),
)
_delete_button.add_theme_stylebox_override(
"normal",
UtilityPageStyle.ocean_button_style(
UtilityPageStyle.OCEAN_DANGER,
),
)
_back_button.add_theme_stylebox_override(
"normal",
UtilityPageStyle.ocean_button_style(
UtilityPageStyle.OCEAN_PANEL_DEEP,
),
)
_status.add_theme_color_override(
"font_color",
UtilityPageStyle.OCEAN_TEXT_SECONDARY,
)
func setup(
catalog: PlayerSaveSlotCatalog,
data_root: PlayerDataRoot,
interface_fonts: InterfaceFontController,
) -> void:
_catalog = catalog
_data_root = data_root
_interface_fonts = interface_fonts
if not _catalog.slots_changed.is_connected(_on_slots_changed):
_catalog.slots_changed.connect(_on_slots_changed)
func open_page() -> void:
_busy = false
_status.text = ""
_duplicate_source_slot_id = ""
_refresh_slots()
if _catalog != null and _catalog.has_slots():
_select_page(PAGE_SAVES, false)
else:
_prepare_fresh_slot()
_select_page(PAGE_NEW, false)
show()
_main_panel.modulate.a = 1.0
_main_panel.scale = Vector2.ONE
UtilityPageStyle.animate_in(self)
call_deferred("_focus_open_page")
func close_page() -> void:
_release_owned_focus()
hide()
func request_back() -> void:
if _import_dialog != null and _import_dialog.visible:
_import_dialog.hide()
return
if _export_dialog != null and _export_dialog.visible:
_export_dialog.hide()
return
if _delete_dialog != null and _delete_dialog.visible:
_delete_dialog.hide()
return
if _busy:
return
if _active_page_id == PAGE_NEW and _catalog != null and _catalog.has_slots():
_duplicate_source_slot_id = ""
_select_page(PAGE_SAVES, true)
return
back_requested.emit()
func set_status(message: String) -> void:
_busy = false
_status.text = message
_refresh_action_state()
func finish_request_if_pending(message: String) -> void:
if _busy:
set_status(message)
func refresh_page() -> void:
if visible:
_refresh_slots()
func get_active_page_id() -> StringName:
return _active_page_id if visible else StringName()
func _select_page(page_id: StringName, focus_tab: bool) -> void:
if page_id not in [PAGE_SAVES, PAGE_NEW]:
return
_active_page_id = page_id
_saves_page.visible = page_id == PAGE_SAVES
_new_slot_page.visible = page_id == PAGE_NEW
_saves_tab.set_selected(page_id == PAGE_SAVES, is_inside_tree())
_new_slot_tab.set_selected(page_id == PAGE_NEW, is_inside_tree())
_configure_controller_focus()
if focus_tab:
(_saves_tab if page_id == PAGE_SAVES else _new_slot_tab).grab_focus()
func _open_fresh_slot_page() -> void:
_prepare_fresh_slot()
_select_page(PAGE_NEW, true)
func _prepare_fresh_slot() -> void:
_duplicate_source_slot_id = ""
_new_slot_heading.text = "new progression"
_new_slot_name.text = _suggested_slot_name()
_world_layout = WorldLayoutType.GENERATED
_seed_mode = SeedMode.RANDOM
_roll_random_seed()
_refresh_new_slot_presentation()
func _prepare_duplicate() -> void:
var slot: Dictionary = _selected_slot()
if slot.is_empty() or not bool(slot.get("has_save", false)):
return
_duplicate_source_slot_id = _selected_slot_id
_new_slot_heading.text = "duplicate progression"
_new_slot_name.text = PlayerSaveSlotCatalog.normalized_name(
"copy of %s" % str(slot.get("display_name", "save"))
)
_world_layout = WorldLayoutType.GENERATED
_seed_mode = SeedMode.RANDOM
_roll_random_seed()
_refresh_new_slot_presentation()
_select_page(PAGE_NEW, true)
func _refresh_slots() -> void:
for button: Button in _slot_buttons:
_slot_list.remove_child(button)
button.queue_free()
_slot_buttons.clear()
if _catalog == null:
_selected_slot_id = ""
_empty_slots_label.show()
_refresh_selected_slot()
return
var slots: Array[Dictionary] = _catalog.list_slots()
var known_selection: bool = false
for slot: Dictionary in slots:
var slot_id: String = str(slot.get("slot_id", ""))
var button := Button.new()
button.name = "Slot_%s" % slot_id.left(8)
button.custom_minimum_size = Vector2(0.0, 56.0)
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.focus_mode = Control.FOCUS_ALL
button.toggle_mode = true
button.text = str(slot.get("display_name", "save")) + (
" · active" if bool(slot.get("active", false)) else ""
)
button.alignment = HORIZONTAL_ALIGNMENT_LEFT
UtilityPageStyle.apply_ocean_button(button)
button.pressed.connect(_select_slot.bind(slot_id, false))
button.focus_entered.connect(_select_slot.bind(slot_id, true))
_slot_list.add_child(button)
_slot_buttons.append(button)
known_selection = known_selection or slot_id == _selected_slot_id
_empty_slots_label.visible = slots.is_empty()
if not known_selection:
_selected_slot_id = _catalog.get_active_slot_id()
if _selected_slot_id.is_empty() and not slots.is_empty():
_selected_slot_id = str(slots.front().get("slot_id", ""))
_refresh_selected_slot()
_configure_controller_focus()
func _select_slot(slot_id: String, _from_focus: bool) -> void:
if _catalog == null or _catalog.get_slot(slot_id).is_empty():
return
_selected_slot_id = slot_id
_refresh_selected_slot()
func _refresh_selected_slot() -> void:
var slot: Dictionary = _selected_slot()
for button: Button in _slot_buttons:
button.set_pressed_no_signal(
button.name == "Slot_%s" % _selected_slot_id.left(8)
)
if slot.is_empty():
_slot_name_edit.text = ""
_slot_name_edit.editable = false
_slot_summary.text = "[center]create or import a save slot to begin.[/center]"
else:
_slot_name_edit.editable = true
_slot_name_edit.text = str(slot.get("display_name", "save"))
_slot_summary.text = _format_slot_summary(slot)
_refresh_action_state()
func _format_slot_summary(slot: Dictionary) -> String:
if not bool(slot.get("has_save", false)):
return "[center]%s[/center]" % str(
slot.get("status_message", "this slot has no progression yet.")
)
var last_played: String = _format_timestamp(
int(slot.get("last_played_at_unix", 0))
)
return (
"[font_size=24]level %d[/font_size]\n\n"
+ "%d catches · %d discovered\n"
+ "%d fishcoins\n\n"
+ "%s · seed %d\n"
+ "last played %s"
) % [
int(slot.get("player_level", 1)),
int(slot.get("catch_count", 0)),
int(slot.get("discovered_species_count", 0)),
int(slot.get("wallet_balance", 0)),
WorldLayoutType.display_name(slot.get("world_layout", "")),
int(slot.get("world_seed", 0)),
last_played,
]
func _refresh_action_state() -> void:
var slot: Dictionary = _selected_slot()
var has_slot: bool = not slot.is_empty()
var has_save: bool = has_slot and bool(slot.get("has_save", false))
_play_button.disabled = _busy or not has_save
_rename_button.disabled = _busy or not has_slot
_duplicate_button.disabled = _busy or not has_save
_export_button.disabled = _busy or not has_save
_delete_button.disabled = _busy or not has_slot
_import_button.disabled = _busy or _catalog == null
_create_button.disabled = _busy or not _new_slot_request_is_valid()
func _request_play() -> void:
if _play_button.disabled or _selected_slot_id.is_empty():
return
_busy = true
_status.text = "loading save slot..."
_refresh_action_state()
play_requested.emit(_selected_slot_id)
func _rename_selected_slot() -> void:
if _catalog == null or _rename_button.disabled:
return
if not _catalog.rename_slot(_selected_slot_id, _slot_name_edit.text):
_status.text = "the save slot could not be renamed."
return
_status.text = "save slot renamed."
func _confirm_delete_selected_slot() -> void:
var slot: Dictionary = _selected_slot()
if slot.is_empty() or _delete_button.disabled:
return
if _delete_dialog == null:
_delete_dialog = ConfirmationDialog.new()
_delete_dialog.title = "delete save slot?"
_delete_dialog.ok_button_text = "delete"
_delete_dialog.confirmed.connect(_delete_selected_slot)
if _interface_fonts != null:
_interface_fonts.apply_utility_theme(_delete_dialog)
add_child(_delete_dialog)
_delete_dialog.dialog_text = (
"delete \"%s\"? this cannot be undone from the game."
% str(slot.get("display_name", "save"))
)
_delete_dialog.popup_centered(Vector2i(560, 260))
_configure_delete_dialog.call_deferred()
func _configure_delete_dialog() -> void:
if (
_delete_dialog != null
and is_instance_valid(_delete_dialog)
and _delete_dialog.visible
):
DialogControllerNavigationType.configure_scope(
_delete_dialog,
_delete_dialog.get_cancel_button(),
)
func _delete_selected_slot() -> void:
if _catalog == null or _selected_slot_id.is_empty():
return
if not _catalog.delete_slot(_selected_slot_id):
_status.text = "the save slot could not be deleted."
return
_selected_slot_id = _catalog.get_active_slot_id()
_status.text = "save slot deleted."
_refresh_slots()
if not _catalog.has_slots():
_prepare_fresh_slot()
_select_page(PAGE_NEW, true)
func _choose_import_path() -> void:
if _catalog == null or _data_root == null:
return
if _import_dialog == null:
_import_dialog = FileDialog.new()
_import_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
_import_dialog.access = FileDialog.ACCESS_FILESYSTEM
_import_dialog.use_native_dialog = false
_import_dialog.filters = PackedStringArray([
"*.nfsave ; straywild progression archive",
])
_import_dialog.file_selected.connect(_import_file_selected)
if _interface_fonts != null:
_interface_fonts.apply_utility_theme(_import_dialog)
add_child(_import_dialog)
_import_dialog.current_dir = _data_root.progression_backup_directory()
if _interface_fonts != null:
_interface_fonts.popup_file_dialog(_import_dialog)
else:
_import_dialog.popup_centered_ratio(0.85)
func _import_file_selected(path: String) -> void:
var suggested_name: String = path.get_file().get_basename().replace("_", " ")
suggested_name = suggested_name.replace("-", " ")
var result: Dictionary = _catalog.import_slot(path, suggested_name)
_status.text = str(result.get("message", "progression import failed."))
if bool(result.get("ok", false)):
_selected_slot_id = str(result.get("slot_id", ""))
_refresh_slots()
_select_page(PAGE_SAVES, false)
func _choose_export_path() -> void:
var slot: Dictionary = _selected_slot()
if slot.is_empty() or _data_root == null:
return
var timestamp: String = Time.get_datetime_string_from_system().replace(":", "-")
var filename: String = "%s-%s%s" % [
_safe_filename(str(slot.get("display_name", "save"))),
timestamp,
PlayerSaveManager.ARCHIVE_EXTENSION,
]
if _export_dialog == null:
_export_dialog = FileDialog.new()
_export_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
_export_dialog.access = FileDialog.ACCESS_FILESYSTEM
_export_dialog.use_native_dialog = false
_export_dialog.filters = PackedStringArray([
"*.nfsave ; straywild progression archive",
])
_export_dialog.file_selected.connect(_export_file_selected)
if _interface_fonts != null:
_interface_fonts.apply_utility_theme(_export_dialog)
add_child(_export_dialog)
_export_dialog.current_dir = _data_root.progression_backup_directory()
_export_dialog.current_file = filename
if _interface_fonts != null:
_interface_fonts.popup_file_dialog(_export_dialog)
else:
_export_dialog.popup_centered_ratio(0.85)
func _export_file_selected(path: String) -> void:
var destination: String = (
path
if path.ends_with(PlayerSaveManager.ARCHIVE_EXTENSION)
else path + PlayerSaveManager.ARCHIVE_EXTENSION
)
var result: Dictionary = _catalog.export_slot(
_selected_slot_id,
destination,
)
_status.text = str(result.get("message", "progression export failed."))
func _request_create() -> void:
if _create_button.disabled:
return
var seed: int = _selected_world_seed()
if _world_layout == WorldLayoutType.GENERATED and seed == 0:
_status.text = "enter text or a whole number from 1 to %d." % SaveManagerType.MAX_WORLD_SEED
_seed_edit.grab_focus()
_seed_edit.select_all()
return
if seed == 0:
seed = SaveManagerType.roll_world_seed()
_busy = true
_status.text = "creating save slot..."
_refresh_action_state()
create_requested.emit(
PlayerSaveSlotCatalog.normalized_name(_new_slot_name.text),
_world_layout,
seed,
_duplicate_source_slot_id,
)
func _set_world_layout(layout: StringName) -> void:
if not WorldLayoutType.is_valid(layout):
return
_world_layout = layout
_status.text = ""
_refresh_new_slot_presentation()
func _choose_random_seed() -> void:
_seed_mode = SeedMode.RANDOM
_roll_random_seed()
_status.text = ""
_refresh_new_slot_presentation()
func _choose_custom_seed() -> void:
_seed_mode = SeedMode.CUSTOM
if NewGameSetupPageType.parse_seed_text(_seed_edit.text) == _random_seed:
_seed_edit.text = ""
_status.text = ""
_refresh_new_slot_presentation()
_seed_edit.grab_focus.call_deferred()
_seed_edit.select_all.call_deferred()
func _roll_random_seed() -> void:
_random_seed = SaveManagerType.roll_world_seed()
_seed_edit.text = str(_random_seed)
func _selected_world_seed() -> int:
return (
_random_seed
if _seed_mode == SeedMode.RANDOM
else NewGameSetupPageType.parse_seed_text(_seed_edit.text)
)
func _on_seed_text_changed(_text: String) -> void:
if _seed_mode == SeedMode.CUSTOM:
_status.text = ""
_refresh_action_state()
func _on_new_slot_name_changed(_text: String) -> void:
_refresh_action_state()
func _on_seed_submitted(_text: String) -> void:
_request_create()
func _on_new_slot_name_submitted(_text: String) -> void:
if _world_layout == WorldLayoutType.STARTER_ISLAND:
_request_create()
func _refresh_new_slot_presentation() -> void:
var generated: bool = _world_layout == WorldLayoutType.GENERATED
_generated_button.set_pressed_no_signal(generated)
_starter_button.set_pressed_no_signal(not generated)
_world_description.text = (
"build a new island from terrain chunks.\n"
+ "the same seed always builds the same world."
if generated
else "play on the starter island."
)
_seed_section.visible = generated
_random_seed_button.set_pressed_no_signal(_seed_mode == SeedMode.RANDOM)
_custom_seed_button.set_pressed_no_signal(_seed_mode == SeedMode.CUSTOM)
_seed_edit.editable = _seed_mode == SeedMode.CUSTOM
_seed_edit.focus_mode = (
Control.FOCUS_ALL if _seed_mode == SeedMode.CUSTOM else Control.FOCUS_NONE
)
_seed_help.text = (
"press random seed again to roll another world."
if _seed_mode == SeedMode.RANDOM
else "enter text or a number from 1 to %d."
% SaveManagerType.MAX_WORLD_SEED
)
_create_button.text = (
"duplicate and play"
if not _duplicate_source_slot_id.is_empty()
else "create and play"
)
_refresh_action_state()
_configure_controller_focus()
func _new_slot_request_is_valid() -> bool:
return (
not PlayerSaveSlotCatalog.normalized_name(_new_slot_name.text).is_empty()
and (
_world_layout != WorldLayoutType.GENERATED
or _seed_mode == SeedMode.RANDOM
or NewGameSetupPageType.parse_seed_text(_seed_edit.text) > 0
)
)
func _configure_controller_focus() -> void:
if not is_node_ready():
return
_saves_tab.focus_neighbor_right = _saves_tab.get_path_to(_new_slot_tab)
_new_slot_tab.focus_neighbor_left = _new_slot_tab.get_path_to(_saves_tab)
if _active_page_id == PAGE_SAVES:
var first: Control = _slot_buttons.front() if not _slot_buttons.is_empty() else _import_button
var last: Control = _slot_buttons.back() if not _slot_buttons.is_empty() else _import_button
_saves_tab.focus_neighbor_bottom = _saves_tab.get_path_to(first)
_new_slot_tab.focus_neighbor_bottom = _new_slot_tab.get_path_to(first)
for index: int in _slot_buttons.size():
var button: Button = _slot_buttons[index]
var top: Control = _saves_tab if index == 0 else _slot_buttons[index - 1]
var bottom: Control = _import_button if index == _slot_buttons.size() - 1 else _slot_buttons[index + 1]
_set_neighbors(button, button, _slot_name_edit, top, bottom)
_set_neighbors(_import_button, _import_button, _slot_name_edit, last, _back_button)
_set_neighbors(_slot_name_edit, first, _slot_name_edit, _saves_tab, _play_button)
_set_neighbors(_play_button, _import_button, _play_button, _slot_name_edit, _rename_button)
_set_neighbors(_rename_button, _import_button, _duplicate_button, _play_button, _export_button)
_set_neighbors(_duplicate_button, _rename_button, _duplicate_button, _play_button, _delete_button)
_set_neighbors(_export_button, _import_button, _delete_button, _rename_button, _back_button)
_set_neighbors(_delete_button, _export_button, _delete_button, _duplicate_button, _back_button)
_set_neighbors(_back_button, _back_button, _back_button, _import_button, _back_button)
return
_saves_tab.focus_neighbor_bottom = _saves_tab.get_path_to(_new_slot_name)
_new_slot_tab.focus_neighbor_bottom = _new_slot_tab.get_path_to(_new_slot_name)
var generated: bool = _world_layout == WorldLayoutType.GENERATED
var custom: bool = generated and _seed_mode == SeedMode.CUSTOM
_set_neighbors(_new_slot_name, _new_slot_name, _new_slot_name, _new_slot_tab, _generated_button)
_set_neighbors(_generated_button, _generated_button, _starter_button, _new_slot_name, _random_seed_button if generated else _create_button)
_set_neighbors(_starter_button, _generated_button, _starter_button, _new_slot_name, _custom_seed_button if generated else _back_button)
_set_neighbors(_random_seed_button, _random_seed_button, _custom_seed_button, _generated_button, _seed_edit if custom else _create_button)
_set_neighbors(_custom_seed_button, _random_seed_button, _custom_seed_button, _starter_button, _seed_edit if custom else _back_button)
_set_neighbors(_seed_edit, _seed_edit, _seed_edit, _custom_seed_button, _create_button)
var action_top: Control = _seed_edit if custom else _random_seed_button if generated else _generated_button
_set_neighbors(_create_button, _create_button, _back_button, action_top, _create_button)
_set_neighbors(_back_button, _create_button, _back_button, action_top, _back_button)
func _set_neighbors(
control: Control,
left: Control,
right: Control,
top: Control,
bottom: Control,
) -> void:
control.focus_neighbor_left = control.get_path_to(left)
control.focus_neighbor_right = control.get_path_to(right)
control.focus_neighbor_top = control.get_path_to(top)
control.focus_neighbor_bottom = control.get_path_to(bottom)
func _focus_open_page() -> void:
if not visible:
return
(_saves_tab if _active_page_id == PAGE_SAVES else _new_slot_tab).grab_focus()
func _selected_slot() -> Dictionary:
return _catalog.get_slot(_selected_slot_id) if _catalog != null else {}
func _suggested_slot_name() -> String:
var used: Dictionary[String, bool] = {}
if _catalog != null:
for slot: Dictionary in _catalog.list_slots():
used[str(slot.get("display_name", "")).to_lower()] = true
var number: int = 1
while used.has("save %d" % number):
number += 1
return "save %d" % number
func _format_timestamp(unix_time: int) -> String:
if unix_time <= 0:
return "never"
var value: Dictionary = Time.get_datetime_dict_from_unix_time(unix_time)
return "%04d-%02d-%02d" % [
int(value.get("year", 0)),
int(value.get("month", 0)),
int(value.get("day", 0)),
]
func _safe_filename(value: String) -> String:
var result: String = value.strip_edges().to_lower().replace(" ", "-")
for character: String in ["/", "\\", ":", "*", "?", "\"", "<", ">", "|"]:
result = result.replace(character, "")
return "straywild-save" if result.is_empty() else result
func _on_slots_changed() -> void:
if visible:
_refresh_slots()
func _on_visibility_changed() -> void:
if not visible:
_release_owned_focus()
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()