using System; using System.Collections.Generic; using UnityEngine; namespace AibisDream.SaveSystem { /// /// 可存点被拒绝的原因,供日志与 UI 提示使用。 /// public enum SavePointRejectReason { None, AutoSaveSuppressed, Restoring, GamePaused, // 遗留值:早期逻辑曾返回,当前实现中不再使用,保留以避免外部代码编译失败。 NoActiveNode, StartNode, FunctionNode, NoAutoSave, /// 节点 tag 在默认禁止保存集合中。 DeniedByTag, /// 节点显式附加了 no_save 标记。 MarkedNoSave, } /// /// 可存点判定器。 /// /// 基于 Yarn 节点 tag 判定是否允许自动存档: /// - 白名单 tag(hub / linear / content)默认允许; /// - 黑名单 tag(start / init / function / detour / center / performance / event / end)默认禁止; /// - no_save 附加标记覆盖默认语义,一律禁止; /// - 无活跃节点、或节点未声明 tag,默认允许并打 warning; /// - 未知 tag 默认允许并打 warning。 /// /// 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", // 维修结束/收尾 }; /// /// 当前是否可以执行 Yarn 显式 <<save>> 命令。 /// 仅检查全局门控,不检查节点 tag / no_save。 /// public static bool CanExplicitSave(out SavePointRejectReason reason) { if (!TryPassGlobalSaveGuards(out reason)) { return false; } reason = SavePointRejectReason.None; return true; } /// /// 当前是否可以自动存档。 /// public static bool CanAutoSave(out SavePointRejectReason reason) { if (!TryPassGlobalSaveGuards(out reason)) { return false; } var (nodeName, tags) = DialogController.Instance != null ? 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; } /// /// 当前是否可以手动存档。 /// 手动档 = 复制最近落盘的自动档(含 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 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.Instance.state.isInPause) { 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 (t == tag) { return true; } } return false; } /// /// 从 tags 中找到第一个已知的保存语义主 tag;若均未知则返回 null。 /// private static string FindPrimaryTag(IReadOnlyList tags) { foreach (var tag in tags) { if (AutoSaveAllowedTags.Contains(tag) || AutoSaveDeniedTags.Contains(tag)) { return tag; } } return null; } } }