diff --git a/Assets/Scripts/Dialog System/DialogController.cs b/Assets/Scripts/Dialog System/DialogController.cs index 816eae864..b2031a28d 100644 --- a/Assets/Scripts/Dialog System/DialogController.cs +++ b/Assets/Scripts/Dialog System/DialogController.cs @@ -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.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 /// public (string nodeName, IReadOnlyList tags) GetCurrentNodeContext() { - if (_dialogueRunner == null) + if (string.IsNullOrEmpty(_currentNodeName)) { return (null, Array.Empty()); } - var dialogue = _dialogueRunner.Dialogue; - if (dialogue == null) + return (_currentNodeName, _currentNodeTags ?? Array.Empty()); + } + + 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()); + ClearCurrentNodeContext(); + return; } - var nodeName = dialogue.CurrentNode; - if (string.IsNullOrEmpty(nodeName)) - { - return (null, Array.Empty()); - } + _currentNodeName = nodeName; - var tagsHeader = dialogue.GetHeaderValue(nodeName, "tags"); - if (string.IsNullOrWhiteSpace(tagsHeader)) - { - return (nodeName, Array.Empty()); - } + var tagsHeader = _dialogueRunner.Dialogue.GetHeaderValue(nodeName, "tags"); + _currentNodeTags = string.IsNullOrWhiteSpace(tagsHeader) + ? Array.Empty() + : tagsHeader.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); + } - return (nodeName, tagsHeader.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries)); + private void ClearCurrentNodeContext() + { + _currentNodeName = null; + _currentNodeTags = null; } #region 推进方式修改 diff --git a/Assets/Scripts/SaveSystem/SavePointEvaluator.cs b/Assets/Scripts/SaveSystem/SavePointEvaluator.cs new file mode 100644 index 000000000..113d160c4 --- /dev/null +++ b/Assets/Scripts/SaveSystem/SavePointEvaluator.cs @@ -0,0 +1,105 @@ +namespace AibisDream.SaveSystem +{ + /// + /// 可存点被拒绝的原因,供日志与 UI 提示使用。 + /// + public enum SavePointRejectReason + { + None, + Restoring, + GamePaused, + NoActiveNode, + StartNode, + FunctionNode, + NoAutoSave + } + + /// + /// 可存点判定器。 + /// + /// P3 只搭框架:规则不是最终版,后续随 Yarn 节点 tags 整理和章节策略扩展而更新。 + /// 当前所有 FixState 均放行,不可存状态由 Yarn 节点流程控制。 + /// + /// + public static class SavePointEvaluator + { + /// + /// 当前是否可以自动存档。 + /// + 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; + } + + /// + /// 当前是否可以手动存档。 + /// 手动档 = 复制最近自动档,因此需要当前可自动存档且自动档存在。 + /// + 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 tags, string tag) + { + if (tags == null) return false; + + foreach (var t in tags) + { + if (t == tag) + { + return true; + } + } + + return false; + } + } +} diff --git a/Assets/Scripts/SaveSystem/SavePointEvaluator.cs.meta b/Assets/Scripts/SaveSystem/SavePointEvaluator.cs.meta new file mode 100644 index 000000000..5eae8ae6a --- /dev/null +++ b/Assets/Scripts/SaveSystem/SavePointEvaluator.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 512f62f0cf333514384484ef1ab1cb3e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs b/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs index 509320445..0b2f1f4e7 100644 --- a/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs +++ b/Assets/Scripts/SaveSystem/SaveRestoreOrchestrator.cs @@ -11,7 +11,7 @@ namespace AibisDream.SaveSystem /// /// 存读档流程编排层:连接 UI、 与槽位系统。 /// - /// 对外入口:(<<auto_save>>)、 + /// 对外入口:(节点进入事件触发)、 /// (手动存档)、 /// (读档)、 /// (旧档/调试)。 @@ -19,8 +19,11 @@ namespace AibisDream.SaveSystem /// public static class SaveRestoreOrchestrator { + /// 是否正在读档还原中;为 true 时禁止自动存档覆盖 slot_0。 + public static bool IsRestoring { get; private set; } + /// 捕获快照并写入自动档(slot 0);含保存 Loading UI。 - 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().ShowSaveLoading()) .Delay(1f) @@ -46,39 +55,61 @@ namespace AibisDream.SaveSystem /// 将当前自动档复制到指定手动档。 public static void CreateManualSlot(int slotIndex) { + if (!SavePointEvaluator.CanManualSave(out var reason)) + { + Debug.LogWarning($"[SaveRestoreOrchestrator] 当前不可手动存档:{reason}"); + return; + } + SlotManager.CopyAutoToManual(slotIndex); } /// 从指定槽位读档并还原。 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; + } } /// 从文件读档并还原;自动区分新快照格式与 legacy 格式。 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); } /// 旧档:Yarn 变量 + DeepRepairDataRegistry systemData。