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).");
}
}
}