class_name SaveGameList extends Control signal _picked(save_game: SaveGame) @export var saves: Array[SaveGame] var save_buttons: Array[SaveGameDisplay] @export var update_display: bool: set(value): _load_games() @onready var list_container: VBoxContainer = %ListContainer @onready var back_button: Button = $MarginContainer/VBoxContainer/HBoxContainer/Button func _validate_property(property: Dictionary) -> void: if property.name == "saves": property.usage = PROPERTY_USAGE_READ_ONLY + PROPERTY_USAGE_SCRIPT_VARIABLE + PROPERTY_USAGE_EDITOR var _tween: Tween = null func _ready() -> void: _load_games() hide() set_process_input(false) back_button.pressed.connect(cancel) func _ensure_directory() -> void: var dir := DirAccess.open(State.user_saves_path) # Create dir if needed. if DirAccess.get_open_error() == ERR_INVALID_PARAMETER: dir = DirAccess.open("user://") dir.make_dir_recursive(State.user_saves_path) if DirAccess.get_open_error() != OK: printerr("Error while opening User Save Directory: %s" % error_string(DirAccess.get_open_error())) func _load_games(): _ensure_directory() saves = [] var save_game_dir := DirAccess.open(State.user_saves_path) var filepaths: PackedStringArray = save_game_dir.get_files() for path in filepaths: if path is String and path.ends_with(".json"): var save := SaveGame.new("%s/%s" % [State.user_saves_path, path.get_basename()]) # HACK: Skip empty saves (we decide later what to do with them) if not save.is_empty: saves.append(save) _sort_saves() _rebuild_buttons() func _sort_saves() -> void: saves.sort_custom(func(a: SaveGame, b: SaveGame) -> int: return a.last_saved > b.last_saved ) func _rebuild_buttons() -> void: save_buttons = [] for child in list_container.get_children(): child.queue_free() var save_box := VBoxContainer.new() save_box.add_theme_constant_override("separation", 16) list_container.add_child(save_box) for i in range(saves.size()): var new_button := SaveGameDisplay.new(saves[i], i+1) save_box.add_child(new_button) save_buttons.append(new_button) new_button.pressed.connect(_on_game_picked.bind(i)) new_button.delete_requested.connect(_on_delete_requested.bind(i)) func _on_game_picked(id: int) -> void: _picked.emit(saves[id]) func _on_delete_requested(id: int) -> void: var save_to_delete := saves[id] var save_path := save_to_delete.filepath var thumbnail_path := "%s/thumbnails/%s.png" % [save_path.get_base_dir(), save_to_delete.unique_save_name] # Delete the save file if FileAccess.file_exists(save_path): DirAccess.remove_absolute(save_path) print_debug("Deleted save file: %s" % save_path) # Delete the thumbnail if FileAccess.file_exists(thumbnail_path): DirAccess.remove_absolute(thumbnail_path) print_debug("Deleted thumbnail: %s" % thumbnail_path) # Reload the save list _load_games() # Refocus on a valid button if any exist if save_buttons.size() > 0: var focus_index := mini(id, save_buttons.size() - 1) save_buttons[focus_index].grab_focus() func get_most_recent_save() -> SaveGame: _sort_saves() return saves[0] if saves.size() > 0 else SaveGame.new() func has_more_saves() -> bool: for save in saves: if save != State.save_game: return true return false func _input(event: InputEvent) -> void: if event.is_action_pressed("ui_cancel"): cancel() func cancel()->void: _picked.emit(State.save_game) # This function is called when the user us supposed to choose a slot to load or create a new game. func pick_save_slot() -> SaveGame: await open() var result = await _picked await close() return result # TODO: ugh, godot tweens are the wurst func open() -> void: show() save_buttons[0].grab_focus() modulate = Color.TRANSPARENT if _tween != null: _tween.kill() _tween = create_tween() _tween.tween_property(self, "modulate", Color.WHITE, 0.5) await _tween.finished func close() -> void: if _tween != null: _tween.kill() _tween = create_tween() _tween.tween_property(self, "modulate", Color.TRANSPARENT, 0.5) await _tween.finished hide()