using System; using System.Collections.Generic; using System.IO; using System.Linq; using AibisDream.SaveSystem; using AibisDream.Utility; using Newtonsoft.Json; namespace AibisDream { /// 读取并严格校验项目内可提交的开发检查点清单。 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 Load(string root) { LastError = null; var result = new List(); 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(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()) .OrderBy(section => section.order)) { if (string.IsNullOrWhiteSpace(sectionDto.id)) continue; var entries = (sectionDto.entries ?? new List()) .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 Entries { get; } public DevSaveSection(string id, string title, List entries) { Id = id; Title = title; Entries = entries ?? new List(); } } 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 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 entries = new(); } [Serializable] public sealed class DevSaveCatalogEntryDto { public int order; public string label; public string path; public string anchorNode; } }