384 lines
12 KiB
C#
384 lines
12 KiB
C#
using System;
|
|
using System.IO;
|
|
using System.Collections.Generic;
|
|
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;
|
|
|
|
/// <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>将当前自动档复制到指定手动档。</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)
|
|
{
|
|
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,
|
|
nodeName = snapshot.anchor?.nodeName,
|
|
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
|
|
}
|
|
}
|