剧情+对话完善
This commit is contained in:
@@ -15,17 +15,62 @@ namespace Ichni.Story
|
||||
{
|
||||
public string chapterIndex;
|
||||
|
||||
// 已完成 block 的 id 列表(顺序无关,去重由 StoryTreeController 保证)
|
||||
// 已完成 Block 的 ID 列表(顺序无关,去重由 StoryTreeController 保证)。
|
||||
public List<string> completedBlockIds = new();
|
||||
|
||||
// 章节内的对话选项记录(choiceKey -> 选中项索引)。选项不跨章节引用。
|
||||
public Dictionary<string, int> selectedChoices = 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>
|
||||
/// 全局剧情变量存档。float / string / bool 三类型与 Yarn Spinner 的
|
||||
/// <c>VariableStorageBehaviour.GetAllVariables/SetAllVariables</c> 契约一致,
|
||||
/// 供阶段 2 的 StoryVariableStorage 直接读写。
|
||||
/// 一次 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
|
||||
@@ -34,4 +79,102 @@ namespace Ichni.Story
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Ichni.Menu;
|
||||
using Ichni.Story;
|
||||
@@ -7,50 +8,68 @@ namespace Ichni
|
||||
{
|
||||
/// <summary>
|
||||
/// 剧情存档模块。
|
||||
/// <para>故事树状态与对话选项 <b>按章节</b> 保存(单文件、单 ES3 key)。</para>
|
||||
/// <para>剧情变量 <b>全局</b> 保存,可跨章节引用,契合 Yarn 变量模型。</para>
|
||||
/// <para>一个章节对应一个 ES3 文件,文件内的 <see cref="ChapterStorySave"/> 同时保存 Block 进度、
|
||||
/// 选项结果、章节变量与 Timeline 回滚快照。</para>
|
||||
/// <para>章节之间不共享剧情变量。此设计让“重开本章节”能够精确回滚本章剧情,且绝不触及歌曲成绩、
|
||||
/// Offline 解锁 Key、设置或其它章节。</para>
|
||||
/// </summary>
|
||||
public class StorySaveModule
|
||||
{
|
||||
private const string ChapterStoryKey = "ChapterStory";
|
||||
private const string VariablesKey = "StoryVariables";
|
||||
|
||||
// 每个剧情存档文件都保存自己的 Schema Version;不要把它与歌曲成绩存档的 Version 混用。
|
||||
// 当前项目尚未正式发布,剧情存档统一从 v1 开始;非 v1 的测试存档会被直接重置。
|
||||
// 正式发布后升级 Schema 时,必须用非破坏性迁移替代当前的重置策略。
|
||||
// 每个剧情存档文件独立保存 Schema Version。项目尚未发布,当前不兼容的测试存档会重置为 v1。
|
||||
// 正式发布后若需要变更结构,必须新增显式迁移,而不能沿用删除文件的开发期策略。
|
||||
private const string StorySaveSchemaVersionKey = "StorySaveSchemaVersion";
|
||||
private const int CurrentStorySaveSchemaVersion = 1;
|
||||
|
||||
// 已加载章节的内存缓存(chapterIndex -> 章节存档)
|
||||
private readonly Dictionary<string, ChapterStorySave> _chapterSaves =
|
||||
new Dictionary<string, ChapterStorySave>();
|
||||
// 旧版将剧情变量保存为全局单文件。它不再读取,但 ClearAllStoryline 会清理它,
|
||||
// 以免开发机遗留文件造成“已清档但磁盘仍有旧剧情数据”的误解。
|
||||
private static string LegacyVariablesPath =>
|
||||
Application.persistentDataPath + "/StorySaves/StoryVariables.json";
|
||||
|
||||
// 全局剧情变量(float / string / bool 三类型,匹配 Yarn 契约)
|
||||
public StoryVariablesSave variables = new StoryVariablesSave();
|
||||
// 已加载章节的内存缓存(chapterIndex -> 章节存档)。
|
||||
private readonly Dictionary<string, ChapterStorySave> _chapterSaves = new();
|
||||
|
||||
// 当前仅允许一段 Story TextBlock 对话处于存档事务中。事务期间,章节数据仍会更新内存,
|
||||
// 但不会写入 ES3;由 StoryDialogueController 在对话正常结束时提交,或在中途退出时恢复快照。
|
||||
private string _transactionChapterIndex;
|
||||
|
||||
// 没有进入章节时(例如独立测试场景)使用的非持久化兜底变量。
|
||||
// 正式剧情流程不会写入这里;StoryTreeController 建立章节后会切换到对应 ChapterStorySave。
|
||||
private readonly StoryVariablesSave _fallbackVariables = new();
|
||||
|
||||
private static string GetChapterSavePath(string chapterIndex) =>
|
||||
Application.persistentDataPath + "/StorySaves/" + chapterIndex + ".json";
|
||||
|
||||
private static string VariablesPath =>
|
||||
Application.persistentDataPath + "/StorySaves/StoryVariables.json";
|
||||
|
||||
/// <summary>
|
||||
/// 补齐章节存档的可空集合,并以文件名对应的 chapterIndex 作为唯一归属。
|
||||
/// 当前用于保证新建 v1 存档的运行时集合非空;未来迁移逻辑应单独实现,不应依赖此方法隐式转换。
|
||||
/// 补齐运行时会用到的字典。该方法只做空值修复,不承担跨 Schema 的迁移职责。
|
||||
/// </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>();
|
||||
save.selectedChoices ??= new Dictionary<string, StoryChoiceRecord>();
|
||||
save.storyVariables = NormalizeVariables(save.storyVariables);
|
||||
save.markerRollbackSnapshots ??= new Dictionary<string, StoryRollbackSnapshot>();
|
||||
|
||||
foreach (StoryRollbackSnapshot snapshot in save.markerRollbackSnapshots.Values)
|
||||
{
|
||||
if (snapshot == null)
|
||||
continue;
|
||||
|
||||
snapshot.completedBlockIds ??= new List<string>();
|
||||
snapshot.selectedChoices ??= new Dictionary<string, StoryChoiceRecord>();
|
||||
snapshot.storyVariables = NormalizeVariables(snapshot.storyVariables);
|
||||
}
|
||||
|
||||
return save;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 补齐全局剧情变量字典,保证新建 v1 存档可被 Yarn 变量访问。
|
||||
/// 补齐章节变量的三种底层字典。数值统一使用 float 字典保存,整数读取时由 StoryVariables 转回整数。
|
||||
/// </summary>
|
||||
private static StoryVariablesSave NormalizeVariables(StoryVariablesSave storyVariables)
|
||||
internal static StoryVariablesSave NormalizeVariables(StoryVariablesSave storyVariables)
|
||||
{
|
||||
storyVariables ??= new StoryVariablesSave();
|
||||
storyVariables.floatVariables ??= new Dictionary<string, float>();
|
||||
@@ -59,10 +78,8 @@ namespace Ichni
|
||||
return storyVariables;
|
||||
}
|
||||
|
||||
// ── 故事树 + 选项(按章节)────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 判断某章节是否尚无可用的 v1 存档(首次进入或旧测试存档待重置)。
|
||||
/// 当前是否没有可用的 v1 章节存档。此方法仅用于调试/展示;实际读取仍应调用 <see cref="GetChapter"/>。
|
||||
/// </summary>
|
||||
public bool IsNewChapter(string chapterIndex)
|
||||
{
|
||||
@@ -73,8 +90,8 @@ namespace Ichni
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 加载指定章节的 v1 存档容器;无存档或非 v1 的预发布测试存档会直接重置为空容器。
|
||||
/// 加载结果会写入内存缓存。
|
||||
/// 从磁盘加载一个章节的剧情存档。开发期结构替换后若 ES3 无法反序列化旧 v1 文件,
|
||||
/// 会安全重置该章节文件;这是用户已确认的发布前删档策略。
|
||||
/// </summary>
|
||||
public ChapterStorySave LoadChapter(string chapterIndex)
|
||||
{
|
||||
@@ -86,39 +103,115 @@ namespace Ichni
|
||||
|
||||
if (hasExistingFile && loadedSchemaVersion != CurrentStorySaveSchemaVersion)
|
||||
{
|
||||
Debug.LogWarning($"Resetting pre-release chapter story save '{chapterIndex}' from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}.");
|
||||
Debug.LogWarning($"[StorySave] Resetting pre-release chapter '{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();
|
||||
ChapterStorySave save;
|
||||
try
|
||||
{
|
||||
save = hasExistingFile && ES3.KeyExists(ChapterStoryKey, path)
|
||||
? ES3.Load<ChapterStorySave>(ChapterStoryKey, path)
|
||||
: new ChapterStorySave();
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// 典型原因是开发期 selectedChoices 从 int 改为 StoryChoiceRecord 后的旧 ES3 数据。
|
||||
// 因 Schema 仍为 v1 且项目未发行,按约定仅重置本章节,而不影响任何其它存档系统。
|
||||
Debug.LogWarning($"[StorySave] Resetting unreadable pre-release chapter '{chapterIndex}'. {exception.Message}");
|
||||
if (ES3.FileExists(path))
|
||||
ES3.DeleteFile(path);
|
||||
save = new ChapterStorySave();
|
||||
}
|
||||
|
||||
save = NormalizeChapterSave(save, chapterIndex);
|
||||
_chapterSaves[chapterIndex] = save;
|
||||
return save;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 保存整章存档(blocks + connectors + choices),单次 ES3 写入。
|
||||
/// 将一个完整章节容器写入 ES3。调用方应先更新 Block、变量、选项或快照,再一次性调用本方法。
|
||||
/// </summary>
|
||||
public void SaveChapter(ChapterStorySave save)
|
||||
{
|
||||
if (save == null || string.IsNullOrEmpty(save.chapterIndex))
|
||||
{
|
||||
Debug.LogWarning("Cannot save a chapter story record without a chapterIndex.");
|
||||
Debug.LogWarning("[StorySave] Cannot save a chapter record without chapterIndex.");
|
||||
return;
|
||||
}
|
||||
|
||||
save = NormalizeChapterSave(save, save.chapterIndex);
|
||||
_chapterSaves[save.chapterIndex] = save;
|
||||
|
||||
if (IsChapterTransactionActive(save.chapterIndex))
|
||||
return;
|
||||
|
||||
SaveChapterImmediately(save);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 开始指定章节的临时写入事务。
|
||||
/// 对话播放期间的选项、变量、Marker 快照和 Block 完成状态仍会写入内存,
|
||||
/// 但不会立即覆盖 ES3;这样中途退出时不会把半段剧情永久记录到存档。
|
||||
/// </summary>
|
||||
public bool BeginChapterTransaction(string chapterIndex)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(chapterIndex))
|
||||
return false;
|
||||
|
||||
if (!string.IsNullOrEmpty(_transactionChapterIndex))
|
||||
{
|
||||
Debug.LogWarning($"[StorySave] Chapter transaction '{_transactionChapterIndex}' is already active.");
|
||||
return false;
|
||||
}
|
||||
|
||||
_transactionChapterIndex = chapterIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交指定章节的临时写入事务,并将该章节当前的完整内存状态一次性写入 ES3。
|
||||
/// 仅应在 TextBlock 的 Yarn 对话正常结束后调用。
|
||||
/// </summary>
|
||||
public void CommitChapterTransaction(string chapterIndex)
|
||||
{
|
||||
if (!IsChapterTransactionActive(chapterIndex))
|
||||
return;
|
||||
|
||||
_transactionChapterIndex = null;
|
||||
SaveChapterImmediately(GetChapter(chapterIndex));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 放弃当前章节事务,并用进入对话前的快照恢复内存和 ES3。
|
||||
/// 此方法只作用于章节剧情数据,不会回滚歌曲成绩、解锁 Key、设置或其它章节的数据。
|
||||
/// </summary>
|
||||
public void RollbackChapterTransaction(string chapterIndex, ChapterStorySave snapshot)
|
||||
{
|
||||
if (!IsChapterTransactionActive(chapterIndex) || snapshot == null)
|
||||
return;
|
||||
|
||||
ChapterStorySave restored = StorySaveCloneUtility.CloneChapter(snapshot);
|
||||
restored = NormalizeChapterSave(restored, chapterIndex);
|
||||
_transactionChapterIndex = null;
|
||||
_chapterSaves[chapterIndex] = restored;
|
||||
SaveChapterImmediately(restored);
|
||||
}
|
||||
|
||||
/// <summary>当前指定章节是否正处于由 Story TextBlock 开启的临时存档事务中。</summary>
|
||||
public bool IsChapterTransactionActive(string chapterIndex) =>
|
||||
!string.IsNullOrEmpty(chapterIndex) && _transactionChapterIndex == chapterIndex;
|
||||
|
||||
private void SaveChapterImmediately(ChapterStorySave save)
|
||||
{
|
||||
string path = GetChapterSavePath(save.chapterIndex);
|
||||
ES3.Save(ChapterStoryKey, save, path);
|
||||
ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取内存中的章节存档;若未加载则从磁盘加载。
|
||||
/// 获取章节的内存存档;首次访问时自动从磁盘加载。
|
||||
/// </summary>
|
||||
public ChapterStorySave GetChapter(string chapterIndex)
|
||||
{
|
||||
@@ -127,63 +220,76 @@ namespace Ichni
|
||||
: LoadChapter(chapterIndex);
|
||||
}
|
||||
|
||||
// ── 对话选项(按章节)──────────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 记录某章节内一次对话选项的选择结果。
|
||||
/// 返回当前已打开章节的变量容器。章节尚未建立时返回非持久化兜底容器,供独立场景安全运行。
|
||||
/// </summary>
|
||||
public void SetChoice(string chapterIndex, string choiceKey, int optionIndex)
|
||||
public StoryVariablesSave GetActiveChapterVariables()
|
||||
{
|
||||
GetChapter(chapterIndex).selectedChoices[choiceKey] = optionIndex;
|
||||
string chapterIndex = StoryManager.instance?.treeController?.ActiveChapterIndex;
|
||||
return string.IsNullOrEmpty(chapterIndex)
|
||||
? _fallbackVariables
|
||||
: GetChapter(chapterIndex).storyVariables;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取某章节内已记录的对话选项选择结果。
|
||||
/// 立即保存当前已打开章节。返回 false 代表当前没有章节上下文,因此没有产生磁盘写入。
|
||||
/// </summary>
|
||||
public bool TryGetChoice(string chapterIndex, string choiceKey, out int optionIndex)
|
||||
public bool SaveActiveChapter()
|
||||
{
|
||||
return GetChapter(chapterIndex).selectedChoices.TryGetValue(choiceKey, out optionIndex);
|
||||
string chapterIndex = StoryManager.instance?.treeController?.ActiveChapterIndex;
|
||||
if (string.IsNullOrEmpty(chapterIndex))
|
||||
return false;
|
||||
|
||||
SaveChapter(GetChapter(chapterIndex));
|
||||
return true;
|
||||
}
|
||||
|
||||
// ── 剧情变量(全局)──────────────────────────────────────────────────────
|
||||
// ── Yarn 选项(章节内) ─────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// 从磁盘加载全局剧情变量(启动时调用)。
|
||||
/// 保存一次 Yarn 选项。选项写入应在对应 Timeline 快照已创建后发生,
|
||||
/// 由 StoryChoiceMemory 统一保证该顺序;若当前 TextBlock 事务仍在进行,只更新内存,
|
||||
/// 直到对话正常结束后才统一落盘。
|
||||
/// </summary>
|
||||
public void LoadVariables()
|
||||
public void SetChoice(string chapterIndex, StoryChoiceRecord record)
|
||||
{
|
||||
bool hasExistingFile = ES3.FileExists(VariablesPath);
|
||||
int loadedSchemaVersion = hasExistingFile && ES3.KeyExists(StorySaveSchemaVersionKey, VariablesPath)
|
||||
? ES3.Load<int>(StorySaveSchemaVersionKey, VariablesPath)
|
||||
: 0;
|
||||
|
||||
if (hasExistingFile && loadedSchemaVersion != CurrentStorySaveSchemaVersion)
|
||||
if (string.IsNullOrEmpty(chapterIndex) || record == null || string.IsNullOrEmpty(record.choiceKey))
|
||||
{
|
||||
Debug.LogWarning($"Resetting pre-release global story variables from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}.");
|
||||
ES3.DeleteFile(VariablesPath);
|
||||
hasExistingFile = false;
|
||||
Debug.LogWarning("[StorySave] Cannot save a story choice without chapterIndex and choiceKey.");
|
||||
return;
|
||||
}
|
||||
|
||||
variables = hasExistingFile && ES3.KeyExists(VariablesKey, VariablesPath)
|
||||
? ES3.Load<StoryVariablesSave>(VariablesKey, VariablesPath)
|
||||
: new StoryVariablesSave();
|
||||
variables = NormalizeVariables(variables);
|
||||
ChapterStorySave save = GetChapter(chapterIndex);
|
||||
save.selectedChoices[record.choiceKey] = record;
|
||||
SaveChapter(save);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将全局剧情变量写入磁盘。
|
||||
/// </summary>
|
||||
public void SaveVariables()
|
||||
public bool TryGetChoice(string chapterIndex, string choiceKey, out StoryChoiceRecord record)
|
||||
{
|
||||
variables = NormalizeVariables(variables);
|
||||
ES3.Save(VariablesKey, variables, VariablesPath);
|
||||
ES3.Save(StorySaveSchemaVersionKey, CurrentStorySaveSchemaVersion, VariablesPath);
|
||||
if (string.IsNullOrEmpty(chapterIndex) || string.IsNullOrEmpty(choiceKey))
|
||||
{
|
||||
record = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
return GetChapter(chapterIndex).selectedChoices.TryGetValue(choiceKey, out record) && record != null;
|
||||
}
|
||||
|
||||
// ── 清档 ────────────────────────────────────────────────────────────────
|
||||
/// <summary>
|
||||
/// 删除一个失效选项记录。例如 Yarn 改稿后已选 Text ID 不再存在,下一次进入应让玩家重新选择。
|
||||
/// </summary>
|
||||
public void RemoveChoice(string chapterIndex, string choiceKey)
|
||||
{
|
||||
if (string.IsNullOrEmpty(chapterIndex) || string.IsNullOrEmpty(choiceKey))
|
||||
return;
|
||||
|
||||
ChapterStorySave save = GetChapter(chapterIndex);
|
||||
if (save.selectedChoices.Remove(choiceKey))
|
||||
SaveChapter(save);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清除所有章节的故事树存档与全局剧情变量。
|
||||
/// 清除全部章节剧情存档及遗留全局变量文件。不会清除歌曲成绩、设置或 Offline 解锁 Key。
|
||||
/// </summary>
|
||||
public void ClearAllStoryline()
|
||||
{
|
||||
@@ -198,13 +304,14 @@ namespace Ichni
|
||||
}
|
||||
|
||||
_chapterSaves.Clear();
|
||||
_fallbackVariables.floatVariables.Clear();
|
||||
_fallbackVariables.stringVariables.Clear();
|
||||
_fallbackVariables.boolVariables.Clear();
|
||||
|
||||
if (ES3.FileExists(VariablesPath))
|
||||
ES3.DeleteFile(VariablesPath);
|
||||
if (ES3.FileExists(LegacyVariablesPath))
|
||||
ES3.DeleteFile(LegacyVariablesPath);
|
||||
|
||||
variables = new StoryVariablesSave();
|
||||
|
||||
Debug.Log("Cleared all story saves (chapters + global variables).");
|
||||
Debug.Log("[StorySave] Cleared all chapter story saves.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user