## 存档系统(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"))