feat: GameManager相关重构
This commit is contained in:
@@ -8,13 +8,15 @@ namespace AibisDream.SaveSystem
|
||||
[Serializable]
|
||||
public sealed class RestoreOptions
|
||||
{
|
||||
public bool CleanSessionFirst;
|
||||
public bool StrictValidation;
|
||||
public bool StrictValidation = true;
|
||||
|
||||
public static RestoreOptions Default => new()
|
||||
{
|
||||
StrictValidation = true
|
||||
};
|
||||
|
||||
public static RestoreOptions Default => new();
|
||||
public static RestoreOptions DevJump => new()
|
||||
{
|
||||
CleanSessionFirst = true,
|
||||
StrictValidation = true
|
||||
};
|
||||
}
|
||||
@@ -22,10 +24,32 @@ namespace AibisDream.SaveSystem
|
||||
public sealed class RestoreResult
|
||||
{
|
||||
public bool Success { get; internal set; }
|
||||
public bool Cancelled { get; internal set; }
|
||||
public string FailedPhase { get; internal set; }
|
||||
public IReadOnlyList<string> Errors { get; internal set; } = Array.Empty<string>();
|
||||
public IReadOnlyList<string> Warnings { get; internal set; } = Array.Empty<string>();
|
||||
public IReadOnlyList<string> Log { get; internal set; } = Array.Empty<string>();
|
||||
public TalkSceneSO RestoredTalkScene { get; internal set; }
|
||||
|
||||
public static RestoreResult CreateFailure(string phase, string error)
|
||||
{
|
||||
return new RestoreResult
|
||||
{
|
||||
Success = false,
|
||||
FailedPhase = phase,
|
||||
Errors = new[] { error }
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RestorePreparation
|
||||
{
|
||||
internal bool Success => Result?.Success == true;
|
||||
internal SaveSnapshot Snapshot { get; set; }
|
||||
internal TalkSceneSO TargetTalkScene { get; set; }
|
||||
internal string SourceLabel { get; set; }
|
||||
internal RestoreOptions Options { get; set; }
|
||||
internal RestoreResult Result { get; set; } = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -70,14 +94,20 @@ namespace AibisDream.SaveSystem
|
||||
/// </summary>
|
||||
public sealed class SnapshotRestoreContext
|
||||
{
|
||||
public SnapshotRestoreContext(SaveSnapshot snapshot, bool strictMode = false, Action<string> logStep = null)
|
||||
public SnapshotRestoreContext(
|
||||
SaveSnapshot snapshot,
|
||||
TalkSceneSO targetTalkScene = null,
|
||||
bool strictMode = false,
|
||||
Action<string> logStep = null)
|
||||
{
|
||||
Snapshot = snapshot;
|
||||
TargetTalkScene = targetTalkScene;
|
||||
StrictMode = strictMode;
|
||||
LogStep = logStep;
|
||||
}
|
||||
|
||||
public SaveSnapshot Snapshot { get; }
|
||||
public TalkSceneSO TargetTalkScene { get; }
|
||||
public bool StrictMode { get; }
|
||||
public Action<string> LogStep { get; }
|
||||
public string CurrentPhase { get; private set; }
|
||||
|
||||
@@ -24,22 +24,17 @@ yield return SaveRestoreOrchestrator.ExplicitSaveRoutine();
|
||||
// 手动存档:复制最近落盘档(含 OnNodeStart 与 <<save>>)
|
||||
SaveRestoreOrchestrator.CreateManualSlot(slotIndex);
|
||||
|
||||
// 槽位读档
|
||||
yield return SaveRestoreOrchestrator.RestoreFromSlot(slotIndex);
|
||||
// 槽位读档:会话命令统一从 GameManager 进入
|
||||
GameManager.Instance.TryRestoreSlot(slotIndex, result => { /* 处理结果 */ });
|
||||
|
||||
// 文件读档(旧档 / 调试)
|
||||
yield return SaveRestoreOrchestrator.RestoreFromFile(path);
|
||||
|
||||
// 严格开发跳转:完整清理旧会话,并取得实际成功/失败结果
|
||||
RestoreResult result = null;
|
||||
yield return SaveRestoreOrchestrator.RestoreFromFile(
|
||||
// 严格文件读档 / 开发跳转
|
||||
GameManager.Instance.TryRestoreFile(
|
||||
path,
|
||||
RestoreOptions.DevJump,
|
||||
value => result = value);
|
||||
result => { /* 处理结果 */ });
|
||||
|
||||
// 仅内存快照
|
||||
// 仅捕获内存快照;恢复仍须通过 GameManager,以保证会话互斥和失败清理
|
||||
var snap = SnapshotService.Capture();
|
||||
yield return SnapshotService.Restore(snap);
|
||||
|
||||
// Yarn 变量(与存档无关)
|
||||
YarnVariableStorage.Instance.SetValue("$foo", 1f);
|
||||
@@ -118,17 +113,18 @@ SaveSystem/
|
||||
→ SnapshotPersistence.Save()
|
||||
|
||||
读盘:
|
||||
SaveRestoreOrchestrator
|
||||
→ SnapshotPersistence.Load() (或 legacy 分支)
|
||||
→ SnapshotService.Restore()
|
||||
→ SnapshotRestore(见「读档还原编排」)
|
||||
GameManager
|
||||
→ SaveRestoreOrchestrator.PrepareRestore(读盘 + 预检,不修改运行时)
|
||||
→ SaveRestoreOrchestrator.ExecutePreparedRestore
|
||||
→ SnapshotRestore(变量 + Scene + Providers + Anchor)
|
||||
→ GameManager 提交章节和会话状态
|
||||
```
|
||||
|
||||
## 读档还原编排(终态 vs P1 代码)
|
||||
## 读档还原编排
|
||||
|
||||
> **勿将当前代码当作终态。** P1 中 `ISnapshotProvider.Restore` 统一返回 `IEnumerator`,`SnapshotRestore` 对每个 Provider 做 `yield return`——这是受旧 `IData.Load()` 影响的**临时 scaffolding**。终态见设计文档 **§4.1 / D6**;P4 实施前应 refactor。
|
||||
读档已经采用 Prepare / Execute 边界与 Phase + Barrier 模型。所有文件统一反序列化,并由 schema 与必填字段预检判断是否兼容;不存在旧格式专用识别、恢复或转写分支。
|
||||
|
||||
### 终态:Phase + Barrier
|
||||
### Phase + Barrier
|
||||
|
||||
读档分阶段推进,仅在**必须等待**的边界上 `yield`:
|
||||
|
||||
@@ -137,35 +133,23 @@ Phase 0 Yarn 变量(sync)
|
||||
↓
|
||||
Phase 1 场景加载(Barrier:LoadSceneAsync)← 框架直管,不走 Provider
|
||||
↓
|
||||
Phase 1.5 设置章节 SO(sync)← 框架直管,从 anchor.sceneSoName 读取
|
||||
↓
|
||||
Phase 2 env / actor / audio / timeline / fix / screen(Provider 按 RestoreOrder)
|
||||
└─ timeline:`Stopped` = untouched(从未 Evaluate),读档仅还原 `isActive`,不 Reset/Evaluate
|
||||
↓
|
||||
Phase 2′ 可选 Barrier(如 Timeline Addressable 须显式等待)
|
||||
↓
|
||||
Phase 2.5 SceneReadiness(Barrier)
|
||||
↓
|
||||
Phase 3 加载对话工程 + RestoreAnchor(Barrier:StartDialogue)← 框架直管
|
||||
↓
|
||||
Phase 4 P4:淡入淡出等演出时序
|
||||
Postflight 校验
|
||||
```
|
||||
|
||||
- **核心层**(`scene`、`anchor`、`yarnVariables`)由 `SnapshotRestore` 框架直管,不通过 Provider 注册。
|
||||
- **表现层**(`sections` 内各子系统)通过 `ISnapshotProvider` 扩展;`RestoreOrder` 表示同 Phase 内的建议顺序或软依赖。
|
||||
- **逐项还原(D1)**指各子系统各自写回状态,**不是** Provider 之间逐步 `yield return`。
|
||||
|
||||
### P1 临时形态 vs P4 目标
|
||||
|
||||
| | P1(当前) | P4 目标 |
|
||||
| --- | --- | --- |
|
||||
| 核心层还原 | 硬编码在 `SnapshotRestore` | 保持框架直管 |
|
||||
| Provider 还原 | 全部 `IEnumerator Restore` | 默认 `void Restore`;仅需 async 加载的实现显式协程 |
|
||||
| 编排 | `foreach` 逐步 `yield return` | Phase 编排,仅 Barrier 步骤 `yield` |
|
||||
| Manager | 部分 `IEnumerator RestoreSnapshot` 仅 `yield break` | 默认 `void`;真有 async 才保留协程 |
|
||||
|
||||
### 已知偏离(tech debt)
|
||||
|
||||
- `DirectorHandler` 在 Addressable Timeline 路径下内部 `StartCoroutine`,Provider 已返回,编排层无法感知——P4 应改为可等待路径。
|
||||
- P2/P3 新增代码**不要**再复制「全 Provider 协程链」模式。
|
||||
Provider 使用 `ISyncSnapshotProvider` 或 `IAsyncSnapshotProvider` 显式声明同步/异步恢复;异步 Provider 必须把等待过程返回给编排层,禁止内部 fire-and-forget。
|
||||
|
||||
## 快照 JSON 结构(schemaVersion = 1)
|
||||
|
||||
|
||||
@@ -230,7 +230,7 @@ namespace AibisDream.SaveSystem
|
||||
return false;
|
||||
}
|
||||
|
||||
if (GameManager.Instance != null && GameManager.Instance.state.isInPause)
|
||||
if (GameManager.Instance != null && GameManager.Session.IsPaused)
|
||||
{
|
||||
reason = SavePointRejectReason.GamePaused;
|
||||
return false;
|
||||
|
||||
@@ -25,8 +25,6 @@ namespace AibisDream.SaveSystem
|
||||
private static int _autoSaveSuppressDepth;
|
||||
private static readonly List<string> _lastRestoreLog = new();
|
||||
|
||||
private const float RestoreFadeDuration = 0.2f;
|
||||
|
||||
/// <summary>启动自动存档协程(手动/调试入口)。</summary>
|
||||
public static void TryAutoSave()
|
||||
{
|
||||
@@ -138,86 +136,145 @@ namespace AibisDream.SaveSystem
|
||||
SlotManager.CopyAutoToManual(slotIndex);
|
||||
}
|
||||
|
||||
/// <summary>从指定槽位读档并还原。</summary>
|
||||
public static IEnumerator RestoreFromSlot(int slotIndex)
|
||||
internal static RestorePreparation PrepareRestoreFromSlot(
|
||||
int slotIndex,
|
||||
TalkSceneGraphIndex talkSceneIndex,
|
||||
RestoreOptions options)
|
||||
{
|
||||
var snapshot = SlotManager.LoadSnapshot(slotIndex);
|
||||
if (snapshot == null)
|
||||
{
|
||||
Debug.LogError($"[SaveRestoreOrchestrator] 槽位 {slotIndex} 不存在或读取失败");
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return RestoreSnapshot(snapshot, $"slot_{slotIndex}");
|
||||
}
|
||||
|
||||
/// <summary>从文件读档并还原;自动区分新快照格式与 legacy 格式。</summary>
|
||||
public static IEnumerator RestoreFromFile(string savePath)
|
||||
{
|
||||
yield return RestoreFromFile(savePath, RestoreOptions.Default, null);
|
||||
}
|
||||
|
||||
/// <summary>从文件恢复并返回结构化结果;开发跳转使用严格校验和完整会话清理。</summary>
|
||||
public static IEnumerator RestoreFromFile(
|
||||
string savePath,
|
||||
RestoreOptions options,
|
||||
Action<RestoreResult> completed)
|
||||
{
|
||||
options ??= RestoreOptions.Default;
|
||||
var result = new RestoreResult();
|
||||
|
||||
bool isLegacy;
|
||||
var sourceLabel = $"slot_{slotIndex}";
|
||||
SaveSnapshot snapshot;
|
||||
try
|
||||
{
|
||||
isLegacy = SnapshotPersistence.IsLegacyFormat(savePath);
|
||||
snapshot = SlotManager.LoadSnapshot(slotIndex);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.FailedPhase = "LoadSnapshot";
|
||||
result.Errors = new[] { ex.Message };
|
||||
completed?.Invoke(result);
|
||||
yield break;
|
||||
return FailedPreparation(sourceLabel, "LoadSnapshot", ex.Message, options);
|
||||
}
|
||||
|
||||
if (isLegacy)
|
||||
if (snapshot == null)
|
||||
{
|
||||
if (options.StrictValidation)
|
||||
{
|
||||
result.Success = false;
|
||||
result.FailedPhase = "Preflight";
|
||||
result.Errors = new[] { "legacy format is not supported by strict restore" };
|
||||
completed?.Invoke(result);
|
||||
yield break;
|
||||
}
|
||||
|
||||
Debug.LogWarning("[SaveRestoreOrchestrator] 检测到旧格式存档,请使用新快照格式重新保存。");
|
||||
yield return RestoreLegacyWithFlow(savePath);
|
||||
result.Success = true;
|
||||
result.Log = new List<string>(_lastRestoreLog);
|
||||
completed?.Invoke(result);
|
||||
yield break;
|
||||
return FailedPreparation(
|
||||
sourceLabel,
|
||||
"LoadSnapshot",
|
||||
$"Slot {slotIndex} does not exist or could not be read.",
|
||||
options);
|
||||
}
|
||||
|
||||
SaveSnapshot snapshot = null;
|
||||
return PrepareSnapshot(snapshot, sourceLabel, talkSceneIndex, options);
|
||||
}
|
||||
|
||||
internal static RestorePreparation PrepareRestoreFromFile(
|
||||
string savePath,
|
||||
TalkSceneGraphIndex talkSceneIndex,
|
||||
RestoreOptions options)
|
||||
{
|
||||
options ??= RestoreOptions.Default;
|
||||
try
|
||||
{
|
||||
SaveSnapshot snapshot;
|
||||
using (new CodeTimer("LoadSnapshot"))
|
||||
{
|
||||
snapshot = SnapshotPersistence.Load(savePath);
|
||||
}
|
||||
|
||||
return PrepareSnapshot(snapshot, savePath, talkSceneIndex, options);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
result.Success = false;
|
||||
result.FailedPhase = "LoadSnapshot";
|
||||
result.Errors = new[] { ex.Message };
|
||||
completed?.Invoke(result);
|
||||
return FailedPreparation(savePath, "LoadSnapshot", ex.Message, options);
|
||||
}
|
||||
}
|
||||
|
||||
internal static IEnumerator ExecutePreparedRestore(
|
||||
RestorePreparation preparation,
|
||||
Func<bool> isCancellationRequested)
|
||||
{
|
||||
if (preparation?.Success != true)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return RestoreSnapshot(snapshot, savePath, options, result);
|
||||
completed?.Invoke(result);
|
||||
var result = preparation.Result;
|
||||
result.Success = false;
|
||||
|
||||
using (SuppressAutoSaveScope($"RestoreSnapshot:{preparation.SourceLabel}"))
|
||||
{
|
||||
IsRestoring = true;
|
||||
ResetRestoreLog(preparation.SourceLabel);
|
||||
var context = new SnapshotRestoreContext(
|
||||
preparation.Snapshot,
|
||||
preparation.TargetTalkScene,
|
||||
strictMode: preparation.Options?.StrictValidation ?? true,
|
||||
logStep: AddRestoreLog);
|
||||
|
||||
try
|
||||
{
|
||||
SnapshotRegistry.EnsureInitialized();
|
||||
yield return SnapshotRestore.RestoreState(
|
||||
YarnVariableStorage.Instance,
|
||||
preparation.Snapshot,
|
||||
context,
|
||||
isCancellationRequested);
|
||||
|
||||
if (ShouldCancel(isCancellationRequested, result, context))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (context.StrictMode && context.HasErrors)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
context.SetPhase("Phase 2.5: Scene readiness");
|
||||
SceneReadinessResult readiness = null;
|
||||
yield return SceneReadiness.WaitUntilReady(
|
||||
SceneLoader.Instance.CurrentScene,
|
||||
isCancellationRequested,
|
||||
value => readiness = value);
|
||||
|
||||
if (readiness?.Cancelled == true)
|
||||
{
|
||||
result.Cancelled = true;
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (readiness?.Success != true)
|
||||
{
|
||||
context.Error(readiness?.Error ?? "Scene readiness did not return a result.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (ShouldCancel(isCancellationRequested, result, context))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return SnapshotRestore.RestoreAnchor(preparation.Snapshot, context);
|
||||
if (!context.StrictMode || !context.HasErrors)
|
||||
{
|
||||
context.SetPhase("Postflight validation");
|
||||
ValidateRestoredRuntime(preparation, context);
|
||||
}
|
||||
|
||||
result.Success = !context.HasErrors;
|
||||
if (result.Success)
|
||||
{
|
||||
result.RestoredTalkScene = preparation.TargetTalkScene;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRestoring = false;
|
||||
AddRestoreLog("Restore finished");
|
||||
result.Success = result.Success && !result.Cancelled && !context.HasErrors;
|
||||
result.FailedPhase = result.Success || result.Cancelled ? null : context.CurrentPhase;
|
||||
result.Errors = new List<string>(context.Errors);
|
||||
result.Warnings = new List<string>(context.Warnings);
|
||||
result.Log = new List<string>(_lastRestoreLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IDisposable SuppressAutoSaveScope(string reason)
|
||||
@@ -227,180 +284,122 @@ namespace AibisDream.SaveSystem
|
||||
return new AutoSaveSuppressScope(reason);
|
||||
}
|
||||
|
||||
/// <summary>旧档:仅恢复 Yarn 变量,再 Capture 写入 slot_0 完成格式迁移。</summary>
|
||||
private static IEnumerator RestoreLegacy(string savePath)
|
||||
{
|
||||
var saveData = SnapshotPersistence.ReadLegacySaveRoot(savePath);
|
||||
var floatDict = saveData["floatDict"]?.ToObject<Dictionary<string, float>>();
|
||||
var stringDict = saveData["stringDict"]?.ToObject<Dictionary<string, string>>();
|
||||
var boolDict = saveData["boolDict"]?.ToObject<Dictionary<string, bool>>();
|
||||
YarnVariableStorage.Instance.SetAllVariables(floatDict, stringDict, boolDict);
|
||||
|
||||
if (saveData["systemData"] != null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"[SaveRestoreOrchestrator] 旧档 systemData 段已无法还原(IData 已移除);" +
|
||||
"仅恢复 Yarn 变量并迁移为新快照格式。");
|
||||
}
|
||||
|
||||
var snapshot = SnapshotService.Capture();
|
||||
SlotManager.SaveToAutoSlot(snapshot, null);
|
||||
yield break;
|
||||
}
|
||||
|
||||
private static IEnumerator RestoreLegacyWithFlow(string savePath)
|
||||
{
|
||||
using (SuppressAutoSaveScope("RestoreLegacy"))
|
||||
{
|
||||
IsRestoring = true;
|
||||
ResetRestoreLog("legacy");
|
||||
|
||||
try
|
||||
{
|
||||
PrepareGameSessionForRestore();
|
||||
yield return FadeInForRestore();
|
||||
yield return RestoreLegacy(savePath);
|
||||
yield return null;
|
||||
FinalizeGameSessionAfterRestore();
|
||||
yield return FadeOutForRestore();
|
||||
ScreenSnapshotHelper.ApplyDeferredFadeScreenIfNeeded();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRestoring = false;
|
||||
AddRestoreLog("Legacy restore finished");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator RestoreSnapshot(SaveSnapshot snapshot, string sourceLabel)
|
||||
{
|
||||
yield return RestoreSnapshot(snapshot, sourceLabel, RestoreOptions.Default, new RestoreResult());
|
||||
}
|
||||
|
||||
private static IEnumerator RestoreSnapshot(
|
||||
private static RestorePreparation PrepareSnapshot(
|
||||
SaveSnapshot snapshot,
|
||||
string sourceLabel,
|
||||
RestoreOptions options,
|
||||
RestoreResult result)
|
||||
TalkSceneGraphIndex talkSceneIndex,
|
||||
RestoreOptions options)
|
||||
{
|
||||
options ??= RestoreOptions.Default;
|
||||
var preparation = new RestorePreparation
|
||||
{
|
||||
Snapshot = snapshot,
|
||||
SourceLabel = sourceLabel,
|
||||
Options = options,
|
||||
Result = new RestoreResult()
|
||||
};
|
||||
|
||||
var errors = new List<string>();
|
||||
if (snapshot == null)
|
||||
errors.Add("Snapshot is null.");
|
||||
else
|
||||
{
|
||||
Debug.LogError("[SaveRestoreOrchestrator] snapshot 为 null,无法读档。");
|
||||
result.Success = false;
|
||||
result.FailedPhase = "Preflight";
|
||||
result.Errors = new[] { "snapshot is null" };
|
||||
yield break;
|
||||
if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
|
||||
errors.Add($"Unsupported schema {snapshot.schemaVersion}; expected {SaveSnapshotSchema.CurrentVersion}.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.scene?.sceneName))
|
||||
errors.Add("Snapshot scene is missing.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.anchor?.sceneSoName))
|
||||
errors.Add("Snapshot TalkSceneSO is missing.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.anchor?.yarnProjectId))
|
||||
errors.Add("Snapshot YarnProject is missing.");
|
||||
}
|
||||
|
||||
using (SuppressAutoSaveScope($"RestoreSnapshot:{sourceLabel}"))
|
||||
TalkSceneSO targetScene = null;
|
||||
if (errors.Count == 0
|
||||
&& !talkSceneIndex.TryFindByName(snapshot.anchor.sceneSoName, out targetScene, out var lookupError))
|
||||
{
|
||||
IsRestoring = true;
|
||||
ResetRestoreLog(sourceLabel);
|
||||
var context = new SnapshotRestoreContext(
|
||||
snapshot,
|
||||
strictMode: options.StrictValidation,
|
||||
logStep: AddRestoreLog);
|
||||
|
||||
try
|
||||
{
|
||||
context.SetPhase("Preflight");
|
||||
ValidateSnapshotForRestore(snapshot, context);
|
||||
if (context.StrictMode && context.HasErrors)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (options.CleanSessionFirst && GameManager.Instance != null)
|
||||
{
|
||||
context.SetPhase("Clean game session");
|
||||
yield return GameManager.Instance.ResetGameSessionRoutine();
|
||||
}
|
||||
|
||||
PrepareGameSessionForRestore();
|
||||
|
||||
yield return FadeInForRestore();
|
||||
|
||||
SnapshotRegistry.EnsureInitialized();
|
||||
yield return SnapshotRestore.RestoreState(YarnVariableStorage.Instance, snapshot, context);
|
||||
if (!context.StrictMode || !context.HasErrors)
|
||||
{
|
||||
yield return SnapshotRestore.RestoreAnchor(snapshot, context);
|
||||
}
|
||||
|
||||
if (!context.StrictMode || !context.HasErrors)
|
||||
{
|
||||
context.SetPhase("Phase 3.5: settle one frame");
|
||||
yield return null;
|
||||
ValidateRestoredRuntime(snapshot, context);
|
||||
}
|
||||
|
||||
if (!context.StrictMode || !context.HasErrors)
|
||||
{
|
||||
FinalizeGameSessionAfterRestore();
|
||||
}
|
||||
|
||||
yield return FadeOutForRestore();
|
||||
ScreenSnapshotHelper.ApplyDeferredFadeScreenIfNeeded();
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRestoring = false;
|
||||
AddRestoreLog("Restore finished");
|
||||
result.Success = !context.HasErrors;
|
||||
result.FailedPhase = context.HasErrors ? context.CurrentPhase : null;
|
||||
result.Errors = new List<string>(context.Errors);
|
||||
result.Warnings = new List<string>(context.Warnings);
|
||||
result.Log = new List<string>(_lastRestoreLog);
|
||||
}
|
||||
errors.Add(lookupError);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateSnapshotForRestore(SaveSnapshot snapshot, SnapshotRestoreContext context)
|
||||
{
|
||||
if (snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
|
||||
context.Error($"Unsupported schema {snapshot.schemaVersion}; expected {SaveSnapshotSchema.CurrentVersion}.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.scene?.sceneName))
|
||||
context.Error("Snapshot scene is missing.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.anchor?.sceneSoName))
|
||||
context.Error("Snapshot TalkSceneSO is missing.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.anchor?.yarnProjectId))
|
||||
context.Error("Snapshot YarnProject is missing.");
|
||||
if (string.IsNullOrWhiteSpace(snapshot.anchor?.nodeName))
|
||||
context.Error("Snapshot Yarn node is missing.");
|
||||
|
||||
if (context.HasErrors) return;
|
||||
var sceneSo = GameManager.Instance?.FindSceneSoByName(snapshot.anchor.sceneSoName);
|
||||
if (sceneSo == null)
|
||||
if (errors.Count == 0
|
||||
&& (targetScene.yarnProject == null
|
||||
|| !string.Equals(
|
||||
targetScene.yarnProject.name,
|
||||
snapshot.anchor.yarnProjectId,
|
||||
StringComparison.Ordinal)))
|
||||
{
|
||||
context.Error($"TalkSceneSO does not exist: {snapshot.anchor.sceneSoName}.");
|
||||
return;
|
||||
errors.Add($"TalkSceneSO YarnProject mismatch: {snapshot.anchor.yarnProjectId}.");
|
||||
}
|
||||
if (sceneSo.yarnProject == null
|
||||
|| !string.Equals(sceneSo.yarnProject.name, snapshot.anchor.yarnProjectId, StringComparison.Ordinal))
|
||||
{
|
||||
context.Error($"TalkSceneSO YarnProject mismatch: {snapshot.anchor.yarnProjectId}.");
|
||||
return;
|
||||
}
|
||||
if (!Array.Exists(sceneSo.yarnProject.NodeNames,
|
||||
|
||||
if (errors.Count == 0
|
||||
&& !string.IsNullOrWhiteSpace(snapshot.anchor.nodeName)
|
||||
&& !Array.Exists(
|
||||
targetScene.yarnProject.NodeNames,
|
||||
node => string.Equals(node, snapshot.anchor.nodeName, StringComparison.Ordinal)))
|
||||
{
|
||||
context.Error($"Yarn node does not exist: {snapshot.anchor.nodeName}.");
|
||||
errors.Add($"Yarn node does not exist: {snapshot.anchor.nodeName}.");
|
||||
}
|
||||
|
||||
if (errors.Count > 0)
|
||||
{
|
||||
preparation.Result = new RestoreResult
|
||||
{
|
||||
Success = false,
|
||||
FailedPhase = "Preflight",
|
||||
Errors = errors
|
||||
};
|
||||
return preparation;
|
||||
}
|
||||
|
||||
preparation.TargetTalkScene = targetScene;
|
||||
preparation.Result.Success = true;
|
||||
preparation.Result.RestoredTalkScene = targetScene;
|
||||
return preparation;
|
||||
}
|
||||
|
||||
private static void ValidateRestoredRuntime(SaveSnapshot snapshot, SnapshotRestoreContext context)
|
||||
private static RestorePreparation FailedPreparation(
|
||||
string sourceLabel,
|
||||
string phase,
|
||||
string error,
|
||||
RestoreOptions options)
|
||||
{
|
||||
context.SetPhase("Postflight validation");
|
||||
return new RestorePreparation
|
||||
{
|
||||
SourceLabel = sourceLabel,
|
||||
Options = options ?? RestoreOptions.Default,
|
||||
Result = RestoreResult.CreateFailure(phase, error)
|
||||
};
|
||||
}
|
||||
|
||||
private static bool ShouldCancel(
|
||||
Func<bool> isCancellationRequested,
|
||||
RestoreResult result,
|
||||
SnapshotRestoreContext context)
|
||||
{
|
||||
if (isCancellationRequested?.Invoke() != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result.Cancelled = true;
|
||||
context.Log("Restore cancelled between phases.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ValidateRestoredRuntime(
|
||||
RestorePreparation preparation,
|
||||
SnapshotRestoreContext context)
|
||||
{
|
||||
var snapshot = preparation.Snapshot;
|
||||
var sceneLoader = SceneLoader.Instance;
|
||||
if (sceneLoader == null || sceneLoader.IsLoading)
|
||||
context.Error("SceneLoader is not ready.");
|
||||
else if (!string.Equals(sceneLoader.CurrentSceneName, snapshot.scene.sceneName, StringComparison.Ordinal))
|
||||
context.Error($"Scene mismatch: {sceneLoader.CurrentSceneName ?? "none"}.");
|
||||
|
||||
var sceneSo = GameManager.Instance?.GetCurrentTalkSceneSo();
|
||||
if (!string.Equals(sceneSo?.name, snapshot.anchor.sceneSoName, StringComparison.Ordinal))
|
||||
context.Error($"TalkSceneSO mismatch: {sceneSo?.name ?? "none"}.");
|
||||
var targetScene = preparation.TargetTalkScene;
|
||||
if (!string.Equals(targetScene?.name, snapshot.anchor.sceneSoName, StringComparison.Ordinal))
|
||||
context.Error($"TalkSceneSO mismatch: {targetScene?.name ?? "none"}.");
|
||||
|
||||
var runner = DialogController.Instance?.DialogueRunner;
|
||||
if (!string.Equals(runner?.YarnProject?.name, snapshot.anchor.yarnProjectId, StringComparison.Ordinal))
|
||||
@@ -417,78 +416,6 @@ namespace AibisDream.SaveSystem
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读档前对齐「已进入游戏」的 UI 与输入状态。
|
||||
/// 编辑器测试工具等路径可能跳过 <see cref="GameLoopEnum.GameStart"/>,导致 TerminalPanel 未关闭、
|
||||
/// <see cref="GameManager.state.isInGame"/> 为 false,从而无法推进对话。
|
||||
/// </summary>
|
||||
private static void PrepareGameSessionForRestore()
|
||||
{
|
||||
AddRestoreLog("Prepare game session for restore");
|
||||
|
||||
// 读档淡入使用 DOTween(受 timeScale 影响);若处于暂停态须先恢复,否则 FadeIn 永不结束、画面卡在诊室/终端。
|
||||
Time.timeScale = 1f;
|
||||
|
||||
var gameManager = GameManager.Instance;
|
||||
if (gameManager != null && !gameManager.state.isInGame)
|
||||
{
|
||||
EnumEventSystem.Global.Send(GameLoopEnum.GameStart);
|
||||
}
|
||||
|
||||
var ui = UIManager.Instance;
|
||||
if (ui != null)
|
||||
{
|
||||
ui.HidePanel<TerminalPanel>();
|
||||
ui.HidePanel<InGameTerminalPanel>();
|
||||
ui.ShowPanel<MainPanel>();
|
||||
ui.HidePanel<SavesPanel>();
|
||||
ui.HidePanel<EndPanel>();
|
||||
}
|
||||
|
||||
if (gameManager != null && gameManager.state.isInPause)
|
||||
{
|
||||
gameManager.ContinueGame();
|
||||
}
|
||||
else
|
||||
{
|
||||
DialogUIManager.Instance?.OpenCanvas();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>读档完成后确保对话 Canvas 可用、退出暂停态。</summary>
|
||||
private static void FinalizeGameSessionAfterRestore()
|
||||
{
|
||||
AddRestoreLog("Finalize game session after restore");
|
||||
|
||||
DialogUIManager.Instance?.OpenCanvas();
|
||||
|
||||
var gameManager = GameManager.Instance;
|
||||
if (gameManager != null && gameManager.state.isInPause)
|
||||
{
|
||||
gameManager.ContinueGame();
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator FadeInForRestore()
|
||||
{
|
||||
AddRestoreLog("Begin restore fade in");
|
||||
var panel = UIManager.Instance?.GetPanel<ScreenTransitionPanel>();
|
||||
if (panel != null)
|
||||
{
|
||||
yield return panel.FadeInAsync(RestoreFadeDuration, useUnscaledTime: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static IEnumerator FadeOutForRestore()
|
||||
{
|
||||
AddRestoreLog("End restore fade out");
|
||||
var panel = UIManager.Instance?.GetPanel<ScreenTransitionPanel>();
|
||||
if (panel != null)
|
||||
{
|
||||
yield return panel.FadeOutAsync(RestoreFadeDuration, useUnscaledTime: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ResetRestoreLog(string sourceLabel)
|
||||
{
|
||||
_lastRestoreLog.Clear();
|
||||
|
||||
@@ -57,7 +57,7 @@ namespace AibisDream.SaveSystem
|
||||
[Serializable]
|
||||
public class AnchorSnapshot
|
||||
{
|
||||
/// <summary>当前章节 TalkSceneSO 名称,用于 <see cref="GameManager.SetSceneSoByName"/>。</summary>
|
||||
/// <summary>当前章节 TalkSceneSO 名称,由读档预检解析为目标章节。</summary>
|
||||
public string sceneSoName;
|
||||
public string yarnProjectId;
|
||||
public string nodeName;
|
||||
|
||||
@@ -88,8 +88,9 @@ namespace AibisDream.SaveSystem
|
||||
var yarnProject = dialog.DialogueRunner?.YarnProject;
|
||||
var projectId = yarnProject != null ? yarnProject.name : string.Empty;
|
||||
|
||||
var gameManager = GameManager.Instance;
|
||||
var sceneSoName = gameManager != null ? gameManager.GetSceneSoName() : null;
|
||||
var sceneSoName = GameManager.Instance != null
|
||||
? GameManager.Session.CurrentTalkScene?.name
|
||||
: null;
|
||||
|
||||
snapshot.anchor = new AnchorSnapshot
|
||||
{
|
||||
|
||||
@@ -1,16 +1,15 @@
|
||||
using System.IO;
|
||||
using AibisDream.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 快照与磁盘的边界:序列化、读写 JSON 文件、检测旧档格式。
|
||||
/// 快照与磁盘的边界:序列化和读写 JSON 文件。
|
||||
/// <para>
|
||||
/// P2 起,业务存档统一通过 <see cref="SlotManager"/> 写入槽位目录;
|
||||
/// 本类保留按路径读写的低级接口,供旧档兼容、迁移和外部调试使用。
|
||||
/// 本类保留按路径读写的低级接口,供迁移和外部调试使用。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SnapshotPersistence
|
||||
@@ -80,21 +79,5 @@ namespace AibisDream.SaveSystem
|
||||
return Deserialize(json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 是否为旧 StorageSystem 格式(含 systemData、无 schemaVersion)。
|
||||
/// 此类文件应走 <see cref="SaveRestoreOrchestrator"/> 的 legacy 分支。
|
||||
/// </summary>
|
||||
public static bool IsLegacyFormat(string savePath)
|
||||
{
|
||||
var json = File.ReadAllText(JsonUtil.FormatAsJsonPath(savePath));
|
||||
var jobj = JObject.Parse(json);
|
||||
return jobj.Value<int?>("schemaVersion") == null && jobj["systemData"] != null;
|
||||
}
|
||||
|
||||
/// <summary>读取旧格式存档根 JObject,供 legacy 还原使用。</summary>
|
||||
public static JObject ReadLegacySaveRoot(string savePath)
|
||||
{
|
||||
return JsonUtil.ReadJObject(savePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,8 @@ namespace AibisDream.SaveSystem
|
||||
public static IEnumerator RestoreState(
|
||||
YarnVariableStorage storage,
|
||||
SaveSnapshot snapshot,
|
||||
SnapshotRestoreContext context)
|
||||
SnapshotRestoreContext context,
|
||||
System.Func<bool> isCancellationRequested = null)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
@@ -27,17 +28,15 @@ namespace AibisDream.SaveSystem
|
||||
|
||||
context.SetPhase("Phase 0: Restore Yarn variables");
|
||||
RestoreYarnVariables(storage, snapshot);
|
||||
if (IsCancellationRequested(isCancellationRequested, context)) yield break;
|
||||
|
||||
context.SetPhase("Phase 1: Load scene");
|
||||
yield return RestoreScene(snapshot, context);
|
||||
if (context.StrictMode && context.HasErrors) yield break;
|
||||
|
||||
context.SetPhase("Phase 1.5: Restore scene SO");
|
||||
RestoreSceneSo(snapshot, context);
|
||||
if (IsCancellationRequested(isCancellationRequested, context)) yield break;
|
||||
if (context.StrictMode && context.HasErrors) yield break;
|
||||
|
||||
context.SetPhase("Phase 2: Restore providers (ordered by RestoreOrder)");
|
||||
yield return RestoreProvidersInOrder(snapshot, context);
|
||||
yield return RestoreProvidersInOrder(snapshot, context, isCancellationRequested);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -74,7 +73,7 @@ namespace AibisDream.SaveSystem
|
||||
|
||||
if (!string.IsNullOrEmpty(snapshot.anchor.yarnProjectId))
|
||||
{
|
||||
var sceneSo = GameManager.Instance?.GetCurrentTalkSceneSo();
|
||||
var sceneSo = context.TargetTalkScene;
|
||||
if (sceneSo?.yarnProject == null)
|
||||
{
|
||||
context.Error($"章节 {snapshot.anchor.sceneSoName} 没有可用的 YarnProject。");
|
||||
@@ -138,41 +137,37 @@ namespace AibisDream.SaveSystem
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return sceneLoader.LoadSceneAsync(snapshot.scene.sceneName);
|
||||
SceneOperationResult result = null;
|
||||
yield return sceneLoader.LoadSceneAsync(
|
||||
snapshot.scene.sceneName,
|
||||
value => result = value);
|
||||
|
||||
if (result?.Success != true)
|
||||
{
|
||||
context.Error(result?.Error ?? "Scene load did not return a result.");
|
||||
yield break;
|
||||
}
|
||||
|
||||
// 再等一帧,等Awake执行完
|
||||
yield return null;
|
||||
}
|
||||
|
||||
private static void RestoreSceneSo(SaveSnapshot snapshot, SnapshotRestoreContext context)
|
||||
{
|
||||
if (snapshot.anchor == null || string.IsNullOrEmpty(snapshot.anchor.sceneSoName))
|
||||
{
|
||||
context.Warn("快照缺少 anchor.sceneSoName,跳过章节 SO 设置。");
|
||||
return;
|
||||
}
|
||||
|
||||
var gameManager = GameManager.Instance;
|
||||
if (gameManager == null)
|
||||
{
|
||||
context.Warn("GameManager 未初始化,无法设置章节 SO。");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!gameManager.SetSceneSoByName(snapshot.anchor.sceneSoName))
|
||||
{
|
||||
context.Error($"找不到章节 SO:{snapshot.anchor.sceneSoName},无法恢复 YarnProject。");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 按 <see cref="ISnapshotProvider.RestoreOrder"/> 依次还原;
|
||||
/// 同步 Provider 连续调用,异步 Provider 作为 Barrier 挂起,二者混排而非分两批。
|
||||
/// </summary>
|
||||
private static IEnumerator RestoreProvidersInOrder(SaveSnapshot snapshot, SnapshotRestoreContext context)
|
||||
private static IEnumerator RestoreProvidersInOrder(
|
||||
SaveSnapshot snapshot,
|
||||
SnapshotRestoreContext context,
|
||||
System.Func<bool> isCancellationRequested)
|
||||
{
|
||||
foreach (var provider in SnapshotRegistry.GetOrderedProviders())
|
||||
{
|
||||
if (IsCancellationRequested(isCancellationRequested, context))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (!TryGetSectionDto(snapshot, provider, context, out var dto))
|
||||
{
|
||||
continue;
|
||||
@@ -199,9 +194,27 @@ namespace AibisDream.SaveSystem
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (IsCancellationRequested(isCancellationRequested, context))
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsCancellationRequested(
|
||||
System.Func<bool> isCancellationRequested,
|
||||
SnapshotRestoreContext context)
|
||||
{
|
||||
if (isCancellationRequested?.Invoke() != true)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Log("Restore cancellation requested between phases.");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryGetSectionDto(
|
||||
SaveSnapshot snapshot,
|
||||
ISnapshotProvider provider,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
using System.Collections;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
/// 快照层对外 API:Capture / Restore,不涉及文件与 UI。
|
||||
/// 快照层捕获 API。恢复必须通过 GameManager 会话命令进入。
|
||||
/// 游戏逻辑应优先使用 <see cref="SaveRestoreOrchestrator"/> 完成存读档流程。
|
||||
/// </summary>
|
||||
public static class SnapshotService
|
||||
@@ -17,25 +15,5 @@ namespace AibisDream.SaveSystem
|
||||
return SnapshotCapture.Capture(YarnVariableStorage.Instance, triggerNodeName, omitAnchor);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 还原快照状态;默认继续执行 <see cref="SnapshotRestore.RestoreAnchor"/>。
|
||||
/// 业务读档流程应优先使用 <see cref="SaveRestoreOrchestrator"/>,以获得黑屏与自动存档抑制。
|
||||
/// </summary>
|
||||
public static IEnumerator Restore(SaveSnapshot snapshot, bool restoreAnchor = true)
|
||||
{
|
||||
if (snapshot == null)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
SnapshotRegistry.EnsureInitialized();
|
||||
var context = new SnapshotRestoreContext(snapshot);
|
||||
yield return SnapshotRestore.RestoreState(YarnVariableStorage.Instance, snapshot, context);
|
||||
|
||||
if (restoreAnchor)
|
||||
{
|
||||
yield return SnapshotRestore.RestoreAnchor(snapshot, context);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user