feat(save): 添加槽位类型与云存档扩展层
实现 P2 槽位/落盘层核心: - SlotTypes:槽位常量、sidecar 元数据、UI 视图模型 - SlotFileSystem:槽位路径工具与原子写 - SlotManager:自动档、手动档、读档、继续游戏、P1 迁移 - SlotThumbnailCapture:存档截图 helper - CloudSave:Steam/TapTap 云存档适配器接口与协调器 在 ConstRef 中补充槽位、截图、文件名常量。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user