using System; using System.Collections.Generic; using System.IO; using System.Linq; using AibisDream.Utility; using Newtonsoft.Json; using UnityEngine; namespace AibisDream.SaveSystem { /// /// 测试存档模式:每次自动存档写入 下的独立子目录。 /// 开启后不写入 slot_0,存多少次就保留多少份,与正式 saves/ 完全隔离。 /// public static class TestSaveArchive { public const string EditorPrefsEnabledKey = "AibisDream.SaveSystem.TestArchiveEnabled"; /// 测试存档模式是否开启(Editor 下由 EditorPrefs 控制,构建产物恒为 false)。 public static bool IsEnabled { get { #if UNITY_EDITOR return UnityEditor.EditorPrefs.GetBool(EditorPrefsEnabledKey, true); #else return false; #endif } } private static readonly object ArchiveWriteLock = new object(); /// 将快照写入 testsavs 新条目,返回文件夹名。每次调用均新建,不覆盖已有条目。 public static string Archive(SaveSnapshot snapshot, byte[] thumbnailPng) { if (snapshot == null) { return null; } var entryId = BuildUniqueEntryId(snapshot); var entryDir = Path.Combine(ConstRef.TestAutoSaveArchivePath, entryId); Directory.CreateDirectory(entryDir); var json = SnapshotPersistence.Serialize(snapshot); var meta = BuildMeta(entryId, snapshot); lock (ArchiveWriteLock) { SlotAtomicWriter.WriteText(json, Path.Combine(entryDir, $"{ConstRef.SaveSnapshotFileName}.json")); SlotAtomicWriter.WriteJson(meta, Path.Combine(entryDir, $"{ConstRef.SaveMetaFileName}.json")); if (thumbnailPng != null) { SlotAtomicWriter.WriteBytes( thumbnailPng, Path.Combine(entryDir, $"{ConstRef.SaveThumbnailFileName}.png")); } } Debug.Log($"[TestSaveArchive] 已归档测试存档: {entryId}"); return entryId; } public static List ListEntries() { var result = new List(); if (!Directory.Exists(ConstRef.TestAutoSaveArchivePath)) { return result; } foreach (var dir in Directory.GetDirectories(ConstRef.TestAutoSaveArchivePath)) { var entryId = Path.GetFileName(dir); var snapshotPath = Path.Combine(dir, $"{ConstRef.SaveSnapshotFileName}.json"); if (!File.Exists(snapshotPath)) { continue; } var metaPath = Path.Combine(dir, $"{ConstRef.SaveMetaFileName}.json"); var thumbPath = Path.Combine(dir, $"{ConstRef.SaveThumbnailFileName}.png"); SlotMeta meta = null; if (File.Exists(metaPath)) { try { meta = JsonConvert.DeserializeObject(File.ReadAllText(metaPath)); } catch (Exception ex) { Debug.LogWarning($"[TestSaveArchive] 读取 meta 失败 ({entryId}): {ex.Message}"); } } result.Add(new TestSaveArchiveEntry( entryId, dir, snapshotPath, File.Exists(thumbPath) ? thumbPath : null, meta)); } return result .OrderByDescending(e => e.Meta?.savedAt ?? string.Empty) .ThenByDescending(e => e.EntryId, StringComparer.Ordinal) .ToList(); } public static void DeleteEntry(string entryId) { if (string.IsNullOrEmpty(entryId)) { return; } var dir = Path.Combine(ConstRef.TestAutoSaveArchivePath, entryId); if (Directory.Exists(dir)) { Directory.Delete(dir, recursive: true); } } public static void ClearAll() { if (!Directory.Exists(ConstRef.TestAutoSaveArchivePath)) { return; } foreach (var dir in Directory.GetDirectories(ConstRef.TestAutoSaveArchivePath)) { Directory.Delete(dir, recursive: true); } } private static string BuildUniqueEntryId(SaveSnapshot snapshot) { var baseId = BuildEntryId(snapshot); var entryId = baseId; var suffix = 2; while (Directory.Exists(Path.Combine(ConstRef.TestAutoSaveArchivePath, entryId))) { entryId = $"{baseId}_{suffix}"; suffix++; } return entryId; } private static string BuildEntryId(SaveSnapshot snapshot) { var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff"); var nodeName = snapshot.anchor?.nodeName; var safeNode = string.IsNullOrEmpty(nodeName) ? "no_node" : SanitizeEntryName(nodeName); return $"{timestamp}_{safeNode}"; } private static SlotMeta BuildMeta(string entryId, SaveSnapshot snapshot) { var summary = SaveSnapshotSummary.From(snapshot); return new SlotMeta { slotIndex = -1, savedAt = snapshot.savedAt, sceneName = summary.SceneName, sceneSoName = summary.SceneSoName, yarnProjectId = summary.YarnProjectId, nodeName = snapshot.anchor?.nodeName, schemaVersion = snapshot.schemaVersion, gameVersion = snapshot.gameVersion, thumbnailFile = $"{ConstRef.SaveThumbnailFileName}.png" }; } private static string SanitizeEntryName(string name) { foreach (var c in Path.GetInvalidFileNameChars()) { name = name.Replace(c, '_'); } if (name.Length > 48) { name = name.Substring(0, 48); } return string.IsNullOrWhiteSpace(name) ? "no_node" : name; } } public readonly struct TestSaveArchiveEntry { public readonly string EntryId; public readonly string DirectoryPath; public readonly string SnapshotPath; public readonly string ThumbnailPath; public readonly SlotMeta Meta; public TestSaveArchiveEntry( string entryId, string directoryPath, string snapshotPath, string thumbnailPath, SlotMeta meta) { EntryId = entryId; DirectoryPath = directoryPath; SnapshotPath = snapshotPath; ThumbnailPath = thumbnailPath; Meta = meta; } } }