Files
ichni_Official/Assets/Scripts/NewStorySystem/Save/StorySaveModule.cs

211 lines
9.4 KiB
C#
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.
using System.Collections.Generic;
using Ichni.Menu;
using Ichni.Story;
using UnityEngine;
namespace Ichni
{
/// <summary>
/// 剧情存档模块。
/// <para>故事树状态与对话选项 <b>按章节</b> 保存(单文件、单 ES3 key。</para>
/// <para>剧情变量 <b>全局</b> 保存,可跨章节引用,契合 Yarn 变量模型。</para>
/// </summary>
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<string, ChapterStorySave> _chapterSaves =
new Dictionary<string, ChapterStorySave>();
// 全局剧情变量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";
/// <summary>
/// 补齐章节存档的可空集合,并以文件名对应的 chapterIndex 作为唯一归属。
/// 当前用于保证新建 v1 存档的运行时集合非空;未来迁移逻辑应单独实现,不应依赖此方法隐式转换。
/// </summary>
private static ChapterStorySave NormalizeChapterSave(ChapterStorySave save, string chapterIndex)
{
save ??= new ChapterStorySave();
save.chapterIndex = chapterIndex;
save.completedBlockIds ??= new List<string>();
save.selectedChoices ??= new Dictionary<string, int>();
return save;
}
/// <summary>
/// 补齐全局剧情变量字典,保证新建 v1 存档可被 Yarn 变量访问。
/// </summary>
private static StoryVariablesSave NormalizeVariables(StoryVariablesSave storyVariables)
{
storyVariables ??= new StoryVariablesSave();
storyVariables.floatVariables ??= new Dictionary<string, float>();
storyVariables.stringVariables ??= new Dictionary<string, string>();
storyVariables.boolVariables ??= new Dictionary<string, bool>();
return storyVariables;
}
// ── 故事树 + 选项(按章节)────────────────────────────────────────────────
/// <summary>
/// 判断某章节是否尚无可用的 v1 存档(首次进入或旧测试存档待重置)。
/// </summary>
public bool IsNewChapter(string chapterIndex)
{
string path = GetChapterSavePath(chapterIndex);
return !ES3.FileExists(path) || !ES3.KeyExists(ChapterStoryKey, path) ||
!ES3.KeyExists(StorySaveSchemaVersionKey, path) ||
ES3.Load<int>(StorySaveSchemaVersionKey, path) != CurrentStorySaveSchemaVersion;
}
/// <summary>
/// 加载指定章节的 v1 存档容器;无存档或非 v1 的预发布测试存档会直接重置为空容器。
/// 加载结果会写入内存缓存。
/// </summary>
public ChapterStorySave LoadChapter(string chapterIndex)
{
string path = GetChapterSavePath(chapterIndex);
bool hasExistingFile = ES3.FileExists(path);
int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, path)
? ES3.Load<int>(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<ChapterStorySave>(ChapterStoryKey, path)
: new ChapterStorySave();
save = NormalizeChapterSave(save, chapterIndex);
_chapterSaves[chapterIndex] = save;
return save;
}
/// <summary>
/// 保存整章存档blocks + connectors + choices单次 ES3 写入。
/// </summary>
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);
}
/// <summary>
/// 获取内存中的章节存档;若未加载则从磁盘加载。
/// </summary>
public ChapterStorySave GetChapter(string chapterIndex)
{
return _chapterSaves.TryGetValue(chapterIndex, out ChapterStorySave save)
? save
: LoadChapter(chapterIndex);
}
// ── 对话选项(按章节)──────────────────────────────────────────────────────
/// <summary>
/// 记录某章节内一次对话选项的选择结果。
/// </summary>
public void SetChoice(string chapterIndex, string choiceKey, int optionIndex)
{
GetChapter(chapterIndex).selectedChoices[choiceKey] = optionIndex;
}
/// <summary>
/// 读取某章节内已记录的对话选项选择结果。
/// </summary>
public bool TryGetChoice(string chapterIndex, string choiceKey, out int optionIndex)
{
return GetChapter(chapterIndex).selectedChoices.TryGetValue(choiceKey, out optionIndex);
}
// ── 剧情变量(全局)──────────────────────────────────────────────────────
/// <summary>
/// 从磁盘加载全局剧情变量(启动时调用)。
/// </summary>
public void LoadVariables()
{
bool hasExistingFile = ES3.FileExists(VariablesPath);
int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, VariablesPath)
? ES3.Load<int>(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<StoryVariablesSave>(VariablesKey, VariablesPath)
: new StoryVariablesSave();
variables = NormalizeVariables(variables);
}
/// <summary>
/// 将全局剧情变量写入磁盘。
/// </summary>
public void SaveVariables()
{
variables = NormalizeVariables(variables);
ES3.Save(VariablesKey, variables, VariablesPath);
ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, VariablesPath);
}
// ── 清档 ────────────────────────────────────────────────────────────────
/// <summary>
/// 清除所有章节的故事树存档与全局剧情变量。
/// </summary>
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).");
}
}
}