Files
aibis-dream/Assets/Scripts/FrameAnimation/Runtime/FrameAnimationValidation.cs
T

542 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
namespace AibisDream.FrameAnimation
{
public enum FrameAnimationValidationSeverity
{
Info,
Warning,
Error
}
public enum FrameAnimationValidationTargetType
{
Graph,
Clip,
Node,
Edge,
Flow,
ImportSource
}
public enum FrameAnimationValidationCode
{
GraphMissing,
PlayableIdEmpty,
PlayableIdDuplicate,
ClipMissing,
ClipFramesEmpty,
FrameMissing,
FrameDurationInvalid,
SpeedInvalid,
InternalIdInvalid,
InternalIdDuplicate,
NodeClipMissing,
EdgeNodeMissing,
EdgeConditionInvalid,
EdgeMultiplicityInvalid,
EdgeSelfReference,
EdgeCycle,
NodeTerminalHasSuccessor,
FlowEntryMissing,
FlowEntryDuplicate,
DefaultPlayableInvalid,
NodeUnused,
ImportSourceReferenceMissing,
ImportedClipMissingFromSource
}
public sealed class FrameAnimationValidationIssue
{
public FrameAnimationValidationSeverity Severity { get; }
public FrameAnimationValidationCode Code { get; }
public FrameAnimationValidationTargetType TargetType { get; }
public string TargetId { get; }
public string Message { get; }
internal FrameAnimationValidationIssue(
FrameAnimationValidationSeverity severity,
FrameAnimationValidationCode code,
FrameAnimationValidationTargetType targetType,
string targetId,
string message)
{
Severity = severity;
Code = code;
TargetType = targetType;
TargetId = targetId ?? string.Empty;
Message = message ?? string.Empty;
}
}
public sealed class FrameAnimationValidationReport
{
private readonly List<FrameAnimationValidationIssue> issues = new List<FrameAnimationValidationIssue>();
public IReadOnlyList<FrameAnimationValidationIssue> Issues => issues;
public bool HasErrors => issues.Any(issue => issue.Severity == FrameAnimationValidationSeverity.Error);
internal void Add(
FrameAnimationValidationSeverity severity,
FrameAnimationValidationCode code,
FrameAnimationValidationTargetType targetType,
string targetId,
string message)
{
issues.Add(new FrameAnimationValidationIssue(severity, code, targetType, targetId, message));
}
}
public static class FrameAnimationGraphValidator
{
public static FrameAnimationValidationReport Validate(FrameAnimationGraph graph)
{
var report = new FrameAnimationValidationReport();
if (graph == null)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.GraphMissing,
FrameAnimationValidationTargetType.Graph,
string.Empty,
"FrameAnimationGraph 为空。");
return report;
}
ValidatePlayableIds(graph, report);
ValidateClips(graph, report);
ValidateImportAssociations(graph, report);
ValidateInternalIds(graph.Nodes, node => node?.InternalId, FrameAnimationValidationTargetType.Node, report);
ValidateInternalIds(graph.Edges, edge => edge?.InternalId, FrameAnimationValidationTargetType.Edge, report);
ValidateInternalIds(
graph.ImportSources,
source => source?.InternalId,
FrameAnimationValidationTargetType.ImportSource,
report);
ValidateNodesAndEdges(graph, report);
ValidateFlows(graph, report);
ValidateDefaultPlayable(graph, report);
ValidateUnusedNodes(graph, report);
return report;
}
private static void ValidateImportAssociations(
FrameAnimationGraph graph,
FrameAnimationValidationReport report)
{
var sourceIds = new HashSet<string>(
graph.ImportSources
.Where(source => source != null)
.Select(source => source.InternalId));
foreach (var clip in graph.Clips.Where(clip => clip != null && clip.IsImported))
{
var importInfo = clip.ImportInfo;
if (importInfo == null ||
string.IsNullOrWhiteSpace(importInfo.ImportSourceId) ||
!sourceIds.Contains(importInfo.ImportSourceId))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.ImportSourceReferenceMissing,
FrameAnimationValidationTargetType.Clip,
clip.Id,
$"Imported Clip '{clip.Id}' 没有指向本 Graph 中有效的 ImportSource。");
continue;
}
if (importInfo.IsMissingFromSource)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.ImportedClipMissingFromSource,
FrameAnimationValidationTargetType.Clip,
clip.Id,
$"Imported Clip '{clip.Id}' 对应的源 Tag '{importInfo.SourceTagName}' 已消失。");
}
}
}
private static void ValidatePlayableIds(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
var entries = new List<(string id, FrameAnimationValidationTargetType type)>();
foreach (var clip in graph.Clips)
{
if (clip == null)
{
continue;
}
if (string.IsNullOrWhiteSpace(clip.Id))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.PlayableIdEmpty,
FrameAnimationValidationTargetType.Clip,
string.Empty,
"FrameClip playable id 不能为空。");
}
else
{
entries.Add((clip.Id, FrameAnimationValidationTargetType.Clip));
}
}
foreach (var flow in graph.Flows)
{
if (flow == null)
{
continue;
}
if (string.IsNullOrWhiteSpace(flow.Id))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.PlayableIdEmpty,
FrameAnimationValidationTargetType.Flow,
string.Empty,
"AnimationFlow playable id 不能为空。");
}
else
{
entries.Add((flow.Id, FrameAnimationValidationTargetType.Flow));
}
}
foreach (var group in entries.GroupBy(entry => entry.id).Where(group => group.Count() > 1))
{
foreach (var entry in group)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.PlayableIdDuplicate,
entry.type,
group.Key,
$"playable id '{group.Key}' 在 Clip / Flow 统一命名空间中重复。");
}
}
}
private static void ValidateClips(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
for (var clipIndex = 0; clipIndex < graph.Clips.Count; clipIndex++)
{
var clip = graph.Clips[clipIndex];
if (clip == null)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.ClipMissing,
FrameAnimationValidationTargetType.Graph,
graph.Id,
$"Graph clips[{clipIndex}] 引用了空 Clip。");
continue;
}
if (!FrameAnimationValueUtility.IsValidSpeed(clip.Speed))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.SpeedInvalid,
FrameAnimationValidationTargetType.Clip,
clip.Id,
$"Clip '{clip.Id}' 的 speed 必须是有限且不小于 0 的数值。");
}
if (clip.FrameCount == 0)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.ClipFramesEmpty,
FrameAnimationValidationTargetType.Clip,
clip.Id,
$"Clip '{clip.Id}' 没有任何 Frame。");
continue;
}
for (var frameIndex = 0; frameIndex < clip.Frames.Count; frameIndex++)
{
var frame = clip.Frames[frameIndex];
if (frame == null)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.FrameMissing,
FrameAnimationValidationTargetType.Clip,
clip.Id,
$"Clip '{clip.Id}' 的第 {frameIndex} 帧为空数据对象。");
}
else if (frame.DurationMs <= 0)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.FrameDurationInvalid,
FrameAnimationValidationTargetType.Clip,
clip.Id,
$"Clip '{clip.Id}' 的第 {frameIndex} 帧 durationMs 必须大于 0。");
}
}
}
}
private static void ValidateInternalIds<T>(
IReadOnlyList<T> items,
Func<T, string> idSelector,
FrameAnimationValidationTargetType targetType,
FrameAnimationValidationReport report)
{
var validIds = new List<string>();
foreach (var item in items)
{
var id = idSelector(item);
if (!FrameAnimationValueUtility.IsValidInternalId(id))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.InternalIdInvalid,
targetType,
id,
$"{targetType} internalId 必须是不可变的 N 格式 GUID。");
}
else
{
validIds.Add(id);
}
}
foreach (var duplicate in validIds.GroupBy(id => id).Where(group => group.Count() > 1))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.InternalIdDuplicate,
targetType,
duplicate.Key,
$"{targetType} internalId '{duplicate.Key}' 重复。");
}
}
private static void ValidateNodesAndEdges(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
var nodeCounts = graph.Nodes
.Where(node => node != null)
.GroupBy(node => node.InternalId ?? string.Empty)
.ToDictionary(group => group.Key, group => group.Count());
var outgoing = graph.Edges
.Where(edge => edge != null)
.GroupBy(edge => edge.FromNodeId ?? string.Empty)
.ToDictionary(group => group.Key, group => group.ToList());
foreach (var node in graph.Nodes)
{
if (node == null)
{
continue;
}
if (node.Type != AnimationNodeType.Clip)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.NodeClipMissing,
FrameAnimationValidationTargetType.Node,
node.InternalId,
$"Node '{node.InternalId}' 使用了第一版不支持的节点类型。");
}
var clipMatches = graph.Clips.Count(clip => clip != null && clip.Id == node.ClipId);
if (clipMatches != 1)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.NodeClipMissing,
FrameAnimationValidationTargetType.Node,
node.InternalId,
$"Node '{node.InternalId}' 的 clipId '{node.ClipId}' 必须唯一指向一个 Clip。");
}
if (node.SpeedOverride.HasValue &&
!FrameAnimationValueUtility.IsValidSpeed(node.SpeedOverride.Value))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.SpeedInvalid,
FrameAnimationValidationTargetType.Node,
node.InternalId,
$"Node '{node.InternalId}' 的 speedOverride 必须是有限且不小于 0 的数值。");
}
var outgoingCount = outgoing.TryGetValue(node.InternalId ?? string.Empty, out var edges)
? edges.Count
: 0;
if (outgoingCount > 1)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.EdgeMultiplicityInvalid,
FrameAnimationValidationTargetType.Node,
node.InternalId,
$"Node '{node.InternalId}' 第一版最多只能有一个后继 Edge。");
}
if (node.EndBehaviorOverride.HasValue && outgoingCount > 0)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.NodeTerminalHasSuccessor,
FrameAnimationValidationTargetType.Node,
node.InternalId,
$"Node '{node.InternalId}' 已设置终点结束行为,不能同时连接后继 Edge。");
}
}
foreach (var edge in graph.Edges)
{
if (edge == null)
{
continue;
}
var fromValid = nodeCounts.TryGetValue(edge.FromNodeId ?? string.Empty, out var fromCount) && fromCount == 1;
var toValid = nodeCounts.TryGetValue(edge.ToNodeId ?? string.Empty, out var toCount) && toCount == 1;
if (!fromValid || !toValid)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.EdgeNodeMissing,
FrameAnimationValidationTargetType.Edge,
edge.InternalId,
$"Edge '{edge.InternalId}' 的起点或终点没有唯一对应的 Node。");
}
if (edge.Condition != AnimationEdgeCondition.Always)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.EdgeConditionInvalid,
FrameAnimationValidationTargetType.Edge,
edge.InternalId,
$"Edge '{edge.InternalId}' 第一版只支持 Always 条件。");
}
if (!string.IsNullOrEmpty(edge.FromNodeId) && edge.FromNodeId == edge.ToNodeId)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.EdgeSelfReference,
FrameAnimationValidationTargetType.Edge,
edge.InternalId,
$"Edge '{edge.InternalId}' 不允许自连接。");
}
}
ValidateCycles(graph, report);
}
private static void ValidateCycles(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
var topology = new FrameAnimationGraphTopology(graph);
var reported = new HashSet<string>();
foreach (var edge in graph.Edges.Where(edge => edge != null && edge.FromNodeId != edge.ToNodeId))
{
if (topology.WouldCreateCycle(edge.FromNodeId, edge.ToNodeId) &&
reported.Add(edge.FromNodeId ?? string.Empty))
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.EdgeCycle,
FrameAnimationValidationTargetType.Node,
edge.FromNodeId,
$"从 Node '{edge.FromNodeId}' 可达的 Edge 形成了环路。");
}
}
}
private static void ValidateFlows(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
var entryOwners = new Dictionary<string, List<string>>();
foreach (var flow in graph.Flows)
{
if (flow == null)
{
continue;
}
var matches = graph.Nodes.Count(node => node != null && node.InternalId == flow.EntryNodeId);
if (matches != 1)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.FlowEntryMissing,
FrameAnimationValidationTargetType.Flow,
flow.Id,
$"Flow '{flow.Id}' 的入口没有唯一对应的 Node。");
}
var entryNodeId = flow.EntryNodeId ?? string.Empty;
if (!entryOwners.TryGetValue(entryNodeId, out var owners))
{
owners = new List<string>();
entryOwners.Add(entryNodeId, owners);
}
owners.Add(flow.Id);
}
foreach (var pair in entryOwners.Where(pair => pair.Value.Count > 1))
{
foreach (var flowId in pair.Value)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.FlowEntryDuplicate,
FrameAnimationValidationTargetType.Flow,
flowId,
$"Node '{pair.Key}' 不能同时作为多个 Flow 的入口。");
}
}
}
private static void ValidateDefaultPlayable(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
var defaultId = graph.Settings?.DefaultPlayableId;
var matches = graph.Clips.Count(clip => clip != null && clip.Id == defaultId) +
graph.Flows.Count(flow => flow != null && flow.Id == defaultId);
if (string.IsNullOrWhiteSpace(defaultId) || matches != 1)
{
report.Add(
FrameAnimationValidationSeverity.Error,
FrameAnimationValidationCode.DefaultPlayableInvalid,
FrameAnimationValidationTargetType.Graph,
graph.Id,
"defaultPlayableId 必须唯一指向一个 Clip 或 Flow。");
}
}
private static void ValidateUnusedNodes(FrameAnimationGraph graph, FrameAnimationValidationReport report)
{
var reachable = new HashSet<string>();
var topology = new FrameAnimationGraphTopology(graph);
foreach (var flow in graph.Flows.Where(flow => flow != null))
{
foreach (var node in topology.GetReachable(flow.EntryNodeId).Nodes)
{
reachable.Add(node.InternalId ?? string.Empty);
}
}
foreach (var node in graph.Nodes.Where(node => node != null && !reachable.Contains(node.InternalId)))
{
report.Add(
FrameAnimationValidationSeverity.Warning,
FrameAnimationValidationCode.NodeUnused,
FrameAnimationValidationTargetType.Node,
node.InternalId,
$"Node '{node.InternalId}' 未被任何 Flow 使用。");
}
}
}
}