Files
ImmortalSect/src/Core/SaveSystem.gd
T

185 lines
6.4 KiB
GDScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 存档系统(Autoload 单例)。
## 两级存档:存档组 SaveGroup(一个角色/一次轮回)→ 存档槽 SaveSlot(该组下任意多个进度快照)。
## 每个新游戏创建一个组;每次存档在该组下新建一个槽;组与槽数量无上限。
## 文件:user://saves/profile_{组id}/slot_{时间戳}.json
## 各系统通过 register_saver() 注册数据提供者,save() 自动收集,无需改动存档核心。
extends Node
## 存档根目录。
const SAVE_ROOT := "user://saves"
## 存档组列表文件。
const PROFILES_PATH := "user://saves/profiles.json"
## 存档格式版本(兼容迁移预留)。
const VERSION := 1
var _savers: Array[Callable] = []
## 注册存档数据提供者(各系统在 _ready 中调用一次)。
## @param c: 无参 Callable,返回 Dictionarykey 为系统名),如 `_to_save_dict`
func register_saver(c: Callable) -> void:
if not _savers.has(c):
_savers.append(c)
## 遍历收集所有注册系统的数据,合并为一个大 Dictionary。
func collect_data() -> Dictionary:
var data := {}
for c in _savers:
var result: Variant = c.call()
if result is Dictionary:
data.merge(result)
return data
## 新建存档组(主菜单"新游戏"调用),返回组 id(名"存档N")。
func create_profile() -> int:
var profiles := _load_profiles()
var id := 1
for p in profiles:
id = maxi(id, int(p["id"]) + 1)
profiles.append({
"id": id,
"name": "存档%d" % id,
"created": Time.get_date_string_from_system(),
"last_date": "",
})
_save_profiles(profiles)
return id
func get_profiles() -> Array[Dictionary]:
return _load_profiles()
## 保存游戏:在该组下新建一个槽位(文件名 slot_{时间戳}.json),返回槽文件名。
## 不传 data 时自动调用 collect_data() 收集所有注册系统的数据。
## @param profile_id: 存档组 ID
## @param data: 要保存的游戏数据(可选,默认自动收集)
func save(profile_id: int, data: Dictionary = {}) -> String:
if data.is_empty():
data = collect_data()
var slot_name := "slot_%d_%03d" % [Time.get_unix_time_from_system(), Time.get_ticks_msec() % 1000]
_write_slot(profile_id, slot_name, {
"version": VERSION,
"date": Time.get_datetime_string_from_system(),
"data": data,
})
_update_profile_last_date(profile_id)
return slot_name
## 读取指定槽位的游戏数据,槽不存在或损坏返回空 Dictionary。
## 返回的 data 字典由调用方(MainGame)按 key 分发给各系统恢复状态。
## @param profile_id: 存档组 ID
## @param slot_name: 槽文件名(不含 .json,如 "slot_1754734800"
func load(profile_id: int, slot_name: String) -> Dictionary:
var path := "user://saves/profile_%d/%s.json" % [profile_id, slot_name]
if not FileAccess.file_exists(path):
return {}
var f := FileAccess.open(path, FileAccess.READ)
if f == null:
return {}
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary or int(parsed.get("version", 0)) > VERSION:
return {}
return parsed.get("data", {})
## 列出该组下所有槽位信息(文件名 + 存档日期),按时间倒序(最新在前)。
## @param profile_id: 存档组 ID
func list_slots(profile_id: int) -> Array[Dictionary]:
var dir_path := "user://saves/profile_%d" % profile_id
var da := DirAccess.open(dir_path)
if da == null:
return []
var result: Array[Dictionary] = []
da.list_dir_begin()
var fname := da.get_next()
while fname != "":
if fname.ends_with(".json") and not da.current_is_dir():
var path := "%s/%s" % [dir_path, fname]
var f := FileAccess.open(path, FileAccess.READ)
if f != null:
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is Dictionary:
result.append({
"name": fname.trim_suffix(".json"),
"date": str(parsed.get("date", "")),
})
fname = da.get_next()
da.list_dir_end()
result.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a["name"] > b["name"])
return result
## 删除指定槽位。
func delete_slot(profile_id: int, slot_name: String) -> void:
DirAccess.remove_absolute("user://saves/profile_%d/%s.json" % [profile_id, slot_name])
## 删除整个存档组(含其下所有槽位文件)。
## @param profile_id: 存档组 ID
func delete_profile(profile_id: int) -> void:
var profiles := _load_profiles()
profiles = profiles.filter(func(p: Dictionary) -> bool: return int(p["id"]) != profile_id)
_save_profiles(profiles)
_delete_dir_recursive("user://saves/profile_%d" % profile_id)
## 递归删除目录及其内容。
func _delete_dir_recursive(path: String) -> void:
var da := DirAccess.open(path)
if da == null:
return
da.list_dir_begin()
var fname := da.get_next()
while fname != "":
if fname in [".", ".."]:
fname = da.get_next()
continue
if da.current_is_dir():
_delete_dir_recursive("%s/%s" % [path, fname])
else:
DirAccess.remove_absolute("%s/%s" % [path, fname])
fname = da.get_next()
da.list_dir_end()
da.remove(path)
## 写入槽位文件。
func _write_slot(profile_id: int, slot_name: String, payload: Dictionary) -> void:
var dir_path := "user://saves/profile_%d" % profile_id
var da := DirAccess.open("user://")
if da != null:
da.make_dir_recursive(dir_path)
var f := FileAccess.open("%s/%s.json" % [dir_path, slot_name], FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify(payload, ""))
## 保存后更新该组的最近存档时间。
func _update_profile_last_date(profile_id: int) -> void:
var profiles := _load_profiles()
for p in profiles:
if int(p["id"]) == profile_id:
p["last_date"] = Time.get_datetime_string_from_system()
break
_save_profiles(profiles)
## 读取存档组列表。
func _load_profiles() -> Array[Dictionary]:
if not FileAccess.file_exists(PROFILES_PATH):
return []
var f := FileAccess.open(PROFILES_PATH, FileAccess.READ)
if f == null:
return []
var parsed: Variant = JSON.parse_string(f.get_as_text())
if parsed is not Dictionary or not parsed.has("profiles"):
return []
var result: Array[Dictionary] = []
for p in parsed["profiles"]:
result.append(p)
return result
## 写入存档组列表。
## @param profiles: 存档组列表
func _save_profiles(profiles: Array[Dictionary]) -> void:
var da := DirAccess.open("user://")
if da != null:
da.make_dir_recursive("saves")
var f := FileAccess.open(PROFILES_PATH, FileAccess.WRITE)
if f == null:
return
f.store_string(JSON.stringify({"profiles": profiles}, "\t"))