197 lines
6.8 KiB
C#
197 lines
6.8 KiB
C#
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>
|
||
/// 可存点判定器。
|
||
/// <para>
|
||
/// 基于 Yarn 节点 tag 判定是否允许自动存档:
|
||
/// - 白名单 tag(hub / linear / content)默认允许;
|
||
/// - 黑名单 tag(start / init / function / detour / center / performance / event / end)默认禁止;
|
||
/// - no_save 附加标记覆盖默认语义,一律禁止;
|
||
/// - 无活跃节点、或节点未声明 tag,默认允许并打 warning;
|
||
/// - 未知 tag 默认允许并打 warning。
|
||
/// </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", // 维修结束/收尾
|
||
};
|
||
|
||
/// <summary>
|
||
/// 当前是否可以自动存档。
|
||
/// </summary>
|
||
public static bool CanAutoSave(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;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 当前是否可以手动存档。
|
||
/// 手动档 = 复制最近自动档,因此需要当前可自动存档且自动档存在。
|
||
/// </summary>
|
||
public static bool CanManualSave(out SavePointRejectReason reason)
|
||
{
|
||
if (!CanAutoSave(out reason))
|
||
{
|
||
return false;
|
||
}
|
||
|
||
if (!SlotDirectory.Exists(SlotIndex.Auto))
|
||
{
|
||
reason = SavePointRejectReason.NoAutoSave;
|
||
return false;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 从 tags 中找到第一个已知的保存语义主 tag;若均未知则返回 null。
|
||
/// </summary>
|
||
private static string FindPrimaryTag(IReadOnlyList<string> tags)
|
||
{
|
||
foreach (var tag in tags)
|
||
{
|
||
if (AutoSaveAllowedTags.Contains(tag) || AutoSaveDeniedTags.Contains(tag))
|
||
{
|
||
return tag;
|
||
}
|
||
}
|
||
|
||
return null;
|
||
}
|
||
}
|
||
}
|