Spielstand bleibt erhalten, wenn das letzte Umbenennen scheitert
SaveStore.write_save verschob die geprüfte Temporärdatei direkt auf world.grw. Godot löscht unter Windows ein vorhandenes Ziel, bevor es die Datei verschiebt. Scheiterte die Verschiebung, zum Beispiel weil ein Virenscanner world.grw.tmp festhielt, war der vorige Spielstand schon weg. Jetzt wird der vorige Stand zuerst nach world.grw.bak verschoben, dann rückt die Temporärdatei nach. Scheitert das, kommt der Stand aus der Sicherung zurück; nach Erfolg wird die Sicherung gelöscht. Bleibt nach einem Absturz zwischen den beiden Verschiebungen nur world.grw.bak übrig, holt SaveStore sie beim Öffnen des Ordners zurück. Neue Tests: ein offenes Handle auf world.grw.tmp, eine blockierte Sicherung, die Wiederherstellung aus der Sicherung und dass eine Sicherung einen vorhandenen Stand nie ersetzt. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,8 @@ extends RefCounted
|
|||||||
|
|
||||||
const KEY_FILE := "player.key"
|
const KEY_FILE := "player.key"
|
||||||
const SAVE_FILE := "world.grw"
|
const SAVE_FILE := "world.grw"
|
||||||
|
## Holds the previous save while write_save replaces it.
|
||||||
|
const BACKUP_FILE := SAVE_FILE + ".bak"
|
||||||
const RECORDINGS_DIR := "recordings"
|
const RECORDINGS_DIR := "recordings"
|
||||||
## Recordings beyond this number are deleted, oldest first.
|
## Recordings beyond this number are deleted, oldest first.
|
||||||
const KEEP_RECORDINGS := 10
|
const KEEP_RECORDINGS := 10
|
||||||
@@ -13,9 +15,13 @@ const KEY_BYTES := 16
|
|||||||
var base_dir: String
|
var base_dir: String
|
||||||
|
|
||||||
|
|
||||||
|
## Opens the folder and creates it if it is missing. If a crash interrupted a
|
||||||
|
## save and left only the backup of the previous one (see write_save), the
|
||||||
|
## backup becomes the save again.
|
||||||
func _init(directory: String = "user://") -> void:
|
func _init(directory: String = "user://") -> void:
|
||||||
base_dir = directory
|
base_dir = directory
|
||||||
DirAccess.make_dir_recursive_absolute(base_dir)
|
DirAccess.make_dir_recursive_absolute(base_dir)
|
||||||
|
_restore_backup()
|
||||||
|
|
||||||
|
|
||||||
## The key that identifies this installation in every world. It is created
|
## The key that identifies this installation in every world. It is created
|
||||||
@@ -41,15 +47,35 @@ func read_save() -> PackedByteArray:
|
|||||||
## Writes the save through a temporary file, so that a crash or a failed
|
## 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
|
## write never destroys the previous save. Returns false if the new save was
|
||||||
## not stored completely; the previous save then stays as it was.
|
## not stored completely; the previous save then stays as it was.
|
||||||
|
##
|
||||||
|
## The temporary file replaces the previous save in two moves: the previous
|
||||||
|
## save becomes the backup, then the temporary file becomes the save. A single
|
||||||
|
## move onto the previous save would not be safe, because on Windows Godot
|
||||||
|
## deletes the target first and only then moves the file. If that move failed,
|
||||||
|
## for example because a virus scanner holds the temporary file, the previous
|
||||||
|
## save would already be gone. If the second move fails, the backup is moved
|
||||||
|
## back. A crash between the two moves leaves only the backup; the next
|
||||||
|
## SaveStore on the folder restores it.
|
||||||
func write_save(bytes: PackedByteArray) -> bool:
|
func write_save(bytes: PackedByteArray) -> bool:
|
||||||
if bytes.is_empty():
|
if bytes.is_empty():
|
||||||
return false
|
return false
|
||||||
var path := base_dir.path_join(SAVE_FILE)
|
var path := base_dir.path_join(SAVE_FILE)
|
||||||
var temporary := path + ".tmp"
|
var temporary := path + ".tmp"
|
||||||
|
var backup := base_dir.path_join(BACKUP_FILE)
|
||||||
if not _write(temporary, bytes) or FileAccess.get_file_as_bytes(temporary) != bytes:
|
if not _write(temporary, bytes) or FileAccess.get_file_as_bytes(temporary) != bytes:
|
||||||
DirAccess.remove_absolute(temporary)
|
DirAccess.remove_absolute(temporary)
|
||||||
return false
|
return false
|
||||||
return DirAccess.rename_absolute(temporary, path) == OK
|
var has_previous := FileAccess.file_exists(path)
|
||||||
|
if has_previous and DirAccess.rename_absolute(path, backup) != OK:
|
||||||
|
DirAccess.remove_absolute(temporary)
|
||||||
|
return false
|
||||||
|
if DirAccess.rename_absolute(temporary, path) != OK:
|
||||||
|
if has_previous:
|
||||||
|
DirAccess.rename_absolute(backup, path)
|
||||||
|
return false
|
||||||
|
if has_previous:
|
||||||
|
DirAccess.remove_absolute(backup)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
## Moves an unusable save out of the way, so that the next save does not
|
## Moves an unusable save out of the way, so that the next save does not
|
||||||
@@ -89,6 +115,16 @@ func recordings() -> PackedStringArray:
|
|||||||
return names
|
return names
|
||||||
|
|
||||||
|
|
||||||
|
## Moves the backup back to the save if there is no save, see write_save.
|
||||||
|
func _restore_backup() -> void:
|
||||||
|
var path := base_dir.path_join(SAVE_FILE)
|
||||||
|
var backup := base_dir.path_join(BACKUP_FILE)
|
||||||
|
if FileAccess.file_exists(path) or not FileAccess.file_exists(backup):
|
||||||
|
return
|
||||||
|
if DirAccess.rename_absolute(backup, path) != OK:
|
||||||
|
push_warning("the previous save could not be restored from %s" % backup)
|
||||||
|
|
||||||
|
|
||||||
func _write(path: String, bytes: PackedByteArray) -> bool:
|
func _write(path: String, bytes: PackedByteArray) -> bool:
|
||||||
var file := FileAccess.open(path, FileAccess.WRITE)
|
var file := FileAccess.open(path, FileAccess.WRITE)
|
||||||
if file == null:
|
if file == null:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ func test_a_save_is_written_and_read_back() -> void:
|
|||||||
check(store.has_save(), "a save exists")
|
check(store.has_save(), "a save exists")
|
||||||
check_eq(store.read_save(), PackedByteArray([4, 5]), "the newest save is read")
|
check_eq(store.read_save(), PackedByteArray([4, 5]), "the newest save is read")
|
||||||
check(not FileAccess.file_exists(directory.path_join(SaveStore.SAVE_FILE + ".tmp")), "no temporary file is left")
|
check(not FileAccess.file_exists(directory.path_join(SaveStore.SAVE_FILE + ".tmp")), "no temporary file is left")
|
||||||
|
check(not FileAccess.file_exists(directory.path_join(SaveStore.BACKUP_FILE)), "no backup is left")
|
||||||
TestFiles.remove_dir(directory)
|
TestFiles.remove_dir(directory)
|
||||||
|
|
||||||
|
|
||||||
@@ -43,6 +44,58 @@ func test_a_save_that_cannot_be_written_keeps_the_previous_one() -> void:
|
|||||||
TestFiles.remove_dir(directory)
|
TestFiles.remove_dir(directory)
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_save_that_cannot_be_moved_into_place_keeps_the_previous_one() -> void:
|
||||||
|
var directory := TestFiles.temporary_dir("held")
|
||||||
|
var store := SaveStore.new(directory)
|
||||||
|
store.write_save(PackedByteArray([1, 2, 3]))
|
||||||
|
# The open handle stands for an outside program, for example a virus
|
||||||
|
# scanner. The new save can be written, but Windows refuses to move a file
|
||||||
|
# that is held open.
|
||||||
|
var holder := FileAccess.open(directory.path_join(SaveStore.SAVE_FILE + ".tmp"), FileAccess.WRITE)
|
||||||
|
check(not store.write_save(PackedByteArray([4, 5])), "the failed save is reported")
|
||||||
|
check_eq(store.read_save(), PackedByteArray([1, 2, 3]), "the previous save is untouched")
|
||||||
|
check(not FileAccess.file_exists(directory.path_join(SaveStore.BACKUP_FILE)), "no backup is left")
|
||||||
|
holder.close()
|
||||||
|
TestFiles.remove_dir(directory)
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_save_that_cannot_move_the_previous_one_aside_keeps_it() -> void:
|
||||||
|
var directory := TestFiles.temporary_dir("aside-blocked")
|
||||||
|
var store := SaveStore.new(directory)
|
||||||
|
store.write_save(PackedByteArray([1, 2, 3]))
|
||||||
|
# A folder under the backup name keeps the previous save from moving.
|
||||||
|
DirAccess.make_dir_absolute(directory.path_join(SaveStore.BACKUP_FILE))
|
||||||
|
check(not store.write_save(PackedByteArray([4, 5])), "the failed save is reported")
|
||||||
|
check_eq(store.read_save(), PackedByteArray([1, 2, 3]), "the previous save is untouched")
|
||||||
|
check(not FileAccess.file_exists(directory.path_join(SaveStore.SAVE_FILE + ".tmp")), "no temporary file is left")
|
||||||
|
TestFiles.remove_dir(directory)
|
||||||
|
|
||||||
|
|
||||||
|
func test_an_interrupted_save_is_recovered_from_the_backup() -> void:
|
||||||
|
var directory := TestFiles.temporary_dir("recover")
|
||||||
|
# A crash between the two moves of write_save leaves only the backup.
|
||||||
|
var backup := directory.path_join(SaveStore.BACKUP_FILE)
|
||||||
|
var file := FileAccess.open(backup, FileAccess.WRITE)
|
||||||
|
file.store_buffer(PackedByteArray([7, 8, 9]))
|
||||||
|
file.close()
|
||||||
|
var store := SaveStore.new(directory)
|
||||||
|
check(store.has_save(), "the previous save is back")
|
||||||
|
check_eq(store.read_save(), PackedByteArray([7, 8, 9]), "with its content")
|
||||||
|
check(not FileAccess.file_exists(backup), "the backup is moved, not copied")
|
||||||
|
TestFiles.remove_dir(directory)
|
||||||
|
|
||||||
|
|
||||||
|
func test_a_backup_never_replaces_an_existing_save() -> void:
|
||||||
|
var directory := TestFiles.temporary_dir("keep")
|
||||||
|
SaveStore.new(directory).write_save(PackedByteArray([1, 2, 3]))
|
||||||
|
var file := FileAccess.open(directory.path_join(SaveStore.BACKUP_FILE), FileAccess.WRITE)
|
||||||
|
file.store_buffer(PackedByteArray([9, 9]))
|
||||||
|
file.close()
|
||||||
|
var store := SaveStore.new(directory)
|
||||||
|
check_eq(store.read_save(), PackedByteArray([1, 2, 3]), "the save is not replaced by the older backup")
|
||||||
|
TestFiles.remove_dir(directory)
|
||||||
|
|
||||||
|
|
||||||
func test_setting_aside_without_a_save_reports_nothing_moved() -> void:
|
func test_setting_aside_without_a_save_reports_nothing_moved() -> void:
|
||||||
var directory := TestFiles.temporary_dir("nothing")
|
var directory := TestFiles.temporary_dir("nothing")
|
||||||
check_eq(SaveStore.new(directory).set_aside_save(), "", "no save, nothing moved")
|
check_eq(SaveStore.new(directory).set_aside_save(), "", "no save, nothing moved")
|
||||||
|
|||||||
Reference in New Issue
Block a user