Files
ichni_Official/Assets/Scripts/NewStorySystem/Save/StorySave.cs
SoulliesOfficial 810d019619 剧情+对话完善
2026-07-21 15:24:42 -04:00

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