## Performs the managing of saves by saving, loading, and returning a list of saves[br][br] ## ## Below is a basic definition of various components and resources for reference:[br] ## * SaveLevelDataComponent - The root save component that provides the Save Settings[br] ## * SaveDataComponent - Attached to a node that has properties that need to be saved[br] ## * Node Type Resources - Each type of node that requires saving, such as a TileMapLayer, needs a resource created extends Node signal apply_complete signal apply_save ## Apply the loaded data to the tree. Note: This should happen AFTER load_save() signal autosave_complete ## Autosave has completed signal autosave_start ## Autosave has started signal create_autosave signal create_save(save_name: String) signal delete_save(filename: String) signal delete_error(error_message: String) signal load_complete signal load_error(error_message: String) signal load_save(filename: String) ## Loads the save into memory. Does NOT apply the load as this allows for other actions (such as resetting the levle/world).[br]Don't forget to run `apply_save` after loading signal save_complete signal save_error(error_message: String) signal start_autosave signal stop_autosave signal quick_save signal quick_load signal toggle_save_icon_generation(toggled: bool) ## Enable/Disable the generation of a screenshot during save for the save icon. var _autosave_timer: Timer = Timer.new() var _enable_save_icon_generation: bool = true var _loaded_save_resource: SaveGameDataResource = SaveGameDataResource.new() var _save_game_settings: SaveGameSettings ## Contains the save paths, filename prepends, and other save settings var _save_icon_size: Vector2i = Vector2i(896, 504) ## If Vector2.ZERO, uses the user's resolution func _ready() -> void: apply_save.connect(_on_apply_save) create_save.connect(_on_save_game_as_resource) delete_save.connect(_on_delete_save) load_save.connect(_on_load_game_save) quick_load.connect(_on_quick_load) quick_save.connect(_on_quick_save) start_autosave.connect(_on_start_autosave) stop_autosave.connect(_on_stop_autosave) toggle_save_icon_generation.connect(_on_toggle_save_icon_generation) _autosave_timer.name = "AutosaveTimer" _autosave_timer.one_shot = false _autosave_timer.timeout.connect(_on_autosave_timer_timeout) add_child(_autosave_timer) func _unhandled_input(event: InputEvent) -> void: if event.is_action_pressed("quick_save"): quick_save.emit() if event.is_action_pressed("quick_load"): quick_load.emit() func list_saves(include_quick_saves: bool = true, include_autosaves: bool = true) -> Array[SaveFileDetailsResource]: var save_files: Array[SaveFileDetailsResource] = [] if not _load_save_settings(): return save_files var save_game_data_path: String = _save_game_settings.save_game_data_path if !DirAccess.dir_exists_absolute(save_game_data_path): return save_files for filename: String in ResourceLoader.list_directory(save_game_data_path): # TODO: Rework so the settings determine the file_name using prepends if filename.begins_with(_save_game_settings.quicksave_file_name_prepend) and not include_quick_saves: continue elif filename.begins_with(_save_game_settings.autosave_file_name_prepend) and not include_autosaves: continue elif !filename.begins_with(_save_game_settings.save_file_name_prepend) and !filename.begins_with(_save_game_settings.quicksave_file_name_prepend) and !filename.begins_with(_save_game_settings.autosave_file_name_prepend): continue var _save_path: String = save_game_data_path + filename var _save_icon: String = filename.replace(".tres", ".png") var _save_resource: SaveFileDetailsResource = SaveFileDetailsResource.new() # TODO: Reconsider loading the save to get the name # This could be a large save making opening each one slow # Should work out a better method for getting the save name (filename != save name) var _save_file: SaveGameDataResource = ResourceLoader.load(_save_path) _save_resource.save_name = _save_file.save_name _save_resource.filename = filename _save_resource.date_created = Time.get_datetime_string_from_unix_time(FileAccess.get_modified_time(_save_path)) ## In 4.5, this can probably be replaced with FileAccess.get_size(_save_path) ## This should reduce the need for opening the file here var _loaded_file: FileAccess = FileAccess.open(_save_path, FileAccess.READ) _save_resource.filesize = _loaded_file.get_length() _save_resource.save_icon_texture = _generate_save_icon_texture(save_game_data_path + _save_icon) save_files.append(_save_resource) save_files.sort_custom(_custom_save_file_sort) return save_files func _create_autosave() -> void: if not _load_save_settings(): return _autosave_timer.stop() autosave_start.emit() _rotate_autosaves() var autosave_filename: String = _save_game_settings.autosave_file_name_prepend + "01.tres" _save_game_as_resource("Auto Save", autosave_filename) autosave_complete.emit() _autosave_timer.start(_save_game_settings.autosave_duration) ## Sort the save files list by date created, descending func _custom_save_file_sort(a: SaveFileDetailsResource, b: SaveFileDetailsResource) -> bool: return a.date_created > b.date_created ## Save the properties defined on the SaveDataComponents attached to various nodes (such as Block) func _generate_save_game_resource() -> SaveGameDataResource: var nodes: Array = get_tree().get_nodes_in_group("save_data_component") if nodes == null: return var _resource: SaveGameDataResource = SaveGameDataResource.new() for node: Node in nodes: if node is SaveDataComponent: @warning_ignore("unsafe_method_access") var save_data_resource: Node3DDataResource = node._save_data() var save_final_resource: Node3DDataResource = save_data_resource.duplicate() _resource.save_data_nodes.append(save_final_resource) return _resource ## Generate the texture for use in the save file listing func _generate_save_icon_texture(save_icon: String) -> Texture2D: var _icon_texture: Texture2D = ImageTexture.new() if save_icon != null and !FileAccess.file_exists(save_icon): _icon_texture = _save_game_settings.default_save_icon_resource elif save_icon == null: _icon_texture = _save_game_settings.default_save_icon_resource else: var _icon_image: Image = Image.new() _icon_image.load(save_icon) @warning_ignore("unsafe_method_access") _icon_texture.set_image(_icon_image) return _icon_texture func _load_game_resource(resource_filename: String) -> void: if not _load_save_settings(): return var save_game_file_path: String = _save_game_settings.save_game_data_path + resource_filename if !FileAccess.file_exists(save_game_file_path): load_error.emit("Failed to load save. File does not exist: %s" % save_game_file_path) return _loaded_save_resource = ResourceLoader.load(save_game_file_path) if _loaded_save_resource == null: load_error.emit("Failed to load save. Unknown format? %s" % save_game_file_path) return load_complete.emit() ## Find the SaveLevelDataComponent within the tree which stores the save settings func _load_save_settings() -> bool: var _save_level_data_component: SaveLevelDataComponent = get_tree().get_first_node_in_group("save_level_data_component") if _save_level_data_component == null: push_error("Could not find SaveLevelDataComponent node in level") return false _save_game_settings = _save_level_data_component.settings return true ## Increment autosave numbers and if we've reach max number of autosaves, remove the oldest one func _rotate_autosaves() -> void: var saves_dir = DirAccess.open(_save_game_settings.save_game_data_path) if saves_dir == null: DirAccess.make_dir_absolute(_save_game_settings.save_game_data_path) var autosaves: Array[String] = [] for filename in saves_dir.get_files(): if !filename.begins_with(_save_game_settings.autosave_file_name_prepend): continue if !filename.ends_with(".tres"): continue autosaves.append(filename) if autosaves.size() == _save_game_settings.max_autosaves: # Delete oldest save DirAccess.remove_absolute(_save_game_settings.save_game_data_path + autosaves.pop_back()) var filepath: String = _save_game_settings.save_game_data_path + _save_game_settings.autosave_file_name_prepend for index in range(autosaves.size(), 0, -1): var old_save_path: String = "%s%02d.tres" % [filepath, index] var old_screenshot_path: String = "%s%02d.png" % [filepath, index] var new_save_path: String = "%s%02d.tres" % [filepath, index + 1] var new_screenshot_path: String = "%s%02d.png" % [filepath, index + 1] DirAccess.copy_absolute(old_save_path, new_save_path) if FileAccess.file_exists(old_screenshot_path): DirAccess.copy_absolute(old_screenshot_path, new_screenshot_path) func _save_game_as_resource(save_name, resource_filename: String) -> void: if not _load_save_settings(): return if !DirAccess.dir_exists_absolute(_save_game_settings.save_game_data_path): DirAccess.make_dir_absolute(_save_game_settings.save_game_data_path) var save_game_file_path: String = _save_game_settings.save_game_data_path + resource_filename var _save_resource: SaveGameDataResource = _generate_save_game_resource() _save_resource.save_name = save_name var result: int = ResourceSaver.save(_save_resource, save_game_file_path) if result != OK: save_error.emit("Failed to save game (" , result, "): " + save_game_file_path) return _take_save_screenshot(save_game_file_path) save_complete.emit() ## Takes a screenshot and saves next to the save file[br] ## The icon utilizes the same filename as the save file, replacing `.tres` with `.png` func _take_save_screenshot(save_game_file_path: String) -> void: if !_enable_save_icon_generation: return var _icon_filepath: String = save_game_file_path.replace(".tres", ".png") var _icon: Image = get_viewport().get_texture().get_image() if _save_icon_size != Vector2i.ZERO: _icon.resize(_save_icon_size.x, _save_icon_size.y) _icon.save_png(_icon_filepath) ## Apply the loaded save. Should be performed after load_save func _on_apply_save() -> void: if _loaded_save_resource == null: return var root_node: Window = get_tree().root for resource: Resource in _loaded_save_resource.save_data_nodes: if resource is Node3DDataResource: (resource as Node3DDataResource)._load_data(root_node) apply_complete.emit() func _on_autosave_timer_timeout() -> void: _create_autosave() ## Delete both the save file and the related screenshot func _on_delete_save(filename: String) -> void: if filename.length() < 1: delete_error.emit("Empty filename provided") return var save_file_path: String = _save_game_settings.save_game_data_path + filename DirAccess.remove_absolute(save_file_path) DirAccess.remove_absolute(save_file_path.replace(".tres", ".png")) # Delete icon func _on_load_game_save(resource_filename: String) -> void: _load_game_resource(resource_filename) func _on_quick_load() -> void: if not _load_save_settings(): return _load_game_resource(_save_game_settings.quicksave_file_name_prepend + "game_data.tres") func _on_quick_save() -> void: if not _load_save_settings(): return _save_game_as_resource("Quick Save", _save_game_settings.quicksave_file_name_prepend + "game_data.tres") ## Save the game, with a filename of `.tres func _on_save_game_as_resource(save_name: String) -> void: if not _load_save_settings(): return var current_date: String = Time.get_datetime_string_from_system().replace(":", "") var _filename: String = _save_game_settings.save_file_name_prepend + current_date + ".tres" _loaded_save_resource.save_name = save_name _save_game_as_resource(save_name, _filename) save_complete.emit() func _on_start_autosave() -> void: if _save_game_settings == null: _load_save_settings() _autosave_timer.start(_save_game_settings.autosave_duration) func _on_stop_autosave() -> void: _autosave_timer.stop() func _on_toggle_save_icon_generation(toggled: bool) -> void: _enable_save_icon_generation = toggled