185 lines
6.4 KiB
GDScript
185 lines
6.4 KiB
GDScript
## 存档系统(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,返回 Dictionary(key 为系统名),如 `_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"))
|