diff --git a/Assets/Scripts/AssetRefs/ConstRef.cs b/Assets/Scripts/AssetRefs/ConstRef.cs index b4c2dfb6e..fbca620f5 100644 --- a/Assets/Scripts/AssetRefs/ConstRef.cs +++ b/Assets/Scripts/AssetRefs/ConstRef.cs @@ -23,6 +23,16 @@ namespace AibisDream.Utility public static readonly string SaveFilePath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "saves"); + /// P2 槽位相关常量。 + public const int SaveSlotCount = 8; + public const int SaveThumbnailWidth = 640; + public const int SaveThumbnailHeight = 360; + public const string SaveSlotDirPrefix = "slot_"; + public const string SaveSnapshotFileName = "snapshot"; + public const string SaveMetaFileName = "meta"; + public const string SaveThumbnailFileName = "thumbnail"; + public const string LatestSlotFileName = "latest_slot"; + /// P1 快照层自测落盘路径(P2 槽位层接管后迁移)。 public static readonly string SnapshotTestPath = Path.Combine(Application.persistentDataPath, "AllOurBrokenParts", "snapshot_test"); diff --git a/Assets/Scripts/SaveSystem/CloudSave.meta b/Assets/Scripts/SaveSystem/CloudSave.meta new file mode 100644 index 000000000..308741781 --- /dev/null +++ b/Assets/Scripts/SaveSystem/CloudSave.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: 520cc163c88e4c00895fe201004b4384 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/CloudSave/CloudSaveManager.cs b/Assets/Scripts/SaveSystem/CloudSave/CloudSaveManager.cs new file mode 100644 index 000000000..d8db81960 --- /dev/null +++ b/Assets/Scripts/SaveSystem/CloudSave/CloudSaveManager.cs @@ -0,0 +1,102 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using UnityEngine; + +namespace AibisDream.SaveSystem.CloudSave +{ + /// + /// 云存档协调器:负责选择当前平台的 backend,并在槽位写入/读取后触发同步钩子。 + /// + /// P2 不实现真实云同步逻辑,仅保留扩展点。 + /// + /// + public static class CloudSaveManager + { + private static readonly List Backends = new(); + + /// 注册一个云存档后端。应在游戏启动时调用一次。 + public static void RegisterBackend(ICloudSaveBackend backend) + { + if (backend == null) + { + return; + } + + Backends.Add(backend); + } + + /// 清空已注册后端(主要用于测试)。 + public static void ClearBackends() + { + Backends.Clear(); + } + + /// 槽位写入后调用:触发所有可用后端的异步上传。 + public static void OnSlotWritten(int slotIndex) + { + _ = UploadSlotAsync(slotIndex); + } + + /// 槽位读取前调用:可触发下载(P2 默认不自动下载,避免覆盖本地进度)。 + public static void OnSlotRestored(int slotIndex) + { + // P2 占位:未来可在此触发冲突检测与下载。 + } + + /// 手动触发指定槽位的云同步上传。 + public static async Task UploadSlotAsync(int slotIndex) + { + var filePaths = CollectSlotFilePaths(slotIndex); + if (filePaths.Count == 0) + { + return; + } + + foreach (var backend in Backends) + { + if (!backend.IsAvailable) + { + continue; + } + + try + { + await backend.UploadSlotAsync(slotIndex, filePaths.ToArray()); + Debug.Log($"[CloudSaveManager] {backend.PlatformName} 上传槽位 {slotIndex} 完成。"); + } + catch (Exception ex) + { + Debug.LogWarning($"[CloudSaveManager] {backend.PlatformName} 上传槽位 {slotIndex} 失败: {ex.Message}"); + } + } + } + + /// 收集槽位下需要同步的文件路径。 + private static List CollectSlotFilePaths(int slotIndex) + { + var paths = new List(); + + var snapshotPath = SlotDirectory.GetSnapshotPath(slotIndex); + if (File.Exists(snapshotPath)) + { + paths.Add(snapshotPath); + } + + var metaPath = SlotDirectory.GetMetaPath(slotIndex); + if (File.Exists(metaPath)) + { + paths.Add(metaPath); + } + + var thumbnailPath = SlotDirectory.GetThumbnailPath(slotIndex); + if (File.Exists(thumbnailPath)) + { + paths.Add(thumbnailPath); + } + + return paths; + } + } +} diff --git a/Assets/Scripts/SaveSystem/CloudSave/CloudSaveManager.cs.meta b/Assets/Scripts/SaveSystem/CloudSave/CloudSaveManager.cs.meta new file mode 100644 index 000000000..5d33b3aa0 --- /dev/null +++ b/Assets/Scripts/SaveSystem/CloudSave/CloudSaveManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 87e980f299ac4341947c076cff16dc81 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/CloudSave/ICloudSaveBackend.cs b/Assets/Scripts/SaveSystem/CloudSave/ICloudSaveBackend.cs new file mode 100644 index 000000000..867440ef2 --- /dev/null +++ b/Assets/Scripts/SaveSystem/CloudSave/ICloudSaveBackend.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading.Tasks; + +namespace AibisDream.SaveSystem.CloudSave +{ + /// + /// 云存档后端适配器接口。 + /// + /// P2 仅定义接口与本地 no-op 实现,未来 Steamworks.NET / TapTap TDS SDK 通过实现此接口接入。 + /// 每个槽位在本地对应一组文件(snapshot.json / meta.json / thumbnail.png), + /// backend 可选择逐文件上传,或将整组文件打包后上传。 + /// + /// + public interface ICloudSaveBackend + { + /// 平台名称,用于日志。 + string PlatformName { get; } + + /// 当前平台是否可用。 + bool IsAvailable { get; } + + /// 上传槽位文件集。 + /// 槽位编号。 + /// 该槽位下需要同步的文件绝对路径。 + Task UploadSlotAsync(int slotIndex, string[] filePaths); + + /// 下载槽位文件集到目标目录。 + Task DownloadSlotAsync(int slotIndex, string targetDirectory); + + /// 删除云端槽位。 + Task DeleteSlotAsync(int slotIndex); + + /// 获取云端该槽位的时间戳,用于冲突检测。 + /// 远程保存时间;不存在返回 null。 + Task GetRemoteTimestampAsync(int slotIndex); + } +} diff --git a/Assets/Scripts/SaveSystem/CloudSave/ICloudSaveBackend.cs.meta b/Assets/Scripts/SaveSystem/CloudSave/ICloudSaveBackend.cs.meta new file mode 100644 index 000000000..71ddf8481 --- /dev/null +++ b/Assets/Scripts/SaveSystem/CloudSave/ICloudSaveBackend.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 783cf442f0484df9a89c7841604c0b49 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/SlotFileSystem.cs b/Assets/Scripts/SaveSystem/SlotFileSystem.cs new file mode 100644 index 000000000..4083eb4b6 --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotFileSystem.cs @@ -0,0 +1,120 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using AibisDream.Utility; +using Newtonsoft.Json; +using UnityEngine; + +namespace AibisDream.SaveSystem +{ + /// + /// 存档槽位目录与文件路径工具。 + /// + public static class SlotDirectory + { + public static string GetSlotPath(int slotIndex) + { + return Path.Combine(ConstRef.SaveFilePath, $"{ConstRef.SaveSlotDirPrefix}{slotIndex}"); + } + + public static string GetSnapshotPath(int slotIndex) + { + return Path.Combine(GetSlotPath(slotIndex), $"{ConstRef.SaveSnapshotFileName}.json"); + } + + public static string GetMetaPath(int slotIndex) + { + return Path.Combine(GetSlotPath(slotIndex), $"{ConstRef.SaveMetaFileName}.json"); + } + + public static string GetThumbnailPath(int slotIndex) + { + return Path.Combine(GetSlotPath(slotIndex), $"{ConstRef.SaveThumbnailFileName}.png"); + } + + public static string GetLatestSlotIndexPath() + { + return Path.Combine(ConstRef.SaveFilePath, $"{ConstRef.LatestSlotFileName}.json"); + } + + public static bool Exists(int slotIndex) + { + return File.Exists(GetMetaPath(slotIndex)); + } + + public static IEnumerable EnumerateExistingSlots() + { + if (!Directory.Exists(ConstRef.SaveFilePath)) + { + yield break; + } + + for (var i = SlotIndex.Auto; i <= SlotIndex.ManualEnd; i++) + { + if (Exists(i)) + { + yield return i; + } + } + } + + public static void EnsureSlotDirectory(int slotIndex) + { + Directory.CreateDirectory(GetSlotPath(slotIndex)); + } + } + + /// + /// 槽位文件原子写工具。 + /// 先写入临时文件,再重命名为最终文件,避免写入过程中崩溃导致存档损坏。 + /// + public static class SlotAtomicWriter + { + public static void WriteJson(T obj, string finalPath) + { + var json = JsonConvert.SerializeObject(obj, Formatting.Indented); + WriteText(json, finalPath); + } + + public static void WriteText(string text, string finalPath) + { + var tempPath = finalPath + ".tmp"; + var dir = Path.GetDirectoryName(finalPath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + File.WriteAllText(tempPath, text); + CommitTempFile(tempPath, finalPath); + } + + public static void WriteBytes(byte[] data, string finalPath) + { + if (data == null) + { + return; + } + + var tempPath = finalPath + ".tmp"; + var dir = Path.GetDirectoryName(finalPath); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + File.WriteAllBytes(tempPath, data); + CommitTempFile(tempPath, finalPath); + } + + private static void CommitTempFile(string tempPath, string finalPath) + { + if (File.Exists(finalPath)) + { + File.Delete(finalPath); + } + + File.Move(tempPath, finalPath); + } + } +} diff --git a/Assets/Scripts/SaveSystem/SlotFileSystem.cs.meta b/Assets/Scripts/SaveSystem/SlotFileSystem.cs.meta new file mode 100644 index 000000000..0bf2766fd --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotFileSystem.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d819a034669842418c5e6b63b4a67cd3 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/SlotManager.cs b/Assets/Scripts/SaveSystem/SlotManager.cs new file mode 100644 index 000000000..78f5373f9 --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotManager.cs @@ -0,0 +1,381 @@ +using System; +using System.IO; +using System.Collections.Generic; +using AibisDream.Utility; +using Newtonsoft.Json; +using UnityEngine; + +namespace AibisDream.SaveSystem +{ + /// + /// 存档槽位业务核心:管理自动档、手动档、读档、继续游戏与 P1 迁移。 + /// + public static class SlotManager + { + private static bool _migrated; + + /// 将快照与缩略图写入指定槽位。 + public static void SaveToSlot(int slotIndex, SaveSnapshot snapshot, byte[] thumbnailPng) + { + EnsureMigrated(); + 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); + } + + /// 将当前自动档复制到指定手动档。 + public static void CopyAutoToManual(int manualSlotIndex) + { + EnsureMigrated(); + + 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) + { + EnsureMigrated(); + + 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) + { + EnsureMigrated(); + + 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; + } + } + + /// 读取槽位元数据。 + 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() + { + EnsureMigrated(); + + 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() + { + EnsureMigrated(); + + 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) + { + SlotDirectory.EnsureSlotDirectory(slotIndex); + + // snapshot.json + var snapshotPath = SlotDirectory.GetSnapshotPath(slotIndex); + var json = SnapshotPersistence.Serialize(snapshot); + SlotAtomicWriter.WriteText(json, snapshotPath); + + // meta.json + var summary = SaveSnapshotSummary.From(snapshot); + var meta = new SlotMeta + { + slotIndex = slotIndex, + savedAt = snapshot.savedAt, + sceneName = summary.SceneName, + sceneSoName = summary.SceneSoName, + yarnProjectId = summary.YarnProjectId, + schemaVersion = snapshot.schemaVersion, + gameVersion = snapshot.gameVersion, + thumbnailFile = $"{ConstRef.SaveThumbnailFileName}.png" + }; + WriteMeta(slotIndex, meta); + + // thumbnail.png + 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); + } + + private static void EnsureMigrated() + { + if (_migrated) + { + return; + } + + _migrated = true; + TryMigrateP1TestSnapshot(); + } + + private static void TryMigrateP1TestSnapshot() + { + var testPath = Path.Combine(ConstRef.SnapshotTestPath, $"{ConstRef.SnapshotTestFileName}.json"); + if (!File.Exists(testPath)) + { + return; + } + + if (SlotDirectory.Exists(SlotIndex.Auto)) + { + return; + } + + try + { + var snapshot = SnapshotPersistence.Load(testPath); + if (snapshot == null) + { + return; + } + + SaveToAutoSlot(snapshot, null); + + var migratedPath = testPath + ".migrated"; + if (File.Exists(migratedPath)) + { + File.Delete(migratedPath); + } + File.Move(testPath, migratedPath); + + Debug.Log($"[SlotManager] 已将 P1 测试存档从 {testPath} 迁移到 slot_0。"); + } + catch (Exception ex) + { + Debug.LogWarning($"[SlotManager] P1 测试存档迁移失败: {ex.Message}"); + } + } + + [Serializable] + private class LatestSlotRecord + { + public int latestSlot; + public string updatedAt; + } + + #endregion + } +} diff --git a/Assets/Scripts/SaveSystem/SlotManager.cs.meta b/Assets/Scripts/SaveSystem/SlotManager.cs.meta new file mode 100644 index 000000000..14cb7c7d8 --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotManager.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: af4d8624a0d645ce9b07f73024290cd6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/SlotThumbnailCapture.cs b/Assets/Scripts/SaveSystem/SlotThumbnailCapture.cs new file mode 100644 index 000000000..3d4b3561d --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotThumbnailCapture.cs @@ -0,0 +1,48 @@ +using AibisDream.Utility; +using UnityEngine; + +namespace AibisDream.SaveSystem +{ + /// + /// 存档槽位缩略图捕获工具。 + /// + /// 使用主相机渲染到 RenderTexture 后读像素并编码为 PNG。 + /// 捕获失败时安全返回 null,不会阻塞存档流程。 + /// + /// + public static class SlotThumbnailCapture + { + /// 捕获当前画面的 PNG 字节数组。 + public static byte[] CapturePng() + { + var cam = Camera.main; + if (cam == null) + { + Debug.LogWarning("[SlotThumbnailCapture] 未找到 Camera.main,跳过截图。"); + return null; + } + + var width = ConstRef.SaveThumbnailWidth; + var height = ConstRef.SaveThumbnailHeight; + + var rt = RenderTexture.GetTemporary(width, height, 24, RenderTextureFormat.Default); + var prevTarget = cam.targetTexture; + cam.targetTexture = rt; + cam.Render(); + cam.targetTexture = prevTarget; + + RenderTexture.active = rt; + var tex = new Texture2D(width, height, TextureFormat.RGB24, false); + tex.ReadPixels(new Rect(0, 0, width, height), 0, 0); + tex.Apply(); + RenderTexture.active = null; + + var bytes = tex.EncodeToPNG(); + + Object.Destroy(tex); + RenderTexture.ReleaseTemporary(rt); + + return bytes; + } + } +} diff --git a/Assets/Scripts/SaveSystem/SlotThumbnailCapture.cs.meta b/Assets/Scripts/SaveSystem/SlotThumbnailCapture.cs.meta new file mode 100644 index 000000000..41a496b69 --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotThumbnailCapture.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 63f375c0f57444058bef24100d912ece +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/Scripts/SaveSystem/SlotTypes.cs b/Assets/Scripts/SaveSystem/SlotTypes.cs new file mode 100644 index 000000000..b7c3ae0bf --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotTypes.cs @@ -0,0 +1,60 @@ +using System; + +namespace AibisDream.SaveSystem +{ + /// + /// 存档槽位编号常量。 + /// slot_0 为自动档;slot_1..slot_8 为手动档。 + /// + public static class SlotIndex + { + public const int Auto = 0; + public const int ManualStart = 1; + public const int ManualEnd = 8; + public const int ManualSlotCount = ManualEnd - ManualStart + 1; + } + + /// + /// 单个存档槽位的轻量 sidecar 元数据。 + /// 与完整 分离,避免 UI 枚举槽位时反复解析大 JSON。 + /// + [Serializable] + public class SlotMeta + { + public int slotIndex; + public string savedAt; + public string sceneName; + public string sceneSoName; + public string yarnProjectId; + public int schemaVersion; + public string gameVersion; + public string thumbnailFile; + } + + /// + /// 存档槽位 UI 展示用的只读数据契约。 + /// 产出,供 P6 的 SlotPanel 消费。 + /// + public readonly struct SlotViewModel + { + public readonly int SlotIndex; + public readonly bool IsEmpty; + public readonly bool IsAutoSlot; + public readonly string SavedAt; + public readonly string SceneName; + public readonly string SceneSoName; + public readonly string ThumbnailPath; + + public SlotViewModel(int slotIndex, bool isEmpty, bool isAutoSlot, string savedAt, + string sceneName, string sceneSoName, string thumbnailPath) + { + SlotIndex = slotIndex; + IsEmpty = isEmpty; + IsAutoSlot = isAutoSlot; + SavedAt = savedAt; + SceneName = sceneName; + SceneSoName = sceneSoName; + ThumbnailPath = thumbnailPath; + } + } +} diff --git a/Assets/Scripts/SaveSystem/SlotTypes.cs.meta b/Assets/Scripts/SaveSystem/SlotTypes.cs.meta new file mode 100644 index 000000000..e38018807 --- /dev/null +++ b/Assets/Scripts/SaveSystem/SlotTypes.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3fec09cdbac7484ba1f6c6c361aae980 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: