feat(save): 添加槽位类型与云存档扩展层

实现 P2 槽位/落盘层核心:
- SlotTypes:槽位常量、sidecar 元数据、UI 视图模型
- SlotFileSystem:槽位路径工具与原子写
- SlotManager:自动档、手动档、读档、继续游戏、P1 迁移
- SlotThumbnailCapture:存档截图 helper
- CloudSave:Steam/TapTap 云存档适配器接口与协调器

在 ConstRef 中补充槽位、截图、文件名常量。
This commit is contained in:
2026-06-12 17:40:57 +08:00
parent cc3e1dd138
commit 6fcf340230
14 changed files with 832 additions and 0 deletions
+120
View File
@@ -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
{
/// <summary>
/// 存档槽位目录与文件路径工具。
/// </summary>
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<int> 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));
}
}
/// <summary>
/// 槽位文件原子写工具。
/// <para>先写入临时文件,再重命名为最终文件,避免写入过程中崩溃导致存档损坏。</para>
/// </summary>
public static class SlotAtomicWriter
{
public static void WriteJson<T>(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);
}
}
}