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,
/// 同一节点仍在执行中(detour 返回续跑),非 Fresh 进入。
DetourResume,
}
///
/// 可存点判定器。
///
/// 基于 Yarn 节点 tag 判定是否允许自动存档:
/// - 白名单 tag(hub / linear / content)默认允许;
/// - 黑名单 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 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。
///
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 EvaluateTagsForAutoSave(nodeName, tags, out reason);
}
///
/// 当前是否可以手动存档。
/// 手动档 = 复制最近落盘的自动档(含 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 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;
foreach (var t in tags)
{
if (t == tag)
{
return true;
}
}
return false;
}
private static string FindPrimaryTag(IReadOnlyList tags)
{
foreach (var tag in tags)
{
if (AutoSaveAllowedTags.Contains(tag) || AutoSaveDeniedTags.Contains(tag))
{
return tag;
}
}
return null;
}
}
}