From 6fe04442dd263f91bc7700b37352cbbc5e447fe8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 5 Mar 2026 07:57:38 +0000 Subject: [PATCH] Fix SaveCachedPose placement: use common ancestor for multi-consumer cases and outermost AnimGraph layer for fallback Co-authored-by: LoogLong <86428208+LoogLong@users.noreply.github.com> --- FModel/ViewModels/AnimGraphViewModel.cs | 37 +++++++++++++++++++------ 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/FModel/ViewModels/AnimGraphViewModel.cs b/FModel/ViewModels/AnimGraphViewModel.cs index c1438ad8..e9e2a70d 100644 --- a/FModel/ViewModels/AnimGraphViewModel.cs +++ b/FModel/ViewModels/AnimGraphViewModel.cs @@ -215,6 +215,12 @@ public class AnimGraphViewModel primaryGraphLayer ??= vm.Layers[^1]; } + // Determine the outermost animation blueprint layer for fallback. + // In UE, the outermost layer is always named "AnimGraph". + // If found, use it; otherwise fall back to the first _Root layer. + var outermostGraphLayer = vm.Layers.FirstOrDefault(l => + l.Name.Equals("AnimGraph", StringComparison.OrdinalIgnoreCase)) ?? primaryGraphLayer; + // Pass 2: Build state machine state sub-graphs from AnimGraphNode_StateResult nodes. // Each _StateResult node defines a state's sub-graph within a state machine. // These are stored in StateSubGraphs keyed by the root node's property name. @@ -237,7 +243,7 @@ public class AnimGraphViewModel // back through the state machine hierarchy to their parent animation blueprint layer. // Each SaveCachedPose's upstream chain excludes other SaveCachedPose nodes so that // chained SaveCachedPose → UseCachedPose → SaveCachedPose are independently placed. - if (primaryGraphLayer != null) + if (outermostGraphLayer != null) { var unassignedSavePoseNodes = vm.Nodes .Where(n => !assigned.Contains(n) && IsSaveCachedPoseNode(n)) @@ -251,7 +257,7 @@ public class AnimGraphViewModel foreach (var saveNode in unassignedSavePoseNodes) { if (!assigned.Add(saveNode)) continue; - var targetLayer = FindOwnerRootLayer(saveNode, vm, lookups) ?? primaryGraphLayer; + var targetLayer = FindOwnerRootLayer(saveNode, vm, lookups) ?? outermostGraphLayer; var inputChain = CollectUpstream(saveNode, upstreamOf, assigned, excludeNode: IsSaveCachedPoseNode); targetLayer.Nodes.AddRange(inputChain); @@ -309,9 +315,9 @@ public class AnimGraphViewModel // blueprint layers (layers that contain a _Root node). After all passes // above, scan every non-_Root layer (state sub-graphs and fallback layers) // and move any SaveCachedPose nodes to the correct _Root layer. - if (primaryGraphLayer != null) + if (outermostGraphLayer != null) { - EnforceSaveCachedPoseInRootLayers(vm, primaryGraphLayer); + EnforceSaveCachedPoseInRootLayers(vm, outermostGraphLayer); } } @@ -406,15 +412,17 @@ public class AnimGraphViewModel /// /// Finds the correct animation blueprint layer (_Root layer) for a SaveCachedPose node - /// by tracing its downstream UseCachedPose consumers through the state machine - /// hierarchy to find the parent animation blueprint layer. + /// by tracing ALL downstream UseCachedPose consumers through the state machine + /// hierarchy to find each consumer's ancestor _Root layer. If all consumers trace + /// to the same _Root layer, that layer is used. If consumers span different _Root + /// layers, the SaveCachedPose must be placed in the outermost layer (AnimGraph). /// Returns null if no suitable layer is found. /// private static AnimGraphLayer? FindOwnerRootLayer( AnimGraphNode saveNode, AnimGraphViewModel vm, LayerLookups lookups) { - // Find downstream consumers (UseCachedPose nodes referencing this SaveCachedPose) - // Trace each consumer up to find the ancestor _Root layer + // Collect ancestor _Root layers from ALL consumers + var consumerRootLayers = new HashSet(); foreach (var conn in vm.Connections) { if (conn.SourceNode != saveNode) continue; @@ -424,9 +432,20 @@ public class AnimGraphViewModel var rootLayer = GetAncestorRootLayer(consumerLayer, lookups.MachineToLayer); if (rootLayer != null) - return rootLayer; + consumerRootLayers.Add(rootLayer); } + if (consumerRootLayers.Count == 0) + return null; + + // If all consumers trace to the same _Root layer, use it + if (consumerRootLayers.Count == 1) + return consumerRootLayers.First(); + + // Consumers span different _Root layers: the SaveCachedPose must be placed + // in the outermost animation blueprint layer (AnimGraph) since it needs to + // be accessible from all consumer layers. Return null to trigger fallback + // to the outermost layer. return null; }