Files
aibis-dream/Assets/Scripts/Utility/DevSaveCatalog.cs
T
2026-07-20 14:29:47 +08:00

221 lines
8.3 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using AibisDream.SaveSystem;
using AibisDream.Utility;
using Newtonsoft.Json;
namespace AibisDream
{
/// <summary>读取并严格校验项目内可提交的开发检查点清单。</summary>
public static class DevSaveCatalog
{
public const int CurrentVersion = 1;
private const string CatalogFileName = "catalog.json";
public static string LastError { get; private set; }
public static List<DevSaveSection> Load(string root)
{
LastError = null;
var result = new List<DevSaveSection>();
var catalogPath = Path.Combine(root ?? string.Empty, CatalogFileName);
if (string.IsNullOrWhiteSpace(root) || !File.Exists(catalogPath))
{
LastError = $"找不到测试存档清单:{catalogPath}";
return result;
}
DevSaveCatalogDto catalog;
try
{
catalog = JsonConvert.DeserializeObject<DevSaveCatalogDto>(File.ReadAllText(catalogPath));
}
catch (Exception ex)
{
LastError = $"清单 JSON 损坏:{ex.Message}";
return result;
}
if (catalog == null || catalog.catalogVersion != CurrentVersion)
{
LastError = $"不支持的清单版本:{catalog?.catalogVersion.ToString() ?? "none"}";
return result;
}
foreach (var sectionDto in (catalog.sections ?? new List<DevSaveCatalogSectionDto>())
.OrderBy(section => section.order))
{
if (string.IsNullOrWhiteSpace(sectionDto.id)) continue;
var entries = (sectionDto.entries ?? new List<DevSaveCatalogEntryDto>())
.OrderBy(entry => entry.order)
.Select(entry => BuildEntry(root, sectionDto, entry))
.ToList();
result.Add(new DevSaveSection(
sectionDto.id,
string.IsNullOrWhiteSpace(sectionDto.title) ? sectionDto.id : sectionDto.title,
entries));
}
return result;
}
private static DevSaveEntry BuildEntry(string root, DevSaveCatalogSectionDto section, DevSaveCatalogEntryDto dto)
{
var error = ValidateRelativePath(root, dto.path, out var entryDirectory);
var snapshotPath = error == null
? Path.Combine(entryDirectory, ConstRef.SaveSnapshotFileName)
: string.Empty;
var entry = new DevSaveEntry(
dto.order,
string.IsNullOrWhiteSpace(dto.label) ? dto.anchorNode : dto.label,
dto.anchorNode,
snapshotPath,
section.expectedSceneName,
section.expectedSceneSoName,
section.expectedYarnProjectId,
error);
entry.Validate();
return entry;
}
private static string ValidateRelativePath(string root, string relativePath, out string fullPath)
{
fullPath = null;
if (string.IsNullOrWhiteSpace(relativePath)) return "清单路径为空";
if (Path.IsPathRooted(relativePath)) return "清单路径必须是相对路径";
try
{
var rootFull = Path.GetFullPath(root).TrimEnd(Path.DirectorySeparatorChar)
+ Path.DirectorySeparatorChar;
fullPath = Path.GetFullPath(Path.Combine(root, relativePath.Replace('/', Path.DirectorySeparatorChar)));
if (!fullPath.StartsWith(rootFull, StringComparison.OrdinalIgnoreCase))
return "清单路径越过 DevSaveFiles 根目录";
return null;
}
catch (Exception ex)
{
return $"清单路径无效:{ex.Message}";
}
}
}
public sealed class DevSaveSection
{
public string Id { get; }
public string Title { get; }
public List<DevSaveEntry> Entries { get; }
public DevSaveSection(string id, string title, List<DevSaveEntry> entries)
{
Id = id;
Title = title;
Entries = entries ?? new List<DevSaveEntry>();
}
}
public sealed class DevSaveEntry
{
public int Order { get; }
public string Label { get; }
public string AnchorNode { get; }
public string SnapshotPathWithoutExtension { get; }
public bool IsValid => string.IsNullOrEmpty(Error);
public string Error { get; private set; }
public SaveSnapshot Snapshot { get; private set; }
private readonly string _expectedSceneName;
private readonly string _expectedSceneSoName;
private readonly string _expectedYarnProjectId;
public DevSaveEntry(
int order,
string label,
string anchorNode,
string snapshotPathWithoutExtension,
string expectedSceneName,
string expectedSceneSoName,
string expectedYarnProjectId,
string initialError = null)
{
Order = order;
Label = label ?? string.Empty;
AnchorNode = anchorNode ?? string.Empty;
SnapshotPathWithoutExtension = snapshotPathWithoutExtension;
_expectedSceneName = expectedSceneName;
_expectedSceneSoName = expectedSceneSoName;
_expectedYarnProjectId = expectedYarnProjectId;
Error = initialError;
}
public void Validate()
{
if (!string.IsNullOrEmpty(Error)) return;
if (string.IsNullOrEmpty(SnapshotPathWithoutExtension)
|| !File.Exists(SnapshotPathWithoutExtension + ".json"))
{
Error = "存档文件缺失";
return;
}
try
{
Snapshot = SnapshotPersistence.Load(SnapshotPathWithoutExtension);
}
catch (Exception ex)
{
Error = $"JSON 无法读取:{ex.Message}";
return;
}
if (Snapshot == null || Snapshot.schemaVersion != SaveSnapshotSchema.CurrentVersion)
Error = $"存档版本不兼容:{Snapshot?.schemaVersion.ToString() ?? "none"}";
else if (Snapshot.scene == null || string.IsNullOrWhiteSpace(Snapshot.scene.sceneName))
Error = "存档缺少场景";
else if (Snapshot.anchor == null || string.IsNullOrWhiteSpace(Snapshot.anchor.nodeName))
Error = "存档缺少 Yarn 锚点";
else if (!Matches(_expectedSceneName, Snapshot.scene.sceneName))
Error = $"场景不匹配:{Snapshot.scene.sceneName}";
else if (!Matches(_expectedSceneSoName, Snapshot.anchor.sceneSoName))
Error = $"TalkSceneSO 不匹配:{Snapshot.anchor.sceneSoName}";
else if (!Matches(_expectedYarnProjectId, Snapshot.anchor.yarnProjectId))
Error = $"YarnProject 不匹配:{Snapshot.anchor.yarnProjectId}";
else if (!string.IsNullOrWhiteSpace(AnchorNode)
&& !string.Equals(AnchorNode, Snapshot.anchor.nodeName, StringComparison.Ordinal))
Error = $"节点不匹配:{Snapshot.anchor.nodeName}";
}
private static bool Matches(string expected, string actual) =>
string.IsNullOrWhiteSpace(expected) || string.Equals(expected, actual, StringComparison.Ordinal);
}
[Serializable]
public sealed class DevSaveCatalogDto
{
public int catalogVersion = DevSaveCatalog.CurrentVersion;
public List<DevSaveCatalogSectionDto> sections = new();
}
[Serializable]
public sealed class DevSaveCatalogSectionDto
{
public int order;
public string id;
public string title;
public string expectedSceneName;
public string expectedSceneSoName;
public string expectedYarnProjectId;
public List<DevSaveCatalogEntryDto> entries = new();
}
[Serializable]
public sealed class DevSaveCatalogEntryDto
{
public int order;
public string label;
public string path;
public string anchorNode;
}
}