using System; using System.IO; using System.Collections.Generic; using System.Threading.Tasks; using AibisDream.SaveSystem.CloudSave; using AibisDream.Utility; using Newtonsoft.Json; using UnityEngine; namespace AibisDream.SaveSystem { /// /// 存档槽位业务核心:管理自动档、手动档、读档与继续游戏。 /// public static class SlotManager { private static readonly object SlotWriteLock = new object(); /// 将快照与缩略图写入指定槽位。 public static void SaveToSlot(int slotIndex, SaveSnapshot snapshot, byte[] thumbnailPng) { WriteSlot(slotIndex, snapshot, thumbnailPng); } /// 将快照与缩略图写入自动档(slot 0)。 public static void SaveToAutoSlot(SaveSnapshot snapshot, byte[] thumbnailPng) { SaveToSlot(SlotIndex.Auto, snapshot, thumbnailPng); SetLatestSlotIndex(SlotIndex.Auto); CloudSaveManager.OnSlotWritten(SlotIndex.Auto); } /// /// 主线程 Capture 后,在后台线程序列化并写入自动档(slot 0)。 /// public static async Task SaveToAutoSlotAsync(SaveSnapshot snapshot, byte[] thumbnailPng) { await Task.Run(() => { var json = SnapshotPersistence.Serialize(snapshot); var meta = BuildSlotMeta(SlotIndex.Auto, snapshot); WriteSlotFiles(SlotIndex.Auto, json, meta, thumbnailPng); }); SetLatestSlotIndex(SlotIndex.Auto); CloudSaveManager.OnSlotWritten(SlotIndex.Auto); } /// 将当前自动档复制到指定手动档。 public static void CopyAutoToManual(int manualSlotIndex) { if (manualSlotIndex < SlotIndex.ManualStart || manualSlotIndex > SlotIndex.ManualEnd) { Debug.LogError($"[SlotManager] 手动槽位编号非法: {manualSlotIndex}"); return; } if (!SlotDirectory.Exists(SlotIndex.Auto)) { Debug.LogError("[SlotManager] 自动档不存在,无法复制手动档。"); return; } SlotDirectory.EnsureSlotDirectory(manualSlotIndex); var sourceDir = SlotDirectory.GetSlotPath(SlotIndex.Auto); var targetDir = SlotDirectory.GetSlotPath(manualSlotIndex); CopyFileIfExists( Path.Combine(sourceDir, $"{ConstRef.SaveSnapshotFileName}.json"), Path.Combine(targetDir, $"{ConstRef.SaveSnapshotFileName}.json")); CopyFileIfExists( Path.Combine(sourceDir, $"{ConstRef.SaveMetaFileName}.json"), Path.Combine(targetDir, $"{ConstRef.SaveMetaFileName}.json")); CopyFileIfExists( Path.Combine(sourceDir, $"{ConstRef.SaveThumbnailFileName}.png"), Path.Combine(targetDir, $"{ConstRef.SaveThumbnailFileName}.png")); // 修正 meta 中的 slotIndex 为手动档编号 var meta = LoadMeta(manualSlotIndex); if (meta != null) { meta.slotIndex = manualSlotIndex; WriteMeta(manualSlotIndex, meta); } SetLatestSlotIndex(manualSlotIndex); CloudSaveManager.OnSlotWritten(manualSlotIndex); Debug.Log($"[SlotManager] 已复制自动档到手动槽位 {manualSlotIndex}。"); } /// 删除指定槽位。 public static void DeleteSlot(int slotIndex) { if (slotIndex == SlotIndex.Auto) { // 自动档只清空内容,不删除目录 DeleteSlotFiles(slotIndex); Debug.Log("[SlotManager] 已清空自动档。"); return; } if (slotIndex < SlotIndex.ManualStart || slotIndex > SlotIndex.ManualEnd) { Debug.LogError($"[SlotManager] 槽位编号非法: {slotIndex}"); return; } var dir = SlotDirectory.GetSlotPath(slotIndex); if (Directory.Exists(dir)) { Directory.Delete(dir, recursive: true); Debug.Log($"[SlotManager] 已删除槽位 {slotIndex}。"); } CloudSaveManager.OnSlotWritten(slotIndex); } /// 读取槽位的完整快照。 public static SaveSnapshot LoadSnapshot(int slotIndex) { var path = SlotDirectory.GetSnapshotPath(slotIndex); if (!File.Exists(path)) { return null; } try { var json = File.ReadAllText(path); return SnapshotPersistence.Deserialize(json); } catch (Exception ex) { Debug.LogError($"[SlotManager] 读取槽位 {slotIndex} 快照失败: {ex.Message}"); return null; } } /// 是否存在可用的自动档(slot_0 meta + snapshot)。 public static bool HasAutoSave() { return SlotDirectory.Exists(SlotIndex.Auto) && File.Exists(SlotDirectory.GetSnapshotPath(SlotIndex.Auto)); } /// 读取槽位元数据。 public static SlotMeta LoadMeta(int slotIndex) { var path = SlotDirectory.GetMetaPath(slotIndex); if (!File.Exists(path)) { return null; } try { var json = File.ReadAllText(path); return JsonConvert.DeserializeObject(json); } catch (Exception ex) { Debug.LogWarning($"[SlotManager] 读取槽位 {slotIndex} meta 失败: {ex.Message}"); return null; } } /// 读取槽位缩略图字节。 public static byte[] LoadThumbnail(int slotIndex) { var path = SlotDirectory.GetThumbnailPath(slotIndex); if (!File.Exists(path)) { return null; } try { return File.ReadAllBytes(path); } catch (Exception ex) { Debug.LogWarning($"[SlotManager] 读取槽位 {slotIndex} 缩略图失败: {ex.Message}"); return null; } } /// 获取最近写入的槽位编号;不存在返回 null。 public static int? GetLatestSlotIndex() { var path = SlotDirectory.GetLatestSlotIndexPath(); if (!File.Exists(path)) { return null; } try { var json = File.ReadAllText(path); var record = JsonConvert.DeserializeObject(json); if (record != null && SlotDirectory.Exists(record.latestSlot)) { return record.latestSlot; } } catch (Exception ex) { Debug.LogWarning($"[SlotManager] 读取 latest_slot 失败: {ex.Message}"); } return null; } /// 更新最近写入槽位索引。 public static void SetLatestSlotIndex(int slotIndex) { var record = new LatestSlotRecord { latestSlot = slotIndex, updatedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") }; SlotAtomicWriter.WriteJson(record, SlotDirectory.GetLatestSlotIndexPath()); } /// 获取供 UI 展示的全部槽位视图模型。 public static SlotViewModel[] GetSlotViewModels() { var result = new List(); for (var i = SlotIndex.Auto; i <= SlotIndex.ManualEnd; i++) { result.Add(BuildViewModel(i)); } return result.ToArray(); } #region Private Helpers private static void WriteSlot(int slotIndex, SaveSnapshot snapshot, byte[] thumbnailPng) { var json = SnapshotPersistence.Serialize(snapshot); var meta = BuildSlotMeta(slotIndex, snapshot); WriteSlotFiles(slotIndex, json, meta, thumbnailPng); } private static SlotMeta BuildSlotMeta(int slotIndex, SaveSnapshot snapshot) { var summary = SaveSnapshotSummary.From(snapshot); return new SlotMeta { slotIndex = slotIndex, savedAt = snapshot.savedAt, sceneName = summary.SceneName, sceneSoName = summary.SceneSoName, yarnProjectId = summary.YarnProjectId, nodeName = snapshot.anchor?.nodeName, startDialogueOnRestore = snapshot.anchor?.startDialogueOnRestore ?? false, schemaVersion = snapshot.schemaVersion, gameVersion = snapshot.gameVersion, thumbnailFile = $"{ConstRef.SaveThumbnailFileName}.png" }; } private static void WriteSlotFiles(int slotIndex, string json, SlotMeta meta, byte[] thumbnailPng) { lock (SlotWriteLock) { SlotDirectory.EnsureSlotDirectory(slotIndex); SlotAtomicWriter.WriteText(json, SlotDirectory.GetSnapshotPath(slotIndex)); SlotAtomicWriter.WriteJson(meta, SlotDirectory.GetMetaPath(slotIndex)); if (thumbnailPng != null) { SlotAtomicWriter.WriteBytes(thumbnailPng, SlotDirectory.GetThumbnailPath(slotIndex)); } } } private static void WriteMeta(int slotIndex, SlotMeta meta) { SlotAtomicWriter.WriteJson(meta, SlotDirectory.GetMetaPath(slotIndex)); } private static void CopyFileIfExists(string sourcePath, string targetPath) { if (!File.Exists(sourcePath)) { return; } var dir = Path.GetDirectoryName(targetPath); if (!string.IsNullOrEmpty(dir)) { Directory.CreateDirectory(dir); } File.Copy(sourcePath, targetPath, overwrite: true); } private static void DeleteSlotFiles(int slotIndex) { var dir = SlotDirectory.GetSlotPath(slotIndex); if (!Directory.Exists(dir)) { return; } foreach (var file in Directory.GetFiles(dir)) { File.Delete(file); } } private static SlotViewModel BuildViewModel(int slotIndex) { var meta = LoadMeta(slotIndex); if (meta == null) { return new SlotViewModel( slotIndex, isEmpty: true, isAutoSlot: slotIndex == SlotIndex.Auto, savedAt: null, sceneName: null, sceneSoName: null, thumbnailPath: null); } var thumbnailPath = SlotDirectory.GetThumbnailPath(slotIndex); if (!File.Exists(thumbnailPath)) { thumbnailPath = null; } return new SlotViewModel( slotIndex, isEmpty: false, isAutoSlot: slotIndex == SlotIndex.Auto, savedAt: meta.savedAt, sceneName: meta.sceneName, sceneSoName: meta.sceneSoName, thumbnailPath: thumbnailPath); } [Serializable] private class LatestSlotRecord { public int latestSlot; public string updatedAt; } #endregion } }