feat(save): 添加可存点判定器与节点事件触发机制

This commit is contained in:
2026-06-12 19:58:20 +08:00
parent 748c875e93
commit e01c8af449
4 changed files with 206 additions and 33 deletions
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using AibisDream.Kit;
using AibisDream.SaveSystem;
using UnityEngine;
using UnityEngine.Events;
using AibisDream.Framework;
@@ -16,20 +17,30 @@ namespace AibisDream
[SerializeField] private LineAdvanceInput lineAdvanceInput;
private string _currentNodeName;
private string[] _currentNodeTags;
private void Start()
{
_dialogueRunner = GetComponentInChildren<DialogueRunner>();
_dialogueRunner.onDialogueStart ??= new UnityEvent();
_dialogueRunner.onDialogueComplete ??= new UnityEvent();
_dialogueRunner.onNodeStart ??= new UnityEventString();
_dialogueRunner.onNodeComplete ??= new UnityEventString();
_dialogueRunner.onDialogueStart.AddListener(() =>
{
EnumEventSystem.Global.Send(InteractionEventEnum.DialogStart);
});
_dialogueRunner.onDialogueComplete.AddListener(() =>
{
EnumEventSystem.Global.Send(InteractionEventEnum.DialogEnd);
ClearCurrentNodeContext();
});
_dialogueRunner.onNodeStart.AddListener(OnNodeStart);
_dialogueRunner.onNodeComplete.AddListener(OnNodeComplete);
}
private void OnEnable()
@@ -140,30 +151,45 @@ namespace AibisDream
/// </summary>
public (string nodeName, IReadOnlyList<string> tags) GetCurrentNodeContext()
{
if (_dialogueRunner == null)
if (string.IsNullOrEmpty(_currentNodeName))
{
return (null, Array.Empty<string>());
}
var dialogue = _dialogueRunner.Dialogue;
if (dialogue == null)
return (_currentNodeName, _currentNodeTags ?? Array.Empty<string>());
}
private void OnNodeStart(string nodeName)
{
UpdateCurrentNodeContext(nodeName);
SaveRestoreOrchestrator.TryAutoSave();
}
private void OnNodeComplete(string nodeName)
{
ClearCurrentNodeContext();
}
private void UpdateCurrentNodeContext(string nodeName)
{
if (_dialogueRunner == null || _dialogueRunner.Dialogue == null)
{
return (null, Array.Empty<string>());
ClearCurrentNodeContext();
return;
}
var nodeName = dialogue.CurrentNode;
if (string.IsNullOrEmpty(nodeName))
{
return (null, Array.Empty<string>());
}
_currentNodeName = nodeName;
var tagsHeader = dialogue.GetHeaderValue(nodeName, "tags");
if (string.IsNullOrWhiteSpace(tagsHeader))
{
return (nodeName, Array.Empty<string>());
}
var tagsHeader = _dialogueRunner.Dialogue.GetHeaderValue(nodeName, "tags");
_currentNodeTags = string.IsNullOrWhiteSpace(tagsHeader)
? Array.Empty<string>()
: tagsHeader.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries);
}
return (nodeName, tagsHeader.Split(Array.Empty<char>(), StringSplitOptions.RemoveEmptyEntries));
private void ClearCurrentNodeContext()
{
_currentNodeName = null;
_currentNodeTags = null;
}
#region
@@ -0,0 +1,105 @@
namespace AibisDream.SaveSystem
{
/// <summary>
/// 可存点被拒绝的原因,供日志与 UI 提示使用。
/// </summary>
public enum SavePointRejectReason
{
None,
Restoring,
GamePaused,
NoActiveNode,
StartNode,
FunctionNode,
NoAutoSave
}
/// <summary>
/// 可存点判定器。
/// <para>
/// P3 只搭框架:规则不是最终版,后续随 Yarn 节点 tags 整理和章节策略扩展而更新。
/// 当前所有 FixState 均放行,不可存状态由 Yarn 节点流程控制。
/// </para>
/// </summary>
public static class SavePointEvaluator
{
/// <summary>
/// 当前是否可以自动存档。
/// </summary>
public static bool CanAutoSave(out SavePointRejectReason reason)
{
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);
if (string.IsNullOrEmpty(nodeName))
{
reason = SavePointRejectReason.NoActiveNode;
return false;
}
if (nodeName == "Start")
{
reason = SavePointRejectReason.StartNode;
return false;
}
if (tags != null && IsTagPresent(tags, "function"))
{
reason = SavePointRejectReason.FunctionNode;
return false;
}
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(System.Collections.Generic.IReadOnlyList<string> tags, string tag)
{
if (tags == null) return false;
foreach (var t in tags)
{
if (t == tag)
{
return true;
}
}
return false;
}
}
}
@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 512f62f0cf333514384484ef1ab1cb3e
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:
@@ -11,7 +11,7 @@ namespace AibisDream.SaveSystem
/// <summary>
/// 存读档流程编排层:连接 UI、<see cref="SnapshotService"/> 与槽位系统。
/// <para>
/// 对外入口:<see cref="SaveToAutoSlot"/>&lt;&lt;auto_save&gt;&gt;)、
/// 对外入口:<see cref="TryAutoSave"/>节点进入事件触发)、
/// <see cref="CreateManualSlot"/>(手动存档)、
/// <see cref="RestoreFromSlot"/>(读档)、
/// <see cref="RestoreFromFile"/>(旧档/调试)。
@@ -19,8 +19,11 @@ namespace AibisDream.SaveSystem
/// </summary>
public static class SaveRestoreOrchestrator
{
/// <summary>是否正在读档还原中;为 true 时禁止自动存档覆盖 slot_0。</summary>
public static bool IsRestoring { get; private set; }
/// <summary>捕获快照并写入自动档(slot 0);含保存 Loading UI。</summary>
public static void SaveToAutoSlot()
public static void TryAutoSave()
{
var host = YarnVariableStorage.Instance;
if (host == null)
@@ -29,6 +32,12 @@ namespace AibisDream.SaveSystem
return;
}
if (!SavePointEvaluator.CanAutoSave(out var reason))
{
Debug.LogWarning($"[SaveRestoreOrchestrator] 当前不是可存点,跳过自动存档:{reason}");
return;
}
ActionKit.Sequence()
.Callback(() => UIManager.Instance.GetPanel<InfoPanel>().ShowSaveLoading())
.Delay(1f)
@@ -46,39 +55,61 @@ namespace AibisDream.SaveSystem
/// <summary>将当前自动档复制到指定手动档。</summary>
public static void CreateManualSlot(int slotIndex)
{
if (!SavePointEvaluator.CanManualSave(out var reason))
{
Debug.LogWarning($"[SaveRestoreOrchestrator] 当前不可手动存档:{reason}");
return;
}
SlotManager.CopyAutoToManual(slotIndex);
}
/// <summary>从指定槽位读档并还原。</summary>
public static IEnumerator RestoreFromSlot(int slotIndex)
{
var snapshot = SlotManager.LoadSnapshot(slotIndex);
if (snapshot == null)
IsRestoring = true;
try
{
Debug.LogError($"[SaveRestoreOrchestrator] 槽位 {slotIndex} 不存在或读取失败");
yield break;
}
var snapshot = SlotManager.LoadSnapshot(slotIndex);
if (snapshot == null)
{
Debug.LogError($"[SaveRestoreOrchestrator] 槽位 {slotIndex} 不存在或读取失败");
yield break;
}
yield return RestoreSnapshot(snapshot);
yield return RestoreSnapshot(snapshot);
}
finally
{
IsRestoring = false;
}
}
/// <summary>从文件读档并还原;自动区分新快照格式与 legacy 格式。</summary>
public static IEnumerator RestoreFromFile(string savePath)
{
if (SnapshotPersistence.IsLegacyFormat(savePath))
IsRestoring = true;
try
{
Debug.LogWarning("[SaveRestoreOrchestrator] 检测到旧格式存档,请使用新快照格式重新保存。");
yield return RestoreLegacy(savePath);
yield break;
}
if (SnapshotPersistence.IsLegacyFormat(savePath))
{
Debug.LogWarning("[SaveRestoreOrchestrator] 检测到旧格式存档,请使用新快照格式重新保存。");
yield return RestoreLegacy(savePath);
yield break;
}
SaveSnapshot snapshot;
using (new CodeTimer("LoadSnapshot"))
SaveSnapshot snapshot;
using (new CodeTimer("LoadSnapshot"))
{
snapshot = SnapshotPersistence.Load(savePath);
}
yield return RestoreSnapshot(snapshot);
}
finally
{
snapshot = SnapshotPersistence.Load(savePath);
IsRestoring = false;
}
yield return RestoreSnapshot(snapshot);
}
/// <summary>旧档:Yarn 变量 + DeepRepairDataRegistry systemData。</summary>