feat(save): 实现 Yarn tag 自动存档判定与异步写盘
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -162,7 +162,13 @@ namespace AibisDream
|
||||
private void OnNodeStart(string nodeName)
|
||||
{
|
||||
UpdateCurrentNodeContext(nodeName);
|
||||
SaveRestoreOrchestrator.TryAutoSave();
|
||||
|
||||
if (!SavePointEvaluator.CanAutoSave(out _))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StartCoroutine(SaveRestoreOrchestrator.AutoSaveRoutine(nodeName));
|
||||
}
|
||||
|
||||
private void OnNodeComplete(string nodeName)
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Linq;
|
||||
using AibisDream.Framework;
|
||||
using AibisDream.SaveSystem;
|
||||
using UnityEngine;
|
||||
using Yarn;
|
||||
using Yarn.Unity;
|
||||
|
||||
namespace AibisDream
|
||||
@@ -94,17 +95,36 @@ namespace AibisDream
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_variableDict.TryGetValue(variableName, out var value))
|
||||
switch (GetVariableKind(variableName))
|
||||
{
|
||||
if (value is T target)
|
||||
{
|
||||
result = target;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
case VariableKind.Stored:
|
||||
if (_variableDict.TryGetValue(variableName, out var value) && value is T target)
|
||||
{
|
||||
result = target;
|
||||
return true;
|
||||
}
|
||||
|
||||
result = default;
|
||||
return false;
|
||||
if (Program == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Can't get initial value for variable {variableName}, because {nameof(Program)} is not set");
|
||||
}
|
||||
|
||||
return Program.TryGetInitialValue(variableName, out result);
|
||||
|
||||
case VariableKind.Smart:
|
||||
if (SmartVariableEvaluator == null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Can't get value for smart variable {variableName}, because {nameof(SmartVariableEvaluator)} is not set");
|
||||
}
|
||||
|
||||
return SmartVariableEvaluator.TryGetSmartVariable(variableName, out result);
|
||||
|
||||
default:
|
||||
result = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
|
||||
namespace AibisDream.SaveSystem
|
||||
{
|
||||
/// <summary>
|
||||
@@ -8,21 +12,54 @@ namespace AibisDream.SaveSystem
|
||||
None,
|
||||
Restoring,
|
||||
GamePaused,
|
||||
|
||||
// 遗留值:早期逻辑曾返回,当前实现中不再使用,保留以避免外部代码编译失败。
|
||||
NoActiveNode,
|
||||
StartNode,
|
||||
FunctionNode,
|
||||
NoAutoSave
|
||||
NoAutoSave,
|
||||
|
||||
/// <summary>节点 tag 在默认禁止保存集合中。</summary>
|
||||
DeniedByTag,
|
||||
|
||||
/// <summary>节点显式附加了 no_save 标记。</summary>
|
||||
MarkedNoSave,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 可存点判定器。
|
||||
/// <para>
|
||||
/// P3 只搭框架:规则不是最终版,后续随 Yarn 节点 tags 整理和章节策略扩展而更新。
|
||||
/// 当前所有 FixState 均放行,不可存状态由 Yarn 节点流程控制。
|
||||
/// 基于 Yarn 节点 tag 判定是否允许自动存档:
|
||||
/// - 白名单 tag(hub / linear / content)默认允许;
|
||||
/// - 黑名单 tag(start / init / function / detour / center / performance / event / end)默认禁止;
|
||||
/// - no_save 附加标记覆盖默认语义,一律禁止;
|
||||
/// - 无活跃节点、或节点未声明 tag,默认允许并打 warning;
|
||||
/// - 未知 tag 默认允许并打 warning。
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public static class SavePointEvaluator
|
||||
{
|
||||
/// <summary>默认可作为自动存档边界的节点 tag。</summary>
|
||||
private static readonly HashSet<string> AutoSaveAllowedTags = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"hub", // 梦境:场景导航中枢
|
||||
"linear", // 梦境:线性剧情推进
|
||||
"content", // 维修:阶段内容节点
|
||||
};
|
||||
|
||||
/// <summary>默认禁止作为自动存档边界的节点 tag。</summary>
|
||||
private static readonly HashSet<string> AutoSaveDeniedTags = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
"start", // 引擎入口
|
||||
"init", // 变量声明/初始化
|
||||
"function", // 纯指令函数
|
||||
"detour", // 梦境内容片段
|
||||
"center", // 维修阶段调度中枢
|
||||
"performance", // 维修原子化演出
|
||||
"event", // 维修 C# 事件响应入口
|
||||
"end", // 维修结束/收尾
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// 当前是否可以自动存档。
|
||||
/// </summary>
|
||||
@@ -44,24 +81,55 @@ namespace AibisDream.SaveSystem
|
||||
? DialogController.Instance.GetCurrentNodeContext()
|
||||
: (null, null);
|
||||
|
||||
if (string.IsNullOrEmpty(nodeName))
|
||||
// no_save 标记覆盖一切:即使节点类型默认可存,也不允许保存。
|
||||
if (tags != null && IsTagPresent(tags, "no_save"))
|
||||
{
|
||||
reason = SavePointRejectReason.NoActiveNode;
|
||||
reason = SavePointRejectReason.MarkedNoSave;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (nodeName == "Start")
|
||||
// 无活跃节点,或节点未声明任何 tag:允许保存。
|
||||
// 注:FixSystemNew 等交互状态可能没有活跃 Yarn 节点,仍需保存当前状态。
|
||||
if (string.IsNullOrEmpty(nodeName) || tags == null || tags.Count == 0)
|
||||
{
|
||||
reason = SavePointRejectReason.StartNode;
|
||||
if (!string.IsNullOrEmpty(nodeName) && (tags == null || tags.Count == 0))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[SavePointEvaluator] 节点 {nodeName} 未声明任何 tag,默认允许自动存档。请补全节点类型标签。");
|
||||
}
|
||||
|
||||
reason = SavePointRejectReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 按主 tag 判定。
|
||||
var primaryTag = FindPrimaryTag(tags);
|
||||
|
||||
// 未识别到已知主 tag:放行但警告,方便脚本作者补标。
|
||||
if (primaryTag == null)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[SavePointEvaluator] 节点 {nodeName} 的 tags [{string.Join(", ", tags)}] 未声明保存语义,默认允许自动存档。");
|
||||
reason = SavePointRejectReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (AutoSaveDeniedTags.Contains(primaryTag))
|
||||
{
|
||||
reason = SavePointRejectReason.DeniedByTag;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (tags != null && IsTagPresent(tags, "function"))
|
||||
if (AutoSaveAllowedTags.Contains(primaryTag))
|
||||
{
|
||||
reason = SavePointRejectReason.FunctionNode;
|
||||
return false;
|
||||
reason = SavePointRejectReason.None;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 理论上不会到达:已识别主 tag 但既不在允许集合也不在禁止集合。
|
||||
// 保持放行并警告,避免未来新增 tag 时意外阻断存档。
|
||||
Debug.LogWarning(
|
||||
$"[SavePointEvaluator] 节点 {nodeName} 的 tag '{primaryTag}' 未声明保存语义,默认允许自动存档。");
|
||||
reason = SavePointRejectReason.None;
|
||||
return true;
|
||||
}
|
||||
@@ -87,7 +155,7 @@ namespace AibisDream.SaveSystem
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsTagPresent(System.Collections.Generic.IReadOnlyList<string> tags, string tag)
|
||||
private static bool IsTagPresent(IReadOnlyList<string> tags, string tag)
|
||||
{
|
||||
if (tags == null) return false;
|
||||
|
||||
@@ -101,5 +169,21 @@ namespace AibisDream.SaveSystem
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 从 tags 中找到第一个已知的保存语义主 tag;若均未知则返回 null。
|
||||
/// </summary>
|
||||
private static string FindPrimaryTag(IReadOnlyList<string> tags)
|
||||
{
|
||||
foreach (var tag in tags)
|
||||
{
|
||||
if (AutoSaveAllowedTags.Contains(tag) || AutoSaveDeniedTags.Contains(tag))
|
||||
{
|
||||
return tag;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
using System.Collections;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using AibisDream.Framework;
|
||||
using AibisDream.Kit;
|
||||
using AibisDream.UI;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using UnityEngine;
|
||||
@@ -11,7 +10,8 @@ namespace AibisDream.SaveSystem
|
||||
/// <summary>
|
||||
/// 存读档流程编排层:连接 UI、<see cref="SnapshotService"/> 与槽位系统。
|
||||
/// <para>
|
||||
/// 对外入口:<see cref="TryAutoSave"/>(节点进入事件触发)、
|
||||
/// 对外入口:<see cref="TryAutoSave"/>(手动/调试触发)、
|
||||
/// <see cref="AutoSaveRoutine"/>(节点进入事件触发)、
|
||||
/// <see cref="CreateManualSlot"/>(手动存档)、
|
||||
/// <see cref="RestoreFromSlot"/>(读档)、
|
||||
/// <see cref="RestoreFromFile"/>(旧档/调试)。
|
||||
@@ -22,11 +22,12 @@ namespace AibisDream.SaveSystem
|
||||
/// <summary>是否正在读档还原中;为 true 时禁止自动存档覆盖 slot_0。</summary>
|
||||
public static bool IsRestoring { get; private set; }
|
||||
|
||||
/// <summary>捕获快照并写入自动档(slot 0);含保存 Loading UI。</summary>
|
||||
private static bool _isAutoSaving;
|
||||
|
||||
/// <summary>启动自动存档协程(手动/调试入口)。</summary>
|
||||
public static void TryAutoSave()
|
||||
{
|
||||
var host = YarnVariableStorage.Instance;
|
||||
if (host == null)
|
||||
if (YarnVariableStorage.Instance == null)
|
||||
{
|
||||
Debug.LogError("[SaveRestoreOrchestrator] YarnVariableStorage 未初始化");
|
||||
return;
|
||||
@@ -38,17 +39,64 @@ namespace AibisDream.SaveSystem
|
||||
return;
|
||||
}
|
||||
|
||||
ActionKit.Sequence()
|
||||
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().ShowSaveLoading())
|
||||
.Delay(1f)
|
||||
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().HideSaveLoading())
|
||||
.Start(host);
|
||||
var (triggerNodeName, _) = DialogController.Instance.GetCurrentNodeContext();
|
||||
DialogController.Instance.StartCoroutine(AutoSaveRoutine(triggerNodeName));
|
||||
}
|
||||
|
||||
using (new CodeTimer("SaveSnapshot"))
|
||||
/// <summary>自动存档协程:settle 一帧 → 主线程 Capture → 异步写盘。</summary>
|
||||
/// <param name="triggerNodeName"><c>OnNodeStart</c> 触发存档时的节点名,作为 anchor 写入快照。</param>
|
||||
public static IEnumerator AutoSaveRoutine(string triggerNodeName = null)
|
||||
{
|
||||
if (_isAutoSaving)
|
||||
{
|
||||
var snapshot = SnapshotService.Capture();
|
||||
var thumbnail = SlotThumbnailCapture.CapturePng();
|
||||
SlotManager.SaveToAutoSlot(snapshot, thumbnail);
|
||||
yield break;
|
||||
}
|
||||
|
||||
yield return null;
|
||||
|
||||
// 可能在 settle 帧内已有另一条协程开始写盘,须二次检查避免并发覆盖 slot_0。
|
||||
if (_isAutoSaving)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
_isAutoSaving = true;
|
||||
var infoPanel = UIManager.Instance.GetPanel<InfoPanel>();
|
||||
infoPanel?.ShowSaveLoading();
|
||||
|
||||
try
|
||||
{
|
||||
SaveSnapshot snapshot;
|
||||
byte[] thumbnail;
|
||||
|
||||
using (new CodeTimer("SaveSnapshot"))
|
||||
{
|
||||
snapshot = SnapshotService.Capture(triggerNodeName);
|
||||
thumbnail = SlotThumbnailCapture.CapturePng();
|
||||
}
|
||||
|
||||
var writeTask = SlotManager.SaveToAutoSlotAsync(snapshot, thumbnail);
|
||||
while (!writeTask.IsCompleted)
|
||||
{
|
||||
yield return null;
|
||||
}
|
||||
|
||||
if (writeTask.IsFaulted)
|
||||
{
|
||||
Debug.LogError(
|
||||
$"[SaveRestoreOrchestrator] 自动存档失败: {writeTask.Exception?.GetBaseException()}");
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (snapshot?.anchor != null && string.IsNullOrEmpty(snapshot.anchor.nodeName))
|
||||
{
|
||||
Debug.Log("[SaveRestoreOrchestrator] 已保存无 Yarn 节点活跃状态。");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
infoPanel?.HideSaveLoading();
|
||||
_isAutoSaving = false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using AibisDream.Utility;
|
||||
using Newtonsoft.Json;
|
||||
using UnityEngine;
|
||||
@@ -109,12 +110,29 @@ namespace AibisDream.SaveSystem
|
||||
|
||||
private static void CommitTempFile(string tempPath, string finalPath)
|
||||
{
|
||||
if (File.Exists(finalPath))
|
||||
{
|
||||
File.Delete(finalPath);
|
||||
}
|
||||
const int maxAttempts = 5;
|
||||
|
||||
File.Move(tempPath, finalPath);
|
||||
for (var attempt = 0; attempt < maxAttempts; attempt++)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(finalPath))
|
||||
{
|
||||
// Unity 使用的 .NET Standard 2.0 无 File.Move(..., overwrite);Replace 为原子覆盖。
|
||||
File.Replace(tempPath, finalPath, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
File.Move(tempPath, finalPath);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
catch (IOException) when (attempt < maxAttempts - 1)
|
||||
{
|
||||
Thread.Sleep(20 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading.Tasks;
|
||||
using AibisDream.SaveSystem.CloudSave;
|
||||
using AibisDream.Utility;
|
||||
using Newtonsoft.Json;
|
||||
@@ -14,6 +15,7 @@ namespace AibisDream.SaveSystem
|
||||
public static class SlotManager
|
||||
{
|
||||
private static bool _migrated;
|
||||
private static readonly object SlotWriteLock = new object();
|
||||
|
||||
/// <summary>将快照与缩略图写入指定槽位。</summary>
|
||||
public static void SaveToSlot(int slotIndex, SaveSnapshot snapshot, byte[] thumbnailPng)
|
||||
@@ -30,6 +32,23 @@ namespace AibisDream.SaveSystem
|
||||
CloudSaveManager.OnSlotWritten(SlotIndex.Auto);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 主线程完成序列化后,在后台线程写入自动档(slot 0)。
|
||||
/// 调用方须已在主线程完成 <see cref="SnapshotService.Capture"/>。
|
||||
/// </summary>
|
||||
public static async Task SaveToAutoSlotAsync(SaveSnapshot snapshot, byte[] thumbnailPng)
|
||||
{
|
||||
EnsureMigrated();
|
||||
|
||||
var json = SnapshotPersistence.Serialize(snapshot);
|
||||
var meta = BuildSlotMeta(SlotIndex.Auto, snapshot);
|
||||
|
||||
await Task.Run(() => WriteSlotFiles(SlotIndex.Auto, json, meta, thumbnailPng));
|
||||
|
||||
SetLatestSlotIndex(SlotIndex.Auto);
|
||||
CloudSaveManager.OnSlotWritten(SlotIndex.Auto);
|
||||
}
|
||||
|
||||
/// <summary>将当前自动档复制到指定手动档。</summary>
|
||||
public static void CopyAutoToManual(int manualSlotIndex)
|
||||
{
|
||||
@@ -226,16 +245,15 @@ namespace AibisDream.SaveSystem
|
||||
|
||||
private static void WriteSlot(int slotIndex, SaveSnapshot snapshot, byte[] thumbnailPng)
|
||||
{
|
||||
SlotDirectory.EnsureSlotDirectory(slotIndex);
|
||||
|
||||
// snapshot.json
|
||||
var snapshotPath = SlotDirectory.GetSnapshotPath(slotIndex);
|
||||
var json = SnapshotPersistence.Serialize(snapshot);
|
||||
SlotAtomicWriter.WriteText(json, snapshotPath);
|
||||
var meta = BuildSlotMeta(slotIndex, snapshot);
|
||||
WriteSlotFiles(slotIndex, json, meta, thumbnailPng);
|
||||
}
|
||||
|
||||
// meta.json
|
||||
private static SlotMeta BuildSlotMeta(int slotIndex, SaveSnapshot snapshot)
|
||||
{
|
||||
var summary = SaveSnapshotSummary.From(snapshot);
|
||||
var meta = new SlotMeta
|
||||
return new SlotMeta
|
||||
{
|
||||
slotIndex = slotIndex,
|
||||
savedAt = snapshot.savedAt,
|
||||
@@ -247,12 +265,20 @@ namespace AibisDream.SaveSystem
|
||||
gameVersion = snapshot.gameVersion,
|
||||
thumbnailFile = $"{ConstRef.SaveThumbnailFileName}.png"
|
||||
};
|
||||
WriteMeta(slotIndex, meta);
|
||||
}
|
||||
|
||||
// thumbnail.png
|
||||
if (thumbnailPng != null)
|
||||
private static void WriteSlotFiles(int slotIndex, string json, SlotMeta meta, byte[] thumbnailPng)
|
||||
{
|
||||
lock (SlotWriteLock)
|
||||
{
|
||||
SlotAtomicWriter.WriteBytes(thumbnailPng, SlotDirectory.GetThumbnailPath(slotIndex));
|
||||
SlotDirectory.EnsureSlotDirectory(slotIndex);
|
||||
SlotAtomicWriter.WriteText(json, SlotDirectory.GetSnapshotPath(slotIndex));
|
||||
SlotAtomicWriter.WriteJson(meta, SlotDirectory.GetMetaPath(slotIndex));
|
||||
|
||||
if (thumbnailPng != null)
|
||||
{
|
||||
SlotAtomicWriter.WriteBytes(thumbnailPng, SlotDirectory.GetThumbnailPath(slotIndex));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,11 @@ namespace AibisDream.SaveSystem
|
||||
public static class SnapshotCapture
|
||||
{
|
||||
/// <summary>捕获当前完整快照。调用前需已注册全部 provider。</summary>
|
||||
public static SaveSnapshot Capture(YarnVariableStorage storage)
|
||||
/// <param name="triggerNodeName">
|
||||
/// 触发存档时的 Yarn 节点名(通常为 <c>OnNodeStart</c> 传入值)。
|
||||
/// 非 null 时作为 anchor,并与 Capture 时刻的当前节点比较;不一致时打 warning,不阻断写盘。
|
||||
/// </param>
|
||||
public static SaveSnapshot Capture(YarnVariableStorage storage, string triggerNodeName = null)
|
||||
{
|
||||
SnapshotRegistry.ValidateRequiredProviders();
|
||||
|
||||
@@ -25,7 +29,7 @@ namespace AibisDream.SaveSystem
|
||||
};
|
||||
|
||||
CaptureScene(snapshot);
|
||||
CaptureAnchor(snapshot);
|
||||
CaptureAnchor(snapshot, triggerNodeName);
|
||||
CaptureYarnVariables(storage, snapshot);
|
||||
|
||||
foreach (var provider in SnapshotRegistry.GetOrderedProviders())
|
||||
@@ -47,12 +51,25 @@ namespace AibisDream.SaveSystem
|
||||
};
|
||||
}
|
||||
|
||||
private static void CaptureAnchor(SaveSnapshot snapshot)
|
||||
private static void CaptureAnchor(SaveSnapshot snapshot, string triggerNodeName)
|
||||
{
|
||||
var dialog = DialogController.Instance;
|
||||
if (dialog == null) return;
|
||||
|
||||
var (nodeName, _) = dialog.GetCurrentNodeContext();
|
||||
var (currentNodeName, _) = dialog.GetCurrentNodeContext();
|
||||
var anchorNodeName = triggerNodeName ?? currentNodeName;
|
||||
|
||||
if (triggerNodeName != null
|
||||
&& !string.Equals(
|
||||
NormalizeNodeName(triggerNodeName),
|
||||
NormalizeNodeName(currentNodeName),
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[SnapshotCapture] 存档锚点漂移:触发时「{FormatNodeName(triggerNodeName)}」," +
|
||||
$"Capture 时「{FormatNodeName(currentNodeName)}」。anchor 使用触发节点。");
|
||||
}
|
||||
|
||||
var yarnProject = dialog.DialogueRunner?.YarnProject;
|
||||
var projectId = yarnProject != null ? yarnProject.name : string.Empty;
|
||||
|
||||
@@ -63,10 +80,20 @@ namespace AibisDream.SaveSystem
|
||||
{
|
||||
sceneSoName = sceneSoName,
|
||||
yarnProjectId = projectId,
|
||||
nodeName = nodeName ?? string.Empty
|
||||
nodeName = anchorNodeName ?? string.Empty
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizeNodeName(string nodeName)
|
||||
{
|
||||
return string.IsNullOrEmpty(nodeName) ? string.Empty : nodeName;
|
||||
}
|
||||
|
||||
private static string FormatNodeName(string nodeName)
|
||||
{
|
||||
return string.IsNullOrEmpty(nodeName) ? "(无节点)" : nodeName;
|
||||
}
|
||||
|
||||
private static void CaptureYarnVariables(YarnVariableStorage storage, SaveSnapshot snapshot)
|
||||
{
|
||||
var (floats, strings, bools) = storage.GetAllVariables();
|
||||
|
||||
@@ -9,10 +9,11 @@ namespace AibisDream.SaveSystem
|
||||
public static class SnapshotService
|
||||
{
|
||||
/// <summary>捕获当前运行时快照。</summary>
|
||||
public static SaveSnapshot Capture()
|
||||
/// <param name="triggerNodeName">见 <see cref="SnapshotCapture.Capture"/>。</param>
|
||||
public static SaveSnapshot Capture(string triggerNodeName = null)
|
||||
{
|
||||
SnapshotRegistry.EnsureInitialized();
|
||||
return SnapshotCapture.Capture(YarnVariableStorage.Instance);
|
||||
return SnapshotCapture.Capture(YarnVariableStorage.Instance, triggerNodeName);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user