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 =>