class_name SaveSlotStore extends RefCounted const DEFAULT_DIRECTORY := "user://saves" const DEFAULT_SLOT := "quicksave" const MAX_SAVE_BYTES := 16 * 1024 * 1024 var save_directory: String var last_error := "" func _init(directory: String = DEFAULT_DIRECTORY) -> void: save_directory = directory func save(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool: last_error = "" if ( manager == null or not ( manager.has_method("serialize_save_manifest") or manager.has_method("serialize_state") ) ): return _fail("Save source cannot serialize authoritative state") if not _is_valid_slot_name(slot_name): return _fail("Invalid save slot name") var use_manifest := manager.has_method("serialize_save_manifest") if use_manifest and manager.has_method("uses_combined_save_manifest"): use_manifest = bool(manager.call("uses_combined_save_manifest")) if ( not use_manifest and manager.has_method("can_serialize_local_state_only") and not bool(manager.call("can_serialize_local_state_only")) ): return _fail("Save source has regional authority outside its active manifest scope") var json_text: String if use_manifest: json_text = String(manager.call("serialize_save_manifest")) elif manager.has_method("serialize_state"): json_text = String(manager.call("serialize_state")) else: return _fail("Save source cannot serialize its active authority scope") if json_text.to_utf8_buffer().size() > MAX_SAVE_BYTES: return _fail("Save exceeds the supported size limit") if not _is_valid_save_json(json_text): return _fail("Simulation produced an invalid save payload") if not _ensure_directory(): return false var final_path := get_slot_path(slot_name) var temporary_path := final_path + ".tmp" var backup_path := final_path + ".bak" _remove_if_present(temporary_path) var file := FileAccess.open(temporary_path, FileAccess.WRITE) if file == null: return _fail("Could not open temporary save file") file.store_string(json_text) file.flush() file.close() if _read_payload(temporary_path).is_empty(): _remove_if_present(temporary_path) return _fail("Temporary save failed validation") var had_existing := FileAccess.file_exists(final_path) if had_existing: _remove_if_present(backup_path) if DirAccess.rename_absolute(final_path, backup_path) != OK: _remove_if_present(temporary_path) return _fail("Could not preserve the previous save") if DirAccess.rename_absolute(temporary_path, final_path) != OK: if had_existing: DirAccess.rename_absolute(backup_path, final_path) _remove_if_present(temporary_path) return _fail("Could not install the new save") _remove_if_present(backup_path) return true func load_into(manager: Node, slot_name: String = DEFAULT_SLOT) -> bool: last_error = "" if manager == null: return _fail("Save target is missing") if not _is_valid_slot_name(slot_name): return _fail("Invalid save slot name") var final_path := get_slot_path(slot_name) var payload := _read_payload(final_path) if payload.is_empty(): payload = _read_payload(final_path + ".bak") if payload.is_empty(): return _fail("Save slot is missing, invalid, or unsupported") if payload.has("manifest"): if not manager.has_method("restore_save_manifest"): return _fail("Save target cannot restore a combined save manifest") if not bool(manager.call("restore_save_manifest", payload["manifest"])): return _fail("Simulation refused the validated save manifest") elif payload.has("local_state"): if not manager.has_method("restore_state"): return _fail("Save target cannot restore legacy local state") if not bool(manager.call("restore_state", payload["local_state"])): return _fail("Simulation refused the validated legacy save record") else: return _fail("Save slot payload kind is unsupported") return true func has_slot(slot_name: String = DEFAULT_SLOT) -> bool: if not _is_valid_slot_name(slot_name): return false var final_path := get_slot_path(slot_name) return ( not _read_payload(final_path).is_empty() or not _read_payload(final_path + ".bak").is_empty() ) func delete_slot(slot_name: String = DEFAULT_SLOT) -> bool: if not _is_valid_slot_name(slot_name): return false var final_path := get_slot_path(slot_name) _remove_if_present(final_path) _remove_if_present(final_path + ".tmp") _remove_if_present(final_path + ".bak") return true func get_slot_path(slot_name: String = DEFAULT_SLOT) -> String: return save_directory.path_join(slot_name + ".json") func _read_payload(path: String) -> Dictionary: if not FileAccess.file_exists(path): return {} var file := FileAccess.open(path, FileAccess.READ) if file == null: return {} if file.get_length() > MAX_SAVE_BYTES: file.close() return {} var json_text := file.get_as_text() file.close() var manifest := SimulationSaveManifest.from_json(json_text) if manifest != null: return {"manifest": manifest} var local_state := SimulationStateRecord.from_json(json_text) return {"local_state": local_state} if local_state != null else {} func _is_valid_save_json(json_text: String) -> bool: return ( SimulationSaveManifest.from_json(json_text) != null or SimulationStateRecord.from_json(json_text) != null ) func _ensure_directory() -> bool: var absolute_directory := ProjectSettings.globalize_path(save_directory) var error := DirAccess.make_dir_recursive_absolute(absolute_directory) if error != OK and error != ERR_ALREADY_EXISTS: return _fail("Could not create the save directory") return true func _is_valid_slot_name(slot_name: String) -> bool: if slot_name.is_empty() or slot_name.length() > 32: return false var allowed := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-" for character in slot_name: if not allowed.contains(character): return false return true func _remove_if_present(path: String) -> void: if FileAccess.file_exists(path): DirAccess.remove_absolute(path) func _fail(message: String) -> bool: last_error = message return false