2026-07-05 16:08:23 -04:00
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" ;
2026-07-18 16:51:18 -04:00
// 每个剧情存档文件都保存自己的 Schema Version; 不要把它与歌曲成绩存档的 Version 混用。
// 当前项目尚未正式发布,剧情存档统一从 v1 开始;非 v1 的测试存档会被直接重置。
// 正式发布后升级 Schema 时,必须用非破坏性迁移替代当前的重置策略。
private const string StorySaveSchemaVersionKey = "StorySaveSchemaVersion" ;
private const int CurrentStorySaveSchemaVersion = 1 ;
2026-07-05 16:08:23 -04:00
// 已加载章节的内存缓存( 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" ;
2026-07-18 16:51:18 -04:00
/// <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 ;
}
2026-07-05 16:08:23 -04:00
// ── 故事树 + 选项(按章节)────────────────────────────────────────────────
/// <summary>
2026-07-18 16:51:18 -04:00
/// 判断某章节是否尚无可用的 v1 存档(首次进入或旧测试存档待重置)。
2026-07-05 16:08:23 -04:00
/// </summary>
2026-07-18 16:51:18 -04:00
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 ;
}
2026-07-05 16:08:23 -04:00
/// <summary>
2026-07-18 16:51:18 -04:00
/// 加载指定章节的 v1 存档容器;无存档或非 v1 的预发布测试存档会直接重置为空容器。
/// 加载结果会写入内存缓存。
2026-07-05 16:08:23 -04:00
/// </summary>
public ChapterStorySave LoadChapter ( string chapterIndex )
{
string path = GetChapterSavePath ( chapterIndex ) ;
2026-07-18 16:51:18 -04:00
bool hasExistingFile = ES3 . FileExists ( path ) ;
int loadedSchemaVersion = hasExistingFile & & ES3 . KeyExists ( StorySaveSchemaVersionKey , path )
? ES3 . Load < int > ( StorySaveSchemaVersionKey , path )
: 0 ;
2026-07-05 16:08:23 -04:00
2026-07-18 16:51:18 -04:00
if ( hasExistingFile & & loadedSchemaVersion ! = CurrentStorySaveSchemaVersion )
2026-07-05 16:08:23 -04:00
{
2026-07-18 16:51:18 -04:00
Debug . LogWarning ( $"Resetting pre-release chapter story save '{chapterIndex}' from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}." ) ;
ES3 . DeleteFile ( path ) ;
hasExistingFile = false ;
2026-07-05 16:08:23 -04:00
}
2026-07-18 16:51:18 -04:00
ChapterStorySave save = hasExistingFile & & ES3 . KeyExists ( ChapterStoryKey , path )
? ES3 . Load < ChapterStorySave > ( ChapterStoryKey , path )
: new ChapterStorySave ( ) ;
save = NormalizeChapterSave ( save , chapterIndex ) ;
2026-07-05 16:08:23 -04:00
_chapterSaves [ chapterIndex ] = save ;
return save ;
}
/// <summary>
/// 保存整章存档( blocks + connectors + choices) , 单次 ES3 写入。
/// </summary>
public void SaveChapter ( ChapterStorySave save )
{
2026-07-18 16:51:18 -04:00
if ( save = = null | | string . IsNullOrEmpty ( save . chapterIndex ) )
{
Debug . LogWarning ( "Cannot save a chapter story record without a chapterIndex." ) ;
return ;
}
save = NormalizeChapterSave ( save , save . chapterIndex ) ;
2026-07-05 16:08:23 -04:00
_chapterSaves [ save . chapterIndex ] = save ;
2026-07-18 16:51:18 -04:00
string path = GetChapterSavePath ( save . chapterIndex ) ;
ES3 . Save ( ChapterStoryKey , save , path ) ;
ES3 . Save ( StorySaveSchemaVersionKey , CurrentStorySaveSchemaVersion , path ) ;
2026-07-05 16:08:23 -04:00
}
/// <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 ( )
{
2026-07-18 16:51:18 -04:00
bool hasExistingFile = ES3 . FileExists ( VariablesPath ) ;
int loadedSchemaVersion = hasExistingFile & & ES3 . KeyExists ( StorySaveSchemaVersionKey , VariablesPath )
? ES3 . Load < int > ( StorySaveSchemaVersionKey , VariablesPath )
: 0 ;
if ( hasExistingFile & & loadedSchemaVersion ! = CurrentStorySaveSchemaVersion )
2026-07-05 16:08:23 -04:00
{
2026-07-18 16:51:18 -04:00
Debug . LogWarning ( $"Resetting pre-release global story variables from schema v{loadedSchemaVersion} to v{CurrentStorySaveSchemaVersion}." ) ;
ES3 . DeleteFile ( VariablesPath ) ;
hasExistingFile = false ;
2026-07-05 16:08:23 -04:00
}
2026-07-18 16:51:18 -04:00
variables = hasExistingFile & & ES3 . KeyExists ( VariablesKey , VariablesPath )
? ES3 . Load < StoryVariablesSave > ( VariablesKey , VariablesPath )
: new StoryVariablesSave ( ) ;
variables = NormalizeVariables ( variables ) ;
2026-07-05 16:08:23 -04:00
}
/// <summary>
/// 将全局剧情变量写入磁盘。
/// </summary>
public void SaveVariables ( )
{
2026-07-18 16:51:18 -04:00
variables = NormalizeVariables ( variables ) ;
2026-07-05 16:08:23 -04:00
ES3 . Save ( VariablesKey , variables , VariablesPath ) ;
2026-07-18 16:51:18 -04:00
ES3 . Save ( StorySaveSchemaVersionKey , CurrentStorySaveSchemaVersion , VariablesPath ) ;
2026-07-05 16:08:23 -04:00
}
// ── 清档 ────────────────────────────────────────────────────────────────
/// <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)." ) ;
}
}
}