From 0f681b518ee2cf2197e217e852fbfd2bdeddaeef Mon Sep 17 00:00:00 2001 From: Ding Yuntian <1491671119@qq.com> Date: Mon, 29 Jun 2026 15:58:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(save):=20=E6=B7=BB=E5=8A=A0=20Yarn=20?= =?UTF-8?q?=E8=8A=82=E7=82=B9=20InProgress=20=E8=B7=9F=E8=B8=AA=E4=B8=8E?= =?UTF-8?q?=20detour=20=E7=BB=AD=E8=B7=91=E5=8E=BB=E9=87=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Scripts/Dialog System/DialogController.cs | 27 ++- .../Scripts/SaveSystem/SavePointEvaluator.cs | 210 +++++++++++++----- 2 files changed, 180 insertions(+), 57 deletions(-) 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)