using System.Collections.Generic; using Ichni.Menu; using Ichni.Story; using UnityEngine; namespace Ichni { /// /// 剧情存档模块。 /// 故事树状态与对话选项 按章节 保存(单文件、单 ES3 key)。 /// 剧情变量 全局 保存,可跨章节引用,契合 Yarn 变量模型。 /// public class StorySaveModule { private const string ChapterStoryKey = "ChapterStory"; private const string VariablesKey = "StoryVariables"; // 每个剧情存档文件都保存自己的 Schema Version;不要把它与歌曲成绩存档的 Version 混用。 // 当前项目尚未正式发布,剧情存档统一从 v1 开始;非 v1 的测试存档会被直接重置。 // 正式发布后升级 Schema 时,必须用非破坏性迁移替代当前的重置策略。 private const string StorySaveSchemaVersionKey = "StorySaveSchemaVersion"; private const int CurrentStorySaveSchemaVersion = 1; // 已加载章节的内存缓存(chapterIndex -> 章节存档) private readonly Dictionary _chapterSaves = new Dictionary(); // 全局剧情变量(float / string / bool 三类型,匹配 Yarn 契约) public StoryVariablesSave variables = new StoryVariablesSave(); private static string GetChapterSavePath(string chapterIndex) => Application.persistentDataPath + "/StorySaves/" + chapterIndex + ".json"; private static string VariablesPath => Application.persistentDataPath + "/StorySaves/StoryVariables.json"; /// /// 补齐章节存档的可空集合,并以文件名对应的 chapterIndex 作为唯一归属。 /// 当前用于保证新建 v1 存档的运行时集合非空;未来迁移逻辑应单独实现,不应依赖此方法隐式转换。 /// private static ChapterStorySave NormalizeChapterSave(ChapterStorySave save, string chapterIndex) { save ??= new ChapterStorySave(); save.chapterIndex = chapterIndex; save.completedBlockIds ??= new List(); save.selectedChoices ??= new Dictionary(); return save; } /// /// 补齐全局剧情变量字典,保证新建 v1 存档可被 Yarn 变量访问。 /// private static StoryVariablesSave NormalizeVariables(StoryVariablesSave storyVariables) { storyVariables ??= new StoryVariablesSave(); storyVariables.floatVariables ??= new Dictionary(); storyVariables.stringVariables ??= new Dictionary(); storyVariables.boolVariables ??= new Dictionary(); return storyVariables; } // ── 故事树 + 选项(按章节)──────────────────────────────────────────────── /// /// 判断某章节是否尚无可用的 v1 存档(首次进入或旧测试存档待重置)。 /// public bool IsNewChapter(string chapterIndex) { string path = GetChapterSavePath(chapterIndex); return !ES3.FileExists(path) || !ES3.KeyExists(ChapterStoryKey, path) || !ES3.KeyExists(StorySaveSchemaVersionKey, path) || ES3.Load(StorySaveSchemaVersionKey, path) != CurrentStorySaveSchemaVersion; } /// /// 加载指定章节的 v1 存档容器;无存档或非 v1 的预发布测试存档会直接重置为空容器。 /// 加载结果会写入内存缓存。 /// public ChapterStorySave LoadChapter(string chapterIndex) { string path = GetChapterSavePath(chapterIndex); bool hasExistingFile = ES3.FileExists(path); int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, path) ? ES3.Load(StorySaveSchemaVersionKey, path) : 0; if (hasExistingFile && loadedSchemaVersion != CurrentStorySaveSchemaVersion) { Debug.LogWarning($"Resetting pre-release chapter story save '{chapterIndex}' from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}."); ES3.DeleteFile(path); hasExistingFile = false; } ChapterStorySave save = hasExistingFile && ES3.KeyExists(ChapterStoryKey, path) ? ES3.Load(ChapterStoryKey, path) : new ChapterStorySave(); save = NormalizeChapterSave(save, chapterIndex); _chapterSaves[chapterIndex] = save; return save; } /// /// 保存整章存档(blocks + connectors + choices),单次 ES3 写入。 /// public void SaveChapter(ChapterStorySave save) { if (save == null || string.IsNullOrEmpty(save.chapterIndex)) { Debug.LogWarning("Cannot save a chapter story record without a chapterIndex."); return; } save = NormalizeChapterSave(save, save.chapterIndex); _chapterSaves[save.chapterIndex] = save; string path = GetChapterSavePath(save.chapterIndex); ES3.Save(ChapterStoryKey, save, path); ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, path); } /// /// 获取内存中的章节存档;若未加载则从磁盘加载。 /// public ChapterStorySave GetChapter(string chapterIndex) { return _chapterSaves.TryGetValue(chapterIndex, out ChapterStorySave save) ? save : LoadChapter(chapterIndex); } // ── 对话选项(按章节)────────────────────────────────────────────────────── /// /// 记录某章节内一次对话选项的选择结果。 /// public void SetChoice(string chapterIndex, string choiceKey, int optionIndex) { GetChapter(chapterIndex).selectedChoices[choiceKey] = optionIndex; } /// /// 读取某章节内已记录的对话选项选择结果。 /// public bool TryGetChoice(string chapterIndex, string choiceKey, out int optionIndex) { return GetChapter(chapterIndex).selectedChoices.TryGetValue(choiceKey, out optionIndex); } // ── 剧情变量(全局)────────────────────────────────────────────────────── /// /// 从磁盘加载全局剧情变量(启动时调用)。 /// public void LoadVariables() { bool hasExistingFile = ES3.FileExists(VariablesPath); int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, VariablesPath) ? ES3.Load(StorySaveSchemaVersionKey, VariablesPath) : 0; if (hasExistingFile && loadedSchemaVersion != CurrentStorySaveSchemaVersion) { Debug.LogWarning($"Resetting pre-release global story variables from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}."); ES3.DeleteFile(VariablesPath); hasExistingFile = false; } variables = hasExistingFile && ES3.KeyExists(VariablesKey, VariablesPath) ? ES3.Load(VariablesKey, VariablesPath) : new StoryVariablesSave(); variables = NormalizeVariables(variables); } /// /// 将全局剧情变量写入磁盘。 /// public void SaveVariables() { variables = NormalizeVariables(variables); ES3.Save(VariablesKey, variables, VariablesPath); ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, VariablesPath); } // ── 清档 ──────────────────────────────────────────────────────────────── /// /// 清除所有章节的故事树存档与全局剧情变量。 /// public void ClearAllStoryline() { if (ChapterSelectionManager.instance != null) { foreach (ChapterSelectionUnit chapter in ChapterSelectionManager.instance.chapters) { string path = GetChapterSavePath(chapter.chapterIndex); if (ES3.FileExists(path)) ES3.DeleteFile(path); } } _chapterSaves.Clear(); if (ES3.FileExists(VariablesPath)) ES3.DeleteFile(VariablesPath); variables = new StoryVariablesSave(); Debug.Log("Cleared all story saves (chapters + global variables)."); } } }