3761966303
Ein Speicherstand ersetzt den vorigen nur noch, wenn er vollständig geschrieben und zurückgelesen wurde. Ein nicht gespeicherter Spielerschlüssel wird gemeldet, und set_aside_save meldet, wenn nichts verschoben wurde. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
99 lines
3.2 KiB
GDScript
99 lines
3.2 KiB
GDScript
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
|