diff --git a/Assets/Scripts/Dialog System/DialogController.cs b/Assets/Scripts/Dialog System/DialogController.cs
index 0ba7b236a..945090d2b 100644
--- a/Assets/Scripts/Dialog System/DialogController.cs
+++ b/Assets/Scripts/Dialog System/DialogController.cs
@@ -87,6 +87,7 @@ namespace AibisDream
/// Yarn组
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();
+
+ 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();
}
diff --git a/Assets/Scripts/SaveSystem/SavePointEvaluator.cs b/Assets/Scripts/SaveSystem/SavePointEvaluator.cs
index bb15684b3..edd87fe07 100644
--- a/Assets/Scripts/SaveSystem/SavePointEvaluator.cs
+++ b/Assets/Scripts/SaveSystem/SavePointEvaluator.cs
@@ -25,6 +25,9 @@ namespace AibisDream.SaveSystem
/// 节点显式附加了 no_save 标记。
MarkedNoSave,
+
+ /// 同一节点仍在执行中(detour 返回续跑),非 Fresh 进入。
+ DetourResume,
}
///
@@ -37,6 +40,10 @@ namespace AibisDream.SaveSystem
/// - 无活跃节点、或节点未声明 tag,默认允许并打 warning;
/// - 未知 tag 默认允许并打 warning。
///
+ ///
+ /// OnNodeStart 自动档另按 YarnProject 维度跟踪 InProgress 节点:
+ /// detour 返回父节点会再次 onNodeStart,若该节点尚未 onNodeComplete 则视为 DetourResume,不自动存。
+ ///
///
public static class SavePointEvaluator
{
@@ -61,6 +68,93 @@ namespace AibisDream.SaveSystem
"end", // 维修结束/收尾
};
+ private static string _activeProjectId;
+ private static readonly HashSet InProgressNodes = new(StringComparer.Ordinal);
+
+ /// 当前 YarnProject 下尚未 Complete 的节点名(只读,供调试)。
+ public static IReadOnlyCollection InProgressNodeNames => InProgressNodes;
+
+ /// 切换 YarnProject 或停止对话时重置 InProgress 跟踪。
+ public static void ResetForProject(string projectId)
+ {
+ InProgressNodes.Clear();
+ _activeProjectId = projectId;
+ }
+
+ /// 仅在 projectId 变化时重置,避免同 Project 内误清 InProgress。
+ public static void ResetForProjectIfChanged(string projectId)
+ {
+ if (string.Equals(_activeProjectId, projectId, StringComparison.Ordinal))
+ {
+ return;
+ }
+
+ ResetForProject(projectId);
+ }
+
+ /// 读档重进 anchor:标记 InProgress,不写盘。
+ public static void OnRestoreEnterNode(string projectId, string nodeName)
+ {
+ if (string.IsNullOrEmpty(nodeName))
+ {
+ return;
+ }
+
+ EnsureProjectScope(projectId);
+ InProgressNodes.Add(nodeName);
+ }
+
+ ///
+ /// OnNodeStart 自动档判定:Fresh 进入且 tag 通过则允许;detour 返回续跑则拒绝。
+ ///
+ public static bool OnNodeStartForAutoSave(
+ string projectId,
+ string nodeName,
+ IReadOnlyList 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);
+ }
+
+ /// 仅 tag / no_save 判定,不含 InProgress detour 门控(供编辑器模拟)。
+ public static bool EvaluateNodeTagsForAutoSave(
+ string nodeName,
+ IReadOnlyList tags,
+ out SavePointRejectReason reason)
+ {
+ return EvaluateTagsForAutoSave(nodeName, tags, out reason);
+ }
+
+ /// 节点执行完毕,移出 InProgress。
+ public static void OnNodeComplete(string projectId, string nodeName)
+ {
+ if (string.IsNullOrEmpty(nodeName))
+ {
+ return;
+ }
+
+ EnsureProjectScope(projectId);
+ InProgressNodes.Remove(nodeName);
+ }
+
///
/// 当前是否可以执行 Yarn 显式 <<save>> 命令。
/// 仅检查全局门控,不检查节点 tag / no_save。
@@ -77,7 +171,7 @@ namespace AibisDream.SaveSystem
}
///
- /// 当前是否可以自动存档。
+ /// 当前是否可以自动存档(不含 detour resume 判定;供 TryAutoSave 等无节点上下文路径)。
///
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);
}
///
@@ -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 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 tags, string tag)
{
if (tags == null) return false;
@@ -203,9 +306,6 @@ namespace AibisDream.SaveSystem
return false;
}
- ///
- /// 从 tags 中找到第一个已知的保存语义主 tag;若均未知则返回 null。
- ///
private static string FindPrimaryTag(IReadOnlyList tags)
{
foreach (var tag in tags)