221 lines
8.3 KiB
C#
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;
|
|
}
|
|
}
|