class_name SaveStore extends RefCounted ## The game's files on the player's machine: the player key, the save and ## the session recordings for bug reports. const KEY_FILE := "player.key" const SAVE_FILE := "world.grw" const RECORDINGS_DIR := "recordings" ## Recordings beyond this number are deleted, oldest first. const KEEP_RECORDINGS := 10 const KEY_BYTES := 16 var base_dir: String func _init(directory: String = "user://") -> void: base_dir = directory DirAccess.make_dir_recursive_absolute(base_dir) ## The key that identifies this installation in every world. It is created ## on first use and kept from then on. func player_key() -> PackedByteArray: var path := base_dir.path_join(KEY_FILE) var key := FileAccess.get_file_as_bytes(path) if key.size() != KEY_BYTES: key = Crypto.new().generate_random_bytes(KEY_BYTES) if not _write(path, key): push_error("player key could not be stored at %s" % path) return key func has_save() -> bool: return FileAccess.file_exists(base_dir.path_join(SAVE_FILE)) func read_save() -> PackedByteArray: return FileAccess.get_file_as_bytes(base_dir.path_join(SAVE_FILE)) ## Writes the save through a temporary file, so that a crash or a failed ## write never destroys the previous save. Returns false if the new save was ## not stored completely; the previous save then stays as it was. func write_save(bytes: PackedByteArray) -> bool: if bytes.is_empty(): return false var path := base_dir.path_join(SAVE_FILE) var temporary := path + ".tmp" if not _write(temporary, bytes) or FileAccess.get_file_as_bytes(temporary) != bytes: DirAccess.remove_absolute(temporary) return false return DirAccess.rename_absolute(temporary, path) == OK ## Moves an unusable save out of the way, so that the next save does not ## overwrite it. An older set-aside save is replaced. Returns the new path, ## or an empty text if there was no save or it could not be moved. func set_aside_save() -> String: var path := base_dir.path_join(SAVE_FILE) var aside := path + ".broken" if DirAccess.rename_absolute(path, aside) != OK: return "" return aside ## Stores a session recording and deletes the oldest ones beyond ## KEEP_RECORDINGS. Returns the path, or an empty text if nothing was written. func write_recording(bytes: PackedByteArray) -> String: if bytes.is_empty(): return "" var directory := base_dir.path_join(RECORDINGS_DIR) DirAccess.make_dir_recursive_absolute(directory) # Microseconds since 1970, padded so that names sort by age. var stamp := int(Time.get_unix_time_from_system() * 1_000_000.0) var path := directory.path_join("session-%016d.grwr" % stamp) if not _write(path, bytes): return "" var names := recordings() while names.size() > KEEP_RECORDINGS: DirAccess.remove_absolute(directory.path_join(names[0])) names.remove_at(0) return path ## File names of the stored recordings, oldest first. func recordings() -> PackedStringArray: var names := DirAccess.get_files_at(base_dir.path_join(RECORDINGS_DIR)) names.sort() return names func _write(path: String, bytes: PackedByteArray) -> bool: var file := FileAccess.open(path, FileAccess.WRITE) if file == null: return false var stored := file.store_buffer(bytes) file.close() return stored