using System; using System.Collections.Generic; using System.Linq; using UnityEngine; namespace AibisDream.SaveSystem { public enum NodeSaveTiming { None, NodeEnter, DialogueExit } public enum SaveResumeMode { RestartNode, StateOnly } public readonly struct NodeSavePolicy { public NodeSaveTiming Timing { get; } public SaveResumeMode ResumeMode { get; } public NodeSavePolicy(NodeSaveTiming timing, SaveResumeMode resumeMode) { Timing = timing; ResumeMode = resumeMode; } public static NodeSavePolicy None => new(NodeSaveTiming.None, SaveResumeMode.RestartNode); public static NodeSavePolicy Enter => new(NodeSaveTiming.NodeEnter, SaveResumeMode.RestartNode); public static NodeSavePolicy Exit => new(NodeSaveTiming.DialogueExit, SaveResumeMode.StateOnly); } /// /// 可存点被拒绝的原因,供日志与 UI 提示使用。 /// public enum SavePointRejectReason { None, AutoSaveSuppressed, Restoring, GamePaused, // 遗留值:早期逻辑曾返回,当前实现中不再使用,保留以避免外部代码编译失败。 NoActiveNode, StartNode, FunctionNode, NoAutoSave, /// 节点 tag 在默认禁止保存集合中。 DeniedByTag, /// 节点显式附加了 no_save 标记。 MarkedNoSave, /// 同一节点仍在执行中(detour 返回续跑),非 Fresh 进入。 DetourResume, /// 节点类型或保存附加标记互相冲突。 InvalidTagConfiguration, } /// /// 可存点判定器。 /// /// 基于 Yarn 节点 tag 判定是否允许自动存档: /// - 白名单 tag(hub / linear / content)默认允许; /// - interaction 或 save_on_exit 在 Dialogue 正常结束时保存 state-only 档; /// - 黑名单 tag(start / init / function / detour / center / performance / event / end)默认禁止; /// - no_save 附加标记覆盖默认语义;与退出保存语义并存时视为配置冲突; /// - 无活跃节点、或节点未声明 tag,默认允许并打 warning; /// - 未知 tag 默认允许并打 warning。 /// /// /// OnNodeStart 自动档另按 YarnProject 维度跟踪 InProgress 节点: /// detour 返回父节点会再次 onNodeStart,若该节点尚未 onNodeComplete 则视为 DetourResume,不自动存。 /// /// public static class SavePointEvaluator { /// 默认可作为自动存档边界的节点 tag。 private static readonly HashSet AutoSaveAllowedTags = new(StringComparer.OrdinalIgnoreCase) { "hub", // 梦境:场景导航中枢 "linear", // 梦境:线性剧情推进 "content", // 维修:阶段内容节点 }; /// 默认禁止作为自动存档边界的节点 tag。 private static readonly HashSet AutoSaveDeniedTags = new(StringComparer.OrdinalIgnoreCase) { "start", // 引擎入口 "init", // 变量声明/初始化 "function", // 纯指令函数 "detour", // 梦境内容片段 "center", // 维修阶段调度中枢 "performance", // 维修原子化演出 "event", // 维修 C# 事件响应入口 "end", // 维修结束/收尾 }; private static readonly HashSet PrimaryNodeTags = new( AutoSaveAllowedTags.Concat(AutoSaveDeniedTags), StringComparer.OrdinalIgnoreCase) { "interaction", }; 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) { var accepted = OnNodeStartForSavePolicy( projectId, nodeName, tags, out var policy, out reason); return accepted && policy.Timing == NodeSaveTiming.NodeEnter; } /// /// OnNodeStart 存档策略判定:Fresh 进入时返回节点的进入/退出保存策略; /// detour 返回续跑或全局门控失败时返回 false。 /// public static bool OnNodeStartForSavePolicy( string projectId, string nodeName, IReadOnlyList tags, out NodeSavePolicy policy, out SavePointRejectReason reason) { policy = NodeSavePolicy.None; 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); } policy = ResolveNodePolicy(nodeName, tags, out reason); return policy.Timing != NodeSaveTiming.None; } /// 仅 tag / no_save 判定,不含 InProgress detour 门控(供编辑器模拟)。 public static bool EvaluateNodeTagsForAutoSave( string nodeName, IReadOnlyList tags, out SavePointRejectReason reason) { return ResolveNodePolicy(nodeName, tags, out reason, logWarnings: true).Timing == NodeSaveTiming.NodeEnter; } #if UNITY_EDITOR || DEVELOPMENT_BUILD /// 测试存档覆盖率扫描使用;语义与正式判定一致,但不会为全量节点刷 warning。 public static bool EvaluateNodeTagsForAutoSaveSilently( string nodeName, IReadOnlyList tags, out SavePointRejectReason reason) { return ResolveNodePolicy(nodeName, tags, out reason, logWarnings: false).Timing == NodeSaveTiming.NodeEnter; } #endif /// 仅根据节点 tags 解析保存策略,不检查全局门控或 detour 状态。 public static NodeSavePolicy ResolveNodePolicy( string nodeName, IReadOnlyList tags, out SavePointRejectReason reason, bool logWarnings = true) { var hasNoSave = IsTagPresent(tags, "no_save"); var hasSaveOnExit = IsTagPresent(tags, "save_on_exit"); var primaryTags = FindPrimaryTags(tags); var hasInteraction = primaryTags.Exists( tag => string.Equals(tag, "interaction", StringComparison.OrdinalIgnoreCase)); if (primaryTags.Count > 1) { if (logWarnings) { Debug.LogError( $"[SavePointEvaluator] 节点 {FormatNodeName(nodeName)} 声明了多个一级类型: " + $"[{string.Join(", ", primaryTags)}]。为避免错误存档,已禁用该节点存档。"); } reason = SavePointRejectReason.InvalidTagConfiguration; return NodeSavePolicy.None; } if (hasNoSave && (hasInteraction || hasSaveOnExit)) { if (logWarnings) { Debug.LogError( $"[SavePointEvaluator] 节点 {FormatNodeName(nodeName)} 的 no_save 与 " + $"{(hasInteraction ? "interaction" : "save_on_exit")} 冲突,已禁用该节点存档。"); } reason = SavePointRejectReason.InvalidTagConfiguration; return NodeSavePolicy.None; } if (hasNoSave) { reason = SavePointRejectReason.MarkedNoSave; return NodeSavePolicy.None; } if (hasInteraction) { if (hasSaveOnExit && logWarnings) { Debug.LogWarning( $"[SavePointEvaluator] 节点 {FormatNodeName(nodeName)} 的 interaction 已包含 " + "save_on_exit 语义,无需重复标记。"); } reason = SavePointRejectReason.None; return NodeSavePolicy.Exit; } if (hasSaveOnExit) { reason = SavePointRejectReason.None; return NodeSavePolicy.Exit; } if (string.IsNullOrEmpty(nodeName) || tags == null || tags.Count == 0) { if (logWarnings && !string.IsNullOrEmpty(nodeName) && (tags == null || tags.Count == 0)) { Debug.LogWarning( $"[SavePointEvaluator] 节点 {nodeName} 未声明任何 tag,默认允许进入时自动存档。请补全节点类型标签。"); } reason = SavePointRejectReason.None; return NodeSavePolicy.Enter; } var primaryTag = primaryTags.Count == 1 ? primaryTags[0] : null; if (primaryTag == null) { if (logWarnings) { Debug.LogWarning( $"[SavePointEvaluator] 节点 {nodeName} 的 tags [{string.Join(", ", tags)}] " + "未声明保存语义,默认允许进入时自动存档。"); } reason = SavePointRejectReason.None; return NodeSavePolicy.Enter; } if (AutoSaveDeniedTags.Contains(primaryTag)) { reason = SavePointRejectReason.DeniedByTag; return NodeSavePolicy.None; } reason = SavePointRejectReason.None; return NodeSavePolicy.Enter; } /// 节点执行完毕,移出 InProgress。 public static void OnNodeComplete(string projectId, string nodeName) { if (string.IsNullOrEmpty(nodeName)) { return; } EnsureProjectScope(projectId); InProgressNodes.Remove(nodeName); } /// /// 当前是否可以执行 Yarn 显式 <<save>> 命令。 /// 仅检查全局门控,不检查节点 tag / no_save。 /// public static bool CanExplicitSave(out SavePointRejectReason reason) { if (!TryPassGlobalSaveGuards(out reason)) { return false; } reason = SavePointRejectReason.None; return true; } /// /// 当前是否可以自动存档(不含 detour resume 判定;供 TryAutoSave 等无节点上下文路径)。 /// public static bool CanAutoSave(out SavePointRejectReason reason) { if (!TryPassGlobalSaveGuards(out reason)) { return false; } var (nodeName, tags) = DialogController.Instance != null ? DialogController.Instance.GetCurrentNodeContext() : (null, null); return ResolveNodePolicy(nodeName, tags, out reason).Timing == NodeSaveTiming.NodeEnter; } /// /// 当前是否可以手动存档。 /// 手动档 = 复制最近落盘的自动档(含 OnNodeStart 与显式 <<save>>)。 /// public static bool CanManualSave(out SavePointRejectReason reason) { if (!TryPassGlobalSaveGuards(out reason)) { return false; } if (!SlotDirectory.Exists(SlotIndex.Auto)) { reason = SavePointRejectReason.NoAutoSave; return false; } reason = SavePointRejectReason.None; 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) { reason = SavePointRejectReason.AutoSaveSuppressed; return false; } if (SaveRestoreOrchestrator.IsRestoring) { reason = SavePointRejectReason.Restoring; return false; } if (GameManager.Instance != null && GameManager.Session.IsPaused) { reason = SavePointRejectReason.GamePaused; return false; } reason = SavePointRejectReason.None; return true; } private static bool IsTagPresent(IReadOnlyList tags, string tag) { if (tags == null) return false; foreach (var t in tags) { if (string.Equals(t, tag, StringComparison.OrdinalIgnoreCase)) { return true; } } return false; } private static List FindPrimaryTags(IReadOnlyList tags) { var result = new List(); if (tags == null) { return result; } foreach (var tag in tags) { if (PrimaryNodeTags.Contains(tag) && !result.Exists(item => string.Equals(item, tag, StringComparison.OrdinalIgnoreCase))) { result.Add(tag); } } return result; } private static string FormatNodeName(string nodeName) { return string.IsNullOrEmpty(nodeName) ? "(无节点)" : nodeName; } } }