diff --git a/FModel/ViewModels/AnimGraphViewModel.cs b/FModel/ViewModels/AnimGraphViewModel.cs index c6be01cf..54dbffe3 100644 --- a/FModel/ViewModels/AnimGraphViewModel.cs +++ b/FModel/ViewModels/AnimGraphViewModel.cs @@ -131,6 +131,9 @@ public class AnimGraphViewModel ResolveConnections(cdo, animNodeProps, nodeByName, vm); } + // Associate state machine nodes with their baked machine names + AssociateStateMachineNames(animBlueprintClass, cdo, animNodeProps, nodeByName); + // Group nodes into layers (connected subgraphs) BuildLayers(vm); @@ -225,6 +228,14 @@ public class AnimGraphViewModel !string.IsNullOrEmpty(rootName)) return rootName; + // Check if any node belongs to a baked state machine + var smNode = nodes.FirstOrDefault(n => + n.AdditionalProperties.TryGetValue("BelongsToStateMachine", out _)); + if (smNode != null && + smNode.AdditionalProperties.TryGetValue("BelongsToStateMachine", out var smName) && + !string.IsNullOrEmpty(smName)) + return smName; + var stateMachine = nodes.FirstOrDefault(n => n.ExportType.Contains("StateMachine", StringComparison.OrdinalIgnoreCase)); if (stateMachine != null) @@ -250,6 +261,84 @@ public class AnimGraphViewModel return exportType; } + /// + /// Reads BakedStateMachines from the animation blueprint class to associate + /// FAnimNode_StateMachine nodes with their machine names and mark internal + /// state root nodes so they can be grouped into correctly named layers. + /// + private static void AssociateStateMachineNames(UClass animBlueprintClass, UObject? cdo, + List<(string name, string structType)> animNodeProps, + Dictionary nodeByName) + { + // BakedStateMachines is a UPROPERTY on UAnimBlueprintGeneratedClass + // Try reading from both the class and CDO + UScriptArray? bakedMachines = null; + if (animBlueprintClass.TryGetValue(out UScriptArray classBaked, "BakedStateMachines")) + bakedMachines = classBaked; + else if (cdo != null && cdo.TryGetValue(out UScriptArray cdoBaked, "BakedStateMachines")) + bakedMachines = cdoBaked; + + if (bakedMachines == null || bakedMachines.Properties.Count == 0) + return; + + for (var machineIdx = 0; machineIdx < bakedMachines.Properties.Count; machineIdx++) + { + if (bakedMachines.Properties[machineIdx].GetValue(typeof(FStructFallback)) is not FStructFallback machineStruct) + continue; + + // Extract MachineName + var machineName = string.Empty; + foreach (var prop in machineStruct.Properties) + { + if (prop.Name.Text == "MachineName") + { + machineName = prop.Tag?.GenericValue?.ToString() ?? string.Empty; + break; + } + } + if (string.IsNullOrEmpty(machineName)) + continue; + + // Associate FAnimNode_StateMachine nodes that reference this machine index + var machineIdxStr = machineIdx.ToString(); + foreach (var (propName, structType) in animNodeProps) + { + if (!structType.Contains("StateMachine", StringComparison.OrdinalIgnoreCase)) + continue; + if (!nodeByName.TryGetValue(propName, out var smNode)) + continue; + if (!smNode.AdditionalProperties.TryGetValue("StateMachineIndexInClass", out var idxStr)) + continue; + if (idxStr == machineIdxStr) + smNode.AdditionalProperties["StateMachineName"] = machineName; + } + + // Mark state root nodes with BelongsToStateMachine so their layers get the machine name + foreach (var prop in machineStruct.Properties) + { + if (prop.Name.Text != "States") continue; + if (prop.Tag?.GenericValue is not UScriptArray states) break; + + foreach (var stateProp in states.Properties) + { + if (stateProp.GetValue(typeof(FStructFallback)) is not FStructFallback stateStruct) + continue; + + if (!stateStruct.TryGetValue(out int stateRootIndex, "StateRootNodeIndex")) + continue; + + if (stateRootIndex < 0 || stateRootIndex >= animNodeProps.Count) + continue; + + var rootPropName = animNodeProps[stateRootIndex].name; + if (nodeByName.TryGetValue(rootPropName, out var rootNode)) + rootNode.AdditionalProperties["BelongsToStateMachine"] = machineName; + } + break; + } + } + } + /// /// Arranges nodes within a layer in a left-to-right flow layout /// based on connection topology (sinks on the left, sources on the right). diff --git a/FModel/Views/AnimGraphViewer.xaml.cs b/FModel/Views/AnimGraphViewer.xaml.cs index 4cb6c787..f40f5cc4 100644 --- a/FModel/Views/AnimGraphViewer.xaml.cs +++ b/FModel/Views/AnimGraphViewer.xaml.cs @@ -312,7 +312,7 @@ public partial class AnimGraphViewer { if (e.ClickCount == 2) { - TryOpenLinkedLayer(node); + TryOpenSubGraph(node); e.Handled = true; return; } @@ -402,16 +402,21 @@ public partial class AnimGraphViewer } /// - /// When a LinkedAnimLayer node is double-clicked, opens its corresponding - /// layer sub-graph in a new tab. The layer is identified by matching the - /// node's "Layer" property with a layer whose Root node has the same "Name". + /// When a LinkedAnimLayer or StateMachine node is double-clicked, opens its + /// corresponding sub-graph in a new tab. + /// - LinkedAnimLayer: matches "Layer" property → layer name from Root node's "Name" + /// - StateMachine: matches "StateMachineName" property → layer name from BakedStateMachines /// - private void TryOpenLinkedLayer(AnimGraphNode node) + private void TryOpenSubGraph(AnimGraphNode node) { - if (!node.ExportType.Contains("LinkedAnimLayer", StringComparison.OrdinalIgnoreCase)) - return; + string? layerName = null; - if (!node.AdditionalProperties.TryGetValue("Layer", out var layerName) || string.IsNullOrEmpty(layerName)) + if (node.ExportType.Contains("LinkedAnimLayer", StringComparison.OrdinalIgnoreCase)) + node.AdditionalProperties.TryGetValue("Layer", out layerName); + else if (node.ExportType.Contains("StateMachine", StringComparison.OrdinalIgnoreCase)) + node.AdditionalProperties.TryGetValue("StateMachineName", out layerName); + + if (string.IsNullOrEmpty(layerName)) return; var targetLayer = _viewModel.Layers.FirstOrDefault(l =>