#if UNITY_EDITOR || DEVELOPMENT_BUILD using System; using System.Linq; using System.Threading.Tasks; using AibisDream.Utility; using UnityEngine; namespace AibisDream.SaveSystem { /// 正式自动档成功后使用的非阻塞测试存档旁路。 public static class TestSaveRecorder { private static TestSaveRepository _repository; public static bool IsRecording { get; private set; } public static TestSaveRepository Repository => _repository ??= new TestSaveRepository(ConstRef.TestSavePath); public static event Action LibraryChanged; [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] private static void ResetRuntimeState() { IsRecording = false; _repository = null; } public static void SetRecording(bool recording) { IsRecording = recording; } internal static TestSaveRecordRequest CreateRequest( SaveSnapshot snapshot, byte[] thumbnail, string saveTrigger, string resumeMode) { if (!IsRecording || snapshot?.anchor == null || string.IsNullOrWhiteSpace(snapshot.anchor.sceneSoName) || string.IsNullOrWhiteSpace(snapshot.anchor.yarnProjectId) || string.IsNullOrWhiteSpace(snapshot.anchor.nodeName)) { return null; } var anchor = snapshot.anchor; var expectedResumeMode = anchor.startDialogueOnRestore ? nameof(SaveResumeMode.RestartNode) : nameof(SaveResumeMode.StateOnly); if (!string.Equals(resumeMode, expectedResumeMode, StringComparison.Ordinal)) { throw new InvalidOperationException( $"测试存档恢复模式与 snapshot anchor 不一致: meta={resumeMode}, " + $"anchor={expectedResumeMode}"); } var chapter = GameManager.Instance?.RuntimeChapters? .FirstOrDefault(item => item != null && string.Equals(item.name, anchor.sceneSoName, StringComparison.Ordinal) && string.Equals(item.yarnProject?.name, anchor.yarnProjectId, StringComparison.Ordinal)); var dedupeKey = BuildDedupeKey( anchor.sceneSoName, anchor.yarnProjectId, anchor.nodeName, resumeMode); return new TestSaveRecordRequest { Snapshot = snapshot, Thumbnail = thumbnail, DedupeKey = dedupeKey, EntryId = TestSaveRepository.StableHash(dedupeKey), ChapterId = chapter?.name ?? anchor.sceneSoName, ChapterTitle = string.IsNullOrWhiteSpace(chapter?.title) ? anchor.sceneSoName : chapter.title, SceneSoName = anchor.sceneSoName, YarnProjectId = anchor.yarnProjectId, NodeName = anchor.nodeName, SaveTrigger = saveTrigger, ResumeMode = resumeMode, StartDialogueOnRestore = anchor.startDialogueOnRestore, SceneName = snapshot.scene?.sceneName, GameVersion = snapshot.gameVersion }; } internal static void Enqueue(TestSaveRecordRequest request) { if (request == null) return; _ = Task.Run(() => { try { Repository.Record(request); LibraryChanged?.Invoke(); } catch (Exception ex) { Debug.LogError($"[TestSaveRecorder] 测试存档旁路写入失败,不影响正式存档:{ex}"); } }); } public static string BuildDedupeKey( string sceneSoName, string yarnProjectId, string nodeName, string resumeMode = null) { var baseKey = $"{sceneSoName ?? string.Empty}\n{yarnProjectId ?? string.Empty}\n{nodeName ?? string.Empty}"; return string.IsNullOrEmpty(resumeMode) || string.Equals( resumeMode, nameof(SaveResumeMode.RestartNode), StringComparison.Ordinal) ? baseKey : $"{baseKey}\n{resumeMode}"; } } } #endif