Files
aibis-dream/Assets/Scripts/SaveSystem/SlotManager.cs
T

410 lines
13 KiB
C#

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
{
/// <summary>
/// 存档槽位业务核心:管理自动档、手动档、读档、继续游戏与 P1 迁移。
/// </summary>
public static class SlotManager
{
private static bool _migrated;
private static readonly object SlotWriteLock = new object();
/// <summary>将快照与缩略图写入指定槽位。</summary>
public static void SaveToSlot(int slotIndex, SaveSnapshot snapshot, byte[] thumbnailPng)
{
EnsureMigrated();
WriteSlot(slotIndex, snapshot, thumbnailPng);
}
/// <summary>将快照与缩略图写入自动档(slot 0)。</summary>
public static void SaveToAutoSlot(SaveSnapshot snapshot, byte[] thumbnailPng)
{
SaveToSlot(SlotIndex.Auto, snapshot, thumbnailPng);
SetLatestSlotIndex(SlotIndex.Auto);
CloudSaveManager.OnSlotWritten(SlotIndex.Auto);
}
/// <summary>
/// 主线程完成序列化后,在后台线程写入自动档(slot 0)。
/// 调用方须已在主线程完成 <see cref="SnapshotService.Capture"/>。
/// </summary>
public static async Task SaveToAutoSlotAsync(SaveSnapshot snapshot, byte[] thumbnailPng)
{
EnsureMigrated();
var json = SnapshotPersistence.Serialize(snapshot);
var meta = BuildSlotMeta(SlotIndex.Auto, snapshot);
await Task.Run(() => WriteSlotFiles(SlotIndex.Auto, json, meta, thumbnailPng));
SetLatestSlotIndex(SlotIndex.Auto);
CloudSaveManager.OnSlotWritten(SlotIndex.Auto);
}
/// <summary>将当前自动档复制到指定手动档。</summary>
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}。");
}
/// <summary>删除指定槽位。</summary>
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);
}
/// <summary>读取槽位的完整快照。</summary>
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;
}
}
/// <summary>读取槽位元数据。</summary>
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<SlotMeta>(json);
}
catch (Exception ex)
{
Debug.LogWarning($"[SlotManager] 读取槽位 {slotIndex} meta 失败: {ex.Message}");
return null;
}
}
/// <summary>读取槽位缩略图字节。</summary>
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;
}
}
/// <summary>获取最近写入的槽位编号;不存在返回 null。</summary>
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<LatestSlotRecord>(json);
if (record != null && SlotDirectory.Exists(record.latestSlot))
{
return record.latestSlot;
}
}
catch (Exception ex)
{
Debug.LogWarning($"[SlotManager] 读取 latest_slot 失败: {ex.Message}");
}
return null;
}
/// <summary>更新最近写入槽位索引。</summary>
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());
}
/// <summary>获取供 UI 展示的全部槽位视图模型。</summary>
public static SlotViewModel[] GetSlotViewModels()
{
EnsureMigrated();
var result = new List<SlotViewModel>();
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,
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);
}
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
}
}