feat(save): 添加 Yarn 节点 InProgress 跟踪与 detour 续跑去重

This commit is contained in:
2026-06-29 15:58:15 +08:00
parent 382b784605
commit 0f681b518e
2 changed files with 180 additions and 57 deletions
@@ -87,6 +87,7 @@ namespace AibisDream
/// <param name="yarnProject">Yarn组</param>
public void StartDialog(YarnProject yarnProject)
{
PrepareYarnProjectForDialog(yarnProject);
_dialogueRunner.SetProject(yarnProject);
_ = _dialogueRunner.StartDialogue("Start");
}
@@ -94,14 +95,21 @@ namespace AibisDream
public void StopDialog()
{
_ = _dialogueRunner.Stop();
SavePointEvaluator.ResetForProject(null);
YarnVariableStorage.Instance.Clear();
DialogUIManager.Instance.HideDialog();
}
public void LoadDialog(YarnProject yarnProject)
{
PrepareYarnProjectForDialog(yarnProject);
_dialogueRunner.SetProject(yarnProject);
_ = _dialogueRunner.StartDialogue("Center");
}
private static void PrepareYarnProjectForDialog(YarnProject yarnProject)
{
var projectId = yarnProject != null ? yarnProject.name : null;
SavePointEvaluator.ResetForProjectIfChanged(projectId);
}
public void StartDialogNode(string nodeName)
@@ -163,8 +171,22 @@ namespace AibisDream
{
UpdateCurrentNodeContext(nodeName);
if (!SavePointEvaluator.CanAutoSave(out _))
var projectId = _dialogueRunner?.YarnProject?.name;
var tags = _currentNodeTags ?? Array.Empty<string>();
if (SaveRestoreOrchestrator.IsRestoring)
{
SavePointEvaluator.OnRestoreEnterNode(projectId, nodeName);
return;
}
if (!SavePointEvaluator.OnNodeStartForAutoSave(projectId, nodeName, tags, out var reason))
{
if (reason == SavePointRejectReason.DetourResume)
{
Debug.Log($"[DialogController] 跳过 detour 返回续跑的自动存档: {nodeName}");
}
return;
}
@@ -173,6 +195,7 @@ namespace AibisDream
private void OnNodeComplete(string nodeName)
{
SavePointEvaluator.OnNodeComplete(_dialogueRunner?.YarnProject?.name, nodeName);
ClearCurrentNodeContext();
}
+155 -55
View File
@@ -25,6 +25,9 @@ namespace AibisDream.SaveSystem
/// <summary>节点显式附加了 no_save 标记。</summary>
MarkedNoSave,
/// <summary>同一节点仍在执行中(detour 返回续跑),非 Fresh 进入。</summary>
DetourResume,
}
/// <summary>
@@ -37,6 +40,10 @@ namespace AibisDream.SaveSystem
/// - 无活跃节点、或节点未声明 tag,默认允许并打 warning
/// - 未知 tag 默认允许并打 warning。
/// </para>
/// <para>
/// OnNodeStart 自动档另按 YarnProject 维度跟踪 InProgress 节点:
/// detour 返回父节点会再次 onNodeStart,若该节点尚未 onNodeComplete 则视为 DetourResume,不自动存。
/// </para>
/// </summary>
public static class SavePointEvaluator
{
@@ -61,6 +68,93 @@ namespace AibisDream.SaveSystem
"end", // 维修结束/收尾
};
private static string _activeProjectId;
private static readonly HashSet<string> InProgressNodes = new(StringComparer.Ordinal);
/// <summary>当前 YarnProject 下尚未 Complete 的节点名(只读,供调试)。</summary>
public static IReadOnlyCollection<string> InProgressNodeNames => InProgressNodes;
/// <summary>切换 YarnProject 或停止对话时重置 InProgress 跟踪。</summary>
public static void ResetForProject(string projectId)
{
InProgressNodes.Clear();
_activeProjectId = projectId;
}
/// <summary>仅在 projectId 变化时重置,避免同 Project 内误清 InProgress。</summary>
public static void ResetForProjectIfChanged(string projectId)
{
if (string.Equals(_activeProjectId, projectId, StringComparison.Ordinal))
{
return;
}
ResetForProject(projectId);
}
/// <summary>读档重进 anchor:标记 InProgress,不写盘。</summary>
public static void OnRestoreEnterNode(string projectId, string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
{
return;
}
EnsureProjectScope(projectId);
InProgressNodes.Add(nodeName);
}
/// <summary>
/// OnNodeStart 自动档判定:Fresh 进入且 tag 通过则允许;detour 返回续跑则拒绝。
/// </summary>
public static bool OnNodeStartForAutoSave(
string projectId,
string nodeName,
IReadOnlyList<string> tags,
out SavePointRejectReason reason)
{
if (!TryPassGlobalSaveGuards(out reason))
{
return false;
}
EnsureProjectScope(projectId);
if (!string.IsNullOrEmpty(nodeName) && InProgressNodes.Contains(nodeName))
{
reason = SavePointRejectReason.DetourResume;
return false;
}
if (!string.IsNullOrEmpty(nodeName))
{
InProgressNodes.Add(nodeName);
}
return EvaluateTagsForAutoSave(nodeName, tags, out reason);
}
/// <summary>仅 tag / no_save 判定,不含 InProgress detour 门控(供编辑器模拟)。</summary>
public static bool EvaluateNodeTagsForAutoSave(
string nodeName,
IReadOnlyList<string> tags,
out SavePointRejectReason reason)
{
return EvaluateTagsForAutoSave(nodeName, tags, out reason);
}
/// <summary>节点执行完毕,移出 InProgress。</summary>
public static void OnNodeComplete(string projectId, string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
{
return;
}
EnsureProjectScope(projectId);
InProgressNodes.Remove(nodeName);
}
/// <summary>
/// 当前是否可以执行 Yarn 显式 <c>&lt;&lt;save&gt;&gt;</c> 命令。
/// 仅检查全局门控,不检查节点 tag / no_save。
@@ -77,7 +171,7 @@ namespace AibisDream.SaveSystem
}
/// <summary>
/// 当前是否可以自动存档。
/// 当前是否可以自动存档(不含 detour resume 判定;供 TryAutoSave 等无节点上下文路径)
/// </summary>
public static bool CanAutoSave(out SavePointRejectReason reason)
{
@@ -90,57 +184,7 @@ namespace AibisDream.SaveSystem
? DialogController.Instance.GetCurrentNodeContext()
: (null, null);
// no_save 标记覆盖一切:即使节点类型默认可存,也不允许保存。
if (tags != null && IsTagPresent(tags, "no_save"))
{
reason = SavePointRejectReason.MarkedNoSave;
return false;
}
// 无活跃节点,或节点未声明任何 tag:允许保存。
// 注:FixSystemNew 等交互状态可能没有活跃 Yarn 节点,仍需保存当前状态。
if (string.IsNullOrEmpty(nodeName) || tags == null || tags.Count == 0)
{
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 (AutoSaveAllowedTags.Contains(primaryTag))
{
reason = SavePointRejectReason.None;
return true;
}
// 理论上不会到达:已识别主 tag 但既不在允许集合也不在禁止集合。
// 保持放行并警告,避免未来新增 tag 时意外阻断存档。
Debug.LogWarning(
$"[SavePointEvaluator] 节点 {nodeName} 的 tag '{primaryTag}' 未声明保存语义,默认允许自动存档。");
reason = SavePointRejectReason.None;
return true;
return EvaluateTagsForAutoSave(nodeName, tags, out reason);
}
/// <summary>
@@ -164,6 +208,14 @@ namespace AibisDream.SaveSystem
return true;
}
private static void EnsureProjectScope(string projectId)
{
if (!string.Equals(_activeProjectId, projectId, StringComparison.Ordinal))
{
ResetForProject(projectId);
}
}
private static bool TryPassGlobalSaveGuards(out SavePointRejectReason reason)
{
if (SaveRestoreOrchestrator.IsAutoSaveSuppressed)
@@ -188,6 +240,57 @@ namespace AibisDream.SaveSystem
return true;
}
private static bool EvaluateTagsForAutoSave(
string nodeName,
IReadOnlyList<string> tags,
out SavePointRejectReason reason)
{
if (tags != null && IsTagPresent(tags, "no_save"))
{
reason = SavePointRejectReason.MarkedNoSave;
return false;
}
if (string.IsNullOrEmpty(nodeName) || tags == null || tags.Count == 0)
{
if (!string.IsNullOrEmpty(nodeName) && (tags == null || tags.Count == 0))
{
Debug.LogWarning(
$"[SavePointEvaluator] 节点 {nodeName} 未声明任何 tag,默认允许自动存档。请补全节点类型标签。");
}
reason = SavePointRejectReason.None;
return true;
}
var primaryTag = FindPrimaryTag(tags);
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 (AutoSaveAllowedTags.Contains(primaryTag))
{
reason = SavePointRejectReason.None;
return true;
}
Debug.LogWarning(
$"[SavePointEvaluator] 节点 {nodeName} 的 tag '{primaryTag}' 未声明保存语义,默认允许自动存档。");
reason = SavePointRejectReason.None;
return true;
}
private static bool IsTagPresent(IReadOnlyList<string> tags, string tag)
{
if (tags == null) return false;
@@ -203,9 +306,6 @@ namespace AibisDream.SaveSystem
return false;
}
/// <summary>
/// 从 tags 中找到第一个已知的保存语义主 tag;若均未知则返回 null。
/// </summary>
private static string FindPrimaryTag(IReadOnlyList<string> tags)
{
foreach (var tag in tags)