Files
aibis-dream/Assets/Scripts/SaveSystem/SavePointEvaluator.cs
T

323 lines
11 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using UnityEngine;
namespace AibisDream.SaveSystem
{
/// <summary>
/// 可存点被拒绝的原因,供日志与 UI 提示使用。
/// </summary>
public enum SavePointRejectReason
{
None,
AutoSaveSuppressed,
Restoring,
GamePaused,
// 遗留值:早期逻辑曾返回,当前实现中不再使用,保留以避免外部代码编译失败。
NoActiveNode,
StartNode,
FunctionNode,
NoAutoSave,
/// <summary>节点 tag 在默认禁止保存集合中。</summary>
DeniedByTag,
/// <summary>节点显式附加了 no_save 标记。</summary>
MarkedNoSave,
/// <summary>同一节点仍在执行中(detour 返回续跑),非 Fresh 进入。</summary>
DetourResume,
}
/// <summary>
/// 可存点判定器。
/// <para>
/// 基于 Yarn 节点 tag 判定是否允许自动存档:
/// - 白名单 taghub / linear / content)默认允许;
/// - 黑名单 tagstart / init / function / detour / center / performance / event / end)默认禁止;
/// - no_save 附加标记覆盖默认语义,一律禁止;
/// - 无活跃节点、或节点未声明 tag,默认允许并打 warning
/// - 未知 tag 默认允许并打 warning。
/// </para>
/// <para>
/// OnNodeStart 自动档另按 YarnProject 维度跟踪 InProgress 节点:
/// detour 返回父节点会再次 onNodeStart,若该节点尚未 onNodeComplete 则视为 DetourResume,不自动存。
/// </para>
/// </summary>
public static class SavePointEvaluator
{
/// <summary>默认可作为自动存档边界的节点 tag。</summary>
private static readonly HashSet<string> AutoSaveAllowedTags = new(StringComparer.OrdinalIgnoreCase)
{
"hub", // 梦境:场景导航中枢
"linear", // 梦境:线性剧情推进
"content", // 维修:阶段内容节点
};
/// <summary>默认禁止作为自动存档边界的节点 tag。</summary>
private static readonly HashSet<string> AutoSaveDeniedTags = new(StringComparer.OrdinalIgnoreCase)
{
"start", // 引擎入口
"init", // 变量声明/初始化
"function", // 纯指令函数
"detour", // 梦境内容片段
"center", // 维修阶段调度中枢
"performance", // 维修原子化演出
"event", // 维修 C# 事件响应入口
"end", // 维修结束/收尾
};
private static string _activeProjectId;
private static readonly HashSet<string> InProgressNodes = new(StringComparer.Ordinal);
/// <summary>当前 YarnProject 下尚未 Complete 的节点名(只读,供调试)。</summary>
public static IReadOnlyCollection<string> InProgressNodeNames => InProgressNodes;
/// <summary>切换 YarnProject 或停止对话时重置 InProgress 跟踪。</summary>
public static void ResetForProject(string projectId)
{
InProgressNodes.Clear();
_activeProjectId = projectId;
}
/// <summary>仅在 projectId 变化时重置,避免同 Project 内误清 InProgress。</summary>
public static void ResetForProjectIfChanged(string projectId)
{
if (string.Equals(_activeProjectId, projectId, StringComparison.Ordinal))
{
return;
}
ResetForProject(projectId);
}
/// <summary>读档重进 anchor:标记 InProgress,不写盘。</summary>
public static void OnRestoreEnterNode(string projectId, string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
{
return;
}
EnsureProjectScope(projectId);
InProgressNodes.Add(nodeName);
}
/// <summary>
/// OnNodeStart 自动档判定:Fresh 进入且 tag 通过则允许;detour 返回续跑则拒绝。
/// </summary>
public static bool OnNodeStartForAutoSave(
string projectId,
string nodeName,
IReadOnlyList<string> 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);
}
/// <summary>仅 tag / no_save 判定,不含 InProgress detour 门控(供编辑器模拟)。</summary>
public static bool EvaluateNodeTagsForAutoSave(
string nodeName,
IReadOnlyList<string> tags,
out SavePointRejectReason reason)
{
return EvaluateTagsForAutoSave(nodeName, tags, out reason);
}
/// <summary>节点执行完毕,移出 InProgress。</summary>
public static void OnNodeComplete(string projectId, string nodeName)
{
if (string.IsNullOrEmpty(nodeName))
{
return;
}
EnsureProjectScope(projectId);
InProgressNodes.Remove(nodeName);
}
/// <summary>
/// 当前是否可以执行 Yarn 显式 <c>&lt;&lt;save&gt;&gt;</c> 命令。
/// 仅检查全局门控,不检查节点 tag / no_save。
/// </summary>
public static bool CanExplicitSave(out SavePointRejectReason reason)
{
if (!TryPassGlobalSaveGuards(out reason))
{
return false;
}
reason = SavePointRejectReason.None;
return true;
}
/// <summary>
/// 当前是否可以自动存档(不含 detour resume 判定;供 TryAutoSave 等无节点上下文路径)。
/// </summary>
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);
}
/// <summary>
/// 当前是否可以手动存档。
/// 手动档 = 复制最近落盘的自动档(含 OnNodeStart 与显式 <c>&lt;&lt;save&gt;&gt;</c>)。
/// </summary>
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.Instance.state.isInPause)
{
reason = SavePointRejectReason.GamePaused;
return false;
}
reason = SavePointRejectReason.None;
return true;
}
private static bool EvaluateTagsForAutoSave(
string nodeName,
IReadOnlyList<string> 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<string> 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<string> tags)
{
foreach (var tag in tags)
{
if (AutoSaveAllowedTags.Contains(tag) || AutoSaveDeniedTags.Contains(tag))
{
return tag;
}
}
return null;
}
}
}