Files

279 lines
10 KiB
C#
Raw Permalink 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.Collections;
using Newtonsoft.Json.Linq;
using UnityEngine;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 将 <see cref="SaveSnapshot"/> 按 Phase + Barrier 写回运行时(纯内存,不读盘)。
/// </summary>
public static class SnapshotRestore
{
/// <summary>
/// Phase 0~2Yarn 变量、场景、章节与所有 Provider;不包含锚点重进。
/// </summary>
public static IEnumerator RestoreState(
YarnVariableStorage storage,
SaveSnapshot snapshot,
SnapshotRestoreContext context,
System.Func<bool> isCancellationRequested = null)
{
if (snapshot == null)
{
context?.Error("snapshot 为 null");
yield break;
}
context ??= new SnapshotRestoreContext(snapshot);
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 (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, isCancellationRequested);
}
/// <summary>
/// Phase 3:按 <see cref="AnchorSnapshot"/> 加载对话工程,并按显式恢复标记决定是否启动节点。
/// </summary>
public static IEnumerator RestoreAnchor(SaveSnapshot snapshot, SnapshotRestoreContext context = null)
{
if (snapshot?.anchor == null)
{
context?.Log("Phase 3: No anchor, skip RestoreAnchor");
yield break;
}
context ??= new SnapshotRestoreContext(snapshot);
var shouldStartDialogue = snapshot.anchor.startDialogueOnRestore;
context.SetPhase(shouldStartDialogue
? $"Phase 3: Restore anchor {snapshot.anchor.nodeName}"
: $"Phase 3: Restore YarnProject only (source {snapshot.anchor.nodeName})");
var dialog = DialogController.Instance;
if (dialog == null)
{
context.Warn("DialogController 未初始化,无法恢复 Yarn 状态。");
yield break;
}
var runner = dialog.DialogueRunner;
if (runner == null)
{
context.Warn("DialogueRunner 未初始化,无法恢复 Yarn 状态。");
yield break;
}
if (!string.IsNullOrEmpty(snapshot.anchor.yarnProjectId))
{
var sceneSo = context.TargetTalkScene;
if (sceneSo?.yarnProject == null)
{
context.Error($"章节 {snapshot.anchor.sceneSoName} 没有可用的 YarnProject。");
yield break;
}
if (sceneSo.yarnProject.name != snapshot.anchor.yarnProjectId)
{
context.Error(
$"章节 YarnProject 不匹配:章节 {sceneSo.yarnProject.name},存档 {snapshot.anchor.yarnProjectId}");
yield break;
}
if (runner.YarnProject == null || runner.YarnProject.name != snapshot.anchor.yarnProjectId)
{
dialog.LoadDialog(sceneSo.yarnProject);
}
}
if (runner.IsDialogueRunning)
{
yield return runner.Stop();
}
var nodeExists = runner.YarnProject != null
&& System.Array.Exists(
runner.YarnProject.NodeNames,
node => node == snapshot.anchor.nodeName);
if (!shouldStartDialogue)
{
if (!nodeExists)
{
context.Warn(
$"StateOnly 来源节点 {snapshot.anchor.nodeName} 已不在 " +
$"YarnProject {runner.YarnProject?.name ?? "none"} 中;状态恢复继续。");
}
yield break;
}
if (!nodeExists)
{
context.Error(
$"YarnProject {runner.YarnProject?.name ?? "none"} 不包含节点 {snapshot.anchor.nodeName}。");
yield break;
}
yield return runner.StartDialogue(snapshot.anchor.nodeName);
}
private static void RestoreYarnVariables(YarnVariableStorage storage, SaveSnapshot snapshot)
{
var vars = snapshot.yarnVariables;
if (vars == null || storage == null) return;
storage.SetAllVariables(vars.floats, vars.strings, vars.bools);
}
private static IEnumerator RestoreScene(SaveSnapshot snapshot, SnapshotRestoreContext context)
{
if (snapshot.scene == null || string.IsNullOrEmpty(snapshot.scene.sceneName))
{
context.Warn("快照缺少 scene.sceneName,跳过场景加载。");
yield break;
}
var sceneLoader = SceneLoader.Instance;
if (sceneLoader == null)
{
context.Error("SceneLoader 未初始化,无法加载场景。");
yield break;
}
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;
}
/// <summary>
/// 按 <see cref="ISnapshotProvider.RestoreOrder"/> 依次还原;
/// 同步 Provider 连续调用,异步 Provider 作为 Barrier 挂起,二者混排而非分两批。
/// </summary>
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;
}
switch (provider)
{
case ISyncSnapshotProvider syncProvider:
context.Log($"Sync provider [{provider.RestoreOrder}]: {provider.SaveId}");
syncProvider.Restore(dto, context);
break;
case IAsyncSnapshotProvider asyncProvider:
context.Log($"Async provider [{provider.RestoreOrder}]: {provider.SaveId}");
yield return asyncProvider.RestoreAsync(dto, context);
break;
default:
context.Warn($"Provider {provider.SaveId} 未实现同步或异步还原契约,已跳过。");
break;
}
if (context.StrictMode && context.HasErrors)
{
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,
SnapshotRestoreContext context,
out object dto)
{
dto = null;
if (snapshot.sections == null
|| !snapshot.sections.TryGetValue(provider.SaveId, out dto)
|| dto == null)
{
context.Log($"Provider {provider.SaveId}: no section, skip");
return false;
}
dto = CoerceSectionDto(provider.SaveId, dto);
return true;
}
/// <summary>
/// 读盘后 <see cref="SaveSnapshot.sections"/> 中的值常为 <see cref="JObject"/>
/// 按 SaveId 转回各 Provider 所需的强类型 DTO。
/// </summary>
private static object CoerceSectionDto(string saveId, object dto)
{
if (dto is not JObject jobj)
{
return dto;
}
return saveId switch
{
SnapshotProviderIds.Environment => jobj.ToObject<EnvironmentSnapshotDto>(),
SnapshotProviderIds.Actor => jobj.ToObject<ActorSnapshotDto>(),
SnapshotProviderIds.Audio => jobj.ToObject<AudioSnapshotDto>(),
SnapshotProviderIds.Timeline => jobj.ToObject<TimelineSnapshotDto>(),
SnapshotProviderIds.Subway => jobj.ToObject<SubwaySnapshotDto>(),
SnapshotProviderIds.PunchTape => jobj.ToObject<PunchTapeSnapshotDto>(),
SnapshotProviderIds.Fix => jobj.ToObject<FixSnapshotDto>(),
SnapshotProviderIds.FixPanel => jobj.ToObject<FixPanelSnapshotDto>(),
SnapshotProviderIds.BodyModule => jobj.ToObject<BodyModuleSnapshotDto>(),
SnapshotProviderIds.Eye => jobj.ToObject<EyeSnapshotDto>(),
SnapshotProviderIds.Showcase => jobj.ToObject<ShowcaseSnapshotDto>(),
SnapshotProviderIds.Day2SleepPresentation => jobj.ToObject<Day2SleepPresentationSnapshotDto>(),
SnapshotProviderIds.PlayTool => jobj.ToObject<PlayToolSnapshotDto>(),
SnapshotProviderIds.Screen => jobj.ToObject<ScreenSnapshotDto>(),
_ => jobj
};
}
}
}