using System; using System.Collections.Generic; namespace Ichni.Story { /// /// 单章节的存档容器。 /// 故事树的布局与连接完全由 静态定义,故存档不再保存 /// block 位置与连接线;进入章节时按"已完成集合 + 解锁条件"实时推导每个 block 的 /// (Completed / Current / Locked / Forbidden)。 /// 以单个 ES3 key 存储,按章节分文件,一次读写即可加载/保存整章进度。 /// [Serializable] public class ChapterStorySave { public string chapterIndex; // 已完成 Block 的 ID 列表(顺序无关,去重由 StoryTreeController 保证)。 public List completedBlockIds = new(); /// /// 本章节已经确认过的 Yarn 选项。 /// Key 由 根据来源 Block 与可选项文本 ID 生成, /// Value 保存稳定的文本 ID,而非 UI 下标;因此本地化、隐藏不可用选项或调整显示顺序时不会误选。 /// public Dictionary selectedChoices = new(); /// /// 只属于本章节的永久剧情变量。章节之间完全隔离,重开本章节时也只回滚这里的数据。 /// public StoryVariablesSave storyVariables = new(); /// /// 已经到达过的 Timeline 回滚点快照(markerId -> 快照)。 /// 快照创建在 Marker 所在列的第一个 Block 写入前,故恢复后该列及其后的内容都会被重置。 /// public Dictionary markerRollbackSnapshots = new(); } /// /// 一次 Yarn 选项的持久化结果。 /// 不保存运行时 Option 下标,避免因翻译、选项可用性或显示顺序变化而恢复到错误选项。 /// [Serializable] public class StoryChoiceRecord { /// 触发这组选项的 TextBlock ID,用于诊断和后续内容审查。 public string sourceBlockId; /// 由来源 Block 与全部 Option Text ID 生成的稳定选项组 Key。 public string choiceKey; /// 玩家第一次选择的 Yarn DialogueOption.TextID。 public string selectedOptionTextId; } /// /// 一个 TimelineMarker 的章节内回滚快照。 /// 它刻意不包含歌曲成绩、解锁 Key、Settings 或其它章节的数据,保证“重开剧情”只影响当前章节故事。 /// [Serializable] public class StoryRollbackSnapshot { public string markerId; public float markerColumn; public List completedBlockIds = new(); public Dictionary selectedChoices = new(); public StoryVariablesSave storyVariables = new(); } /// /// 章节剧情变量存档。float / string / bool 三种底层类型由 /// 统一读写;Int 以 float 保存,并在读取时恢复为整数语义。 /// [Serializable] public class StoryVariablesSave { public Dictionary floatVariables = new(); public Dictionary stringVariables = new(); public Dictionary boolVariables = new(); } /// /// 剧情存档对象的深拷贝工具。 /// ES3 保存前和 Timeline 回滚前都必须使用副本,避免后续字典写入意外修改已记录的快照。 /// public static class StorySaveCloneUtility { /// /// 深拷贝完整章节剧情存档。 /// 对话中途退出与 Timeline 回滚都需要保留“修改前”的独立副本; /// 不能直接引用运行中的 ,否则后续变量或选项写入会污染快照。 /// public static ChapterStorySave CloneChapter(ChapterStorySave source) { source ??= new ChapterStorySave(); return new ChapterStorySave { chapterIndex = source.chapterIndex, completedBlockIds = source.completedBlockIds != null ? new List(source.completedBlockIds) : new List(), selectedChoices = CloneChoices(source.selectedChoices), storyVariables = CloneVariables(source.storyVariables), markerRollbackSnapshots = CloneRollbackSnapshots(source.markerRollbackSnapshots) }; } public static StoryVariablesSave CloneVariables(StoryVariablesSave source) { source ??= new StoryVariablesSave(); return new StoryVariablesSave { floatVariables = source.floatVariables != null ? new Dictionary(source.floatVariables) : new Dictionary(), stringVariables = source.stringVariables != null ? new Dictionary(source.stringVariables) : new Dictionary(), boolVariables = source.boolVariables != null ? new Dictionary(source.boolVariables) : new Dictionary() }; } public static Dictionary CloneChoices( Dictionary source) { Dictionary copy = new(); if (source == null) return copy; foreach ((string key, StoryChoiceRecord value) in source) { if (value == null) continue; copy[key] = new StoryChoiceRecord { sourceBlockId = value.sourceBlockId, choiceKey = value.choiceKey, selectedOptionTextId = value.selectedOptionTextId }; } return copy; } /// /// 深拷贝 Timeline Marker 快照及其内部的完成列表、选项和变量。 /// public static Dictionary CloneRollbackSnapshots( Dictionary source) { Dictionary copy = new(); if (source == null) return copy; foreach ((string key, StoryRollbackSnapshot value) in source) { if (value == null) continue; copy[key] = new StoryRollbackSnapshot { markerId = value.markerId, markerColumn = value.markerColumn, completedBlockIds = value.completedBlockIds != null ? new List(value.completedBlockIds) : new List(), selectedChoices = CloneChoices(value.selectedChoices), storyVariables = CloneVariables(value.storyVariables) }; } return copy; } } }