219 lines
7.2 KiB
C#
219 lines
7.2 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using AibisDream.Utility;
|
|
using Newtonsoft.Json;
|
|
using UnityEngine;
|
|
|
|
namespace AibisDream.SaveSystem
|
|
{
|
|
/// <summary>
|
|
/// 测试存档模式:每次自动存档写入 <see cref="ConstRef.TestAutoSaveArchivePath"/> 下的独立子目录。
|
|
/// 开启后不写入 slot_0,存多少次就保留多少份,与正式 demo_saves/ 完全隔离。
|
|
/// </summary>
|
|
public static class TestSaveArchive
|
|
{
|
|
public const string EditorPrefsEnabledKey = "AibisDream.SaveSystem.TestArchiveEnabled";
|
|
|
|
/// <summary>测试存档模式是否开启(Editor 下由 EditorPrefs 控制,构建产物恒为 false)。</summary>
|
|
public static bool IsEnabled
|
|
{
|
|
get
|
|
{
|
|
#if UNITY_EDITOR
|
|
return UnityEditor.EditorPrefs.GetBool(EditorPrefsEnabledKey, true);
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private static readonly object ArchiveWriteLock = new object();
|
|
|
|
/// <summary>将快照写入 testsavs 新条目,返回文件夹名。每次调用均新建,不覆盖已有条目。</summary>
|
|
public static string Archive(SaveSnapshot snapshot, byte[] thumbnailPng)
|
|
{
|
|
if (snapshot == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var entryId = BuildUniqueEntryId(snapshot);
|
|
var entryDir = Path.Combine(ConstRef.TestAutoSaveArchivePath, entryId);
|
|
Directory.CreateDirectory(entryDir);
|
|
|
|
var json = SnapshotPersistence.Serialize(snapshot);
|
|
var meta = BuildMeta(entryId, snapshot);
|
|
|
|
lock (ArchiveWriteLock)
|
|
{
|
|
SlotAtomicWriter.WriteText(json, Path.Combine(entryDir, $"{ConstRef.SaveSnapshotFileName}.json"));
|
|
SlotAtomicWriter.WriteJson(meta, Path.Combine(entryDir, $"{ConstRef.SaveMetaFileName}.json"));
|
|
|
|
if (thumbnailPng != null)
|
|
{
|
|
SlotAtomicWriter.WriteBytes(
|
|
thumbnailPng,
|
|
Path.Combine(entryDir, $"{ConstRef.SaveThumbnailFileName}.png"));
|
|
}
|
|
}
|
|
|
|
Debug.Log($"[TestSaveArchive] 已归档测试存档: {entryId}");
|
|
return entryId;
|
|
}
|
|
|
|
public static List<TestSaveArchiveEntry> ListEntries()
|
|
{
|
|
var result = new List<TestSaveArchiveEntry>();
|
|
if (!Directory.Exists(ConstRef.TestAutoSaveArchivePath))
|
|
{
|
|
return result;
|
|
}
|
|
|
|
foreach (var dir in Directory.GetDirectories(ConstRef.TestAutoSaveArchivePath))
|
|
{
|
|
var entryId = Path.GetFileName(dir);
|
|
var snapshotPath = Path.Combine(dir, $"{ConstRef.SaveSnapshotFileName}.json");
|
|
if (!File.Exists(snapshotPath))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
var metaPath = Path.Combine(dir, $"{ConstRef.SaveMetaFileName}.json");
|
|
var thumbPath = Path.Combine(dir, $"{ConstRef.SaveThumbnailFileName}.png");
|
|
|
|
SlotMeta meta = null;
|
|
if (File.Exists(metaPath))
|
|
{
|
|
try
|
|
{
|
|
meta = JsonConvert.DeserializeObject<SlotMeta>(File.ReadAllText(metaPath));
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Debug.LogWarning($"[TestSaveArchive] 读取 meta 失败 ({entryId}): {ex.Message}");
|
|
}
|
|
}
|
|
|
|
result.Add(new TestSaveArchiveEntry(
|
|
entryId,
|
|
dir,
|
|
snapshotPath,
|
|
File.Exists(thumbPath) ? thumbPath : null,
|
|
meta));
|
|
}
|
|
|
|
return result
|
|
.OrderByDescending(e => e.Meta?.savedAt ?? string.Empty)
|
|
.ThenByDescending(e => e.EntryId, StringComparer.Ordinal)
|
|
.ToList();
|
|
}
|
|
|
|
public static void DeleteEntry(string entryId)
|
|
{
|
|
if (string.IsNullOrEmpty(entryId))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dir = Path.Combine(ConstRef.TestAutoSaveArchivePath, entryId);
|
|
if (Directory.Exists(dir))
|
|
{
|
|
Directory.Delete(dir, recursive: true);
|
|
}
|
|
}
|
|
|
|
public static void ClearAll()
|
|
{
|
|
if (!Directory.Exists(ConstRef.TestAutoSaveArchivePath))
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var dir in Directory.GetDirectories(ConstRef.TestAutoSaveArchivePath))
|
|
{
|
|
Directory.Delete(dir, recursive: true);
|
|
}
|
|
}
|
|
|
|
private static string BuildUniqueEntryId(SaveSnapshot snapshot)
|
|
{
|
|
var baseId = BuildEntryId(snapshot);
|
|
var entryId = baseId;
|
|
var suffix = 2;
|
|
|
|
while (Directory.Exists(Path.Combine(ConstRef.TestAutoSaveArchivePath, entryId)))
|
|
{
|
|
entryId = $"{baseId}_{suffix}";
|
|
suffix++;
|
|
}
|
|
|
|
return entryId;
|
|
}
|
|
|
|
private static string BuildEntryId(SaveSnapshot snapshot)
|
|
{
|
|
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss_fff");
|
|
var nodeName = snapshot.anchor?.nodeName;
|
|
var safeNode = string.IsNullOrEmpty(nodeName) ? "no_node" : SanitizeEntryName(nodeName);
|
|
return $"{timestamp}_{safeNode}";
|
|
}
|
|
|
|
private static SlotMeta BuildMeta(string entryId, SaveSnapshot snapshot)
|
|
{
|
|
var summary = SaveSnapshotSummary.From(snapshot);
|
|
return new SlotMeta
|
|
{
|
|
slotIndex = -1,
|
|
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 string SanitizeEntryName(string name)
|
|
{
|
|
foreach (var c in Path.GetInvalidFileNameChars())
|
|
{
|
|
name = name.Replace(c, '_');
|
|
}
|
|
|
|
if (name.Length > 48)
|
|
{
|
|
name = name.Substring(0, 48);
|
|
}
|
|
|
|
return string.IsNullOrWhiteSpace(name) ? "no_node" : name;
|
|
}
|
|
}
|
|
|
|
public readonly struct TestSaveArchiveEntry
|
|
{
|
|
public readonly string EntryId;
|
|
public readonly string DirectoryPath;
|
|
public readonly string SnapshotPath;
|
|
public readonly string ThumbnailPath;
|
|
public readonly SlotMeta Meta;
|
|
|
|
public TestSaveArchiveEntry(
|
|
string entryId,
|
|
string directoryPath,
|
|
string snapshotPath,
|
|
string thumbnailPath,
|
|
SlotMeta meta)
|
|
{
|
|
EntryId = entryId;
|
|
DirectoryPath = directoryPath;
|
|
SnapshotPath = snapshotPath;
|
|
ThumbnailPath = thumbnailPath;
|
|
Meta = meta;
|
|
}
|
|
}
|
|
}
|