#if UNITY_EDITOR || DEVELOPMENT_BUILD using System; using System.Collections.Generic; using System.Linq; namespace AibisDream.SaveSystem { public enum TestSaveCoverageRowKind { Recorded, Missing, Invalid } public sealed class TestSaveCoverageRow { public TestSaveCoverageRowKind Kind { get; internal set; } public string NodeName { get; internal set; } public TestSaveEntry Entry { get; internal set; } public long SortOrder { get; internal set; } } public sealed class TestSaveChapterCoverage { public string ChapterId { get; internal set; } public string Title { get; internal set; } public int ChapterOrder { get; internal set; } public int RecordedCount { get; internal set; } public int ExpectedCount { get; internal set; } public IReadOnlyList Rows { get; internal set; } = Array.Empty(); public bool IsOrphanGroup { get; internal set; } } public static class TestSaveCoverage { public const string OrphanChapterId = "__invalid_or_orphan__"; public static IReadOnlyList Build( IReadOnlyList chapters, IReadOnlyList entries) { chapters ??= Array.Empty(); entries ??= Array.Empty(); var result = new List(); var consumed = new HashSet(); for (var chapterIndex = 0; chapterIndex < chapters.Count; chapterIndex++) { var chapter = chapters[chapterIndex]; if (chapter == null) continue; var expectedNodes = GetExpectedNodes(chapter); var chapterEntries = entries .Where(item => item.Meta != null && string.Equals(item.Meta.sceneSoName, chapter.name, StringComparison.Ordinal) && string.Equals( item.Meta.yarnProjectId, chapter.yarnProject?.name, StringComparison.Ordinal)) .ToArray(); foreach (var entry in chapterEntries) consumed.Add(entry); var validByNode = chapterEntries .Where(item => item.IsValid) .GroupBy(item => item.NodeName, StringComparer.Ordinal) .ToDictionary( group => group.Key, group => group.OrderBy(item => item.Meta.firstSeenOrder).First(), StringComparer.Ordinal); var rows = new List(); foreach (var entry in validByNode.Values.OrderBy(item => item.Meta.firstSeenOrder)) { rows.Add(new TestSaveCoverageRow { Kind = TestSaveCoverageRowKind.Recorded, NodeName = entry.NodeName, Entry = entry, SortOrder = entry.Meta.firstSeenOrder }); } foreach (var nodeName in expectedNodes .Where(node => !validByNode.ContainsKey(node)) .OrderBy(node => node, StringComparer.OrdinalIgnoreCase)) { rows.Add(new TestSaveCoverageRow { Kind = TestSaveCoverageRowKind.Missing, NodeName = nodeName, SortOrder = long.MaxValue }); } foreach (var entry in chapterEntries.Where(item => !item.IsValid) .OrderBy(item => item.Meta?.firstSeenOrder ?? long.MaxValue)) { rows.Add(new TestSaveCoverageRow { Kind = TestSaveCoverageRowKind.Invalid, NodeName = entry.NodeName ?? "(未知节点)", Entry = entry, SortOrder = entry.Meta?.firstSeenOrder ?? long.MaxValue }); } result.Add(new TestSaveChapterCoverage { ChapterId = chapter.name, Title = string.IsNullOrWhiteSpace(chapter.title) ? chapter.name : chapter.title, ChapterOrder = chapterIndex, RecordedCount = validByNode.Keys.Count(expectedNodes.Contains), ExpectedCount = expectedNodes.Count, Rows = rows }); } var orphanEntries = entries.Where(item => !consumed.Contains(item)).ToArray(); if (orphanEntries.Length > 0) { result.Add(new TestSaveChapterCoverage { ChapterId = OrphanChapterId, Title = "无效 / 已脱离当前流程", ChapterOrder = int.MaxValue, RecordedCount = 0, ExpectedCount = 0, IsOrphanGroup = true, Rows = orphanEntries .OrderBy(item => item.Meta?.firstSeenOrder ?? long.MaxValue) .Select(item => new TestSaveCoverageRow { Kind = TestSaveCoverageRowKind.Invalid, NodeName = item.NodeName ?? "(未知节点)", Entry = item, SortOrder = item.Meta?.firstSeenOrder ?? long.MaxValue }) .ToArray() }); } return result; } public static HashSet GetExpectedNodes(TalkSceneSO chapter) { var result = new HashSet(StringComparer.Ordinal); var project = chapter?.yarnProject; if (project?.Program?.Nodes == null) return result; foreach (var pair in project.Program.Nodes) { var node = pair.Value; if (node == null || node.Name.StartsWith("$", StringComparison.Ordinal)) continue; var tagsValue = node.Headers .FirstOrDefault(header => string.Equals(header.Key, "tags", StringComparison.OrdinalIgnoreCase)) ?.Value; var tags = string.IsNullOrWhiteSpace(tagsValue) ? Array.Empty() : tagsValue.Split(Array.Empty(), StringSplitOptions.RemoveEmptyEntries); if (SavePointEvaluator.EvaluateNodeTagsForAutoSaveSilently(node.Name, tags, out _)) { result.Add(node.Name); } } return result; } } } #endif