111 lines
3.6 KiB
GDScript
111 lines
3.6 KiB
GDScript
class_name SaveManager
|
|
extends RefCounted
|
|
## Атомарное сохранение в user://save.json (без секретов).
|
|
##
|
|
## Схема: пишем во временный файл, затем rename; после записи читаем обратно
|
|
## и проверяем версию. На web вызываем force_fs_sync(), чтобы IndexedDB
|
|
## успел синхронизироваться.
|
|
|
|
const SAVE_PATH := "user://save.json"
|
|
const TMP_PATH := "user://save.tmp"
|
|
const VERSION := 1
|
|
|
|
|
|
static func save(data: Dictionary) -> bool:
|
|
var payload := data.duplicate(true)
|
|
payload["version"] = VERSION
|
|
payload["timestamp"] = Time.get_unix_time_from_system()
|
|
var text := JSON.stringify(payload, "\t")
|
|
|
|
var file := FileAccess.open(TMP_PATH, FileAccess.WRITE)
|
|
if file == null:
|
|
push_warning("SaveManager: не удалось открыть %s (код %s)" % [TMP_PATH, FileAccess.get_open_error()])
|
|
return false
|
|
file.store_string(text)
|
|
file.flush()
|
|
file.close()
|
|
|
|
if not _replace_file(TMP_PATH, SAVE_PATH):
|
|
# Резервный путь: прямая перезапись, если rename не поддерживается.
|
|
var direct := FileAccess.open(SAVE_PATH, FileAccess.WRITE)
|
|
if direct == null:
|
|
push_warning("SaveManager: не удалось записать %s" % SAVE_PATH)
|
|
_remove_if_exists(TMP_PATH)
|
|
return false
|
|
direct.store_string(text)
|
|
direct.flush()
|
|
direct.close()
|
|
_remove_if_exists(TMP_PATH)
|
|
|
|
_web_fs_sync()
|
|
|
|
var check := _read_save()
|
|
if check.is_empty() or int(check.get("version", 0)) != VERSION:
|
|
push_warning("SaveManager: проверка после записи не прошла")
|
|
return false
|
|
return true
|
|
|
|
|
|
static func load() -> Dictionary:
|
|
return _read_save()
|
|
|
|
|
|
static func _read_save() -> Dictionary:
|
|
if not FileAccess.file_exists(SAVE_PATH):
|
|
return {}
|
|
var file := FileAccess.open(SAVE_PATH, FileAccess.READ)
|
|
if file == null:
|
|
push_warning("SaveManager: не удалось открыть %s (код %s)" % [SAVE_PATH, FileAccess.get_open_error()])
|
|
return {}
|
|
var text := file.get_as_text()
|
|
file.close()
|
|
var parser := JSON.new()
|
|
var err := parser.parse(text)
|
|
if err != OK:
|
|
push_warning("SaveManager: %s повреждён (строка %d: %s)" % [SAVE_PATH, parser.get_error_line(), parser.get_error_message()])
|
|
return {}
|
|
var parsed = parser.data
|
|
if not (parsed is Dictionary):
|
|
push_warning("SaveManager: %s повреждён (не JSON-объект)" % SAVE_PATH)
|
|
return {}
|
|
var data: Dictionary = parsed
|
|
var version := int(data.get("version", -1))
|
|
if version != VERSION:
|
|
push_warning("SaveManager: версия save %d не поддерживается (ожидается %d)" % [version, VERSION])
|
|
return {}
|
|
return data
|
|
|
|
|
|
static func has_save() -> bool:
|
|
return FileAccess.file_exists(SAVE_PATH)
|
|
|
|
|
|
static func delete_save() -> bool:
|
|
return _remove_if_exists(SAVE_PATH)
|
|
|
|
|
|
# --- служебное -------------------------------------------------------------
|
|
|
|
static func _replace_file(from_path: String, to_path: String) -> bool:
|
|
var err := DirAccess.rename_absolute(from_path, to_path)
|
|
if err != OK:
|
|
_remove_if_exists(to_path)
|
|
err = DirAccess.rename_absolute(from_path, to_path)
|
|
return err == OK
|
|
|
|
|
|
static func _remove_if_exists(path: String) -> bool:
|
|
if not FileAccess.file_exists(path):
|
|
return false
|
|
return DirAccess.remove_absolute(path) == OK
|
|
|
|
|
|
static func _web_fs_sync() -> void:
|
|
if not OS.has_feature("web"):
|
|
return
|
|
if not Engine.has_singleton("JavaScriptBridge"):
|
|
return
|
|
var bridge = Engine.get_singleton("JavaScriptBridge")
|
|
if bridge != null and bridge.has_method("force_fs_sync"):
|
|
bridge.force_fs_sync()
|