Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/BlueprintMCPHandlers_AnimMutation.cpp
2026-03-06 15:06:12 -05:00

1486 lines
47 KiB
C++

#include "BlueprintMCPServer.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "UObject/SavePackage.h"
// Animation Blueprint includes
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/Skeleton.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_AssetPlayerBase.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_BlendSpacePlayer.h"
#include "AnimGraphNode_Base.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimStateConduitNode.h"
#include "AnimStateEntryNode.h"
#include "AnimationStateMachineGraph.h"
#include "AnimationStateMachineSchema.h"
#include "AnimationGraph.h"
#include "AnimationGraphSchema.h"
#include "AnimationTransitionGraph.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "K2Node_VariableGet.h"
// ============================================================
// HandleCreateAnimBlueprint — create a new Animation Blueprint
// ============================================================
void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJsonObject* Result)
{
FString Name = Json->GetStringField(TEXT("name"));
FString PackagePath = Json->GetStringField(TEXT("packagePath"));
FString SkeletonName = Json->GetStringField(TEXT("skeleton"));
FString ParentClassName = Json->GetStringField(TEXT("parentClass"));
if (Name.IsEmpty() || PackagePath.IsEmpty() || SkeletonName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton"));
}
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
FString FullAssetPath = PackagePath / Name;
if (FindBlueprintAsset(Name) || FindBlueprintAsset(FullAssetPath))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."),
*Name));
}
// Resolve skeleton
USkeleton* Skeleton = nullptr;
if (SkeletonName == TEXT("__create_test_skeleton__"))
{
// Create a minimal in-memory skeleton for tests
FString TestSkeletonPath = PackagePath / (Name + TEXT("_TestSkeleton"));
UPackage* SkelPackage = CreatePackage(*TestSkeletonPath);
Skeleton = NewObject<USkeleton>(SkelPackage, FName(*(Name + TEXT("_TestSkeleton"))), RF_Public | RF_Standalone);
if (Skeleton)
{
Skeleton->MarkPackageDirty();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created test skeleton '%s'"), *Skeleton->GetName());
}
}
else
{
// Search asset registry for the skeleton
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> SkeletonAssets;
ARM.Get().GetAssetsByClass(USkeleton::StaticClass()->GetClassPathName(), SkeletonAssets, false);
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString() == SkeletonName ||
Asset.PackageName.ToString() == SkeletonName ||
Asset.GetObjectPathString() == SkeletonName)
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
// Case-insensitive fallback
if (!Skeleton)
{
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase) ||
Asset.PackageName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase))
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
}
}
if (!Skeleton)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."),
*SkeletonName));
}
// Resolve parent class (default: UAnimInstance)
UClass* ParentClass = UAnimInstance::StaticClass();
if (!ParentClassName.IsEmpty() && ParentClassName != TEXT("AnimInstance"))
{
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == ParentClassName && It->IsChildOf(UAnimInstance::StaticClass()))
{
ParentClass = *It;
break;
}
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating AnimBlueprint '%s' in '%s' with skeleton '%s'"),
*Name, *PackagePath, *Skeleton->GetName());
// Create the package
FString FullPackagePath = PackagePath / Name;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
}
// Create the Animation Blueprint
UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>(
FKismetEditorUtilities::CreateBlueprint(
ParentClass,
Package,
FName(*Name),
BPTYPE_Normal,
UAnimBlueprint::StaticClass(),
UAnimBlueprintGeneratedClass::StaticClass()
));
if (!NewAnimBP)
{
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint"));
}
// Set target skeleton
NewAnimBP->TargetSkeleton = Skeleton;
// Compile
FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
// Save
bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
// Refresh asset cache
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
AllBlueprintAssets.Empty();
ARM.Get().GetAssetsByClass(UBlueprint::StaticClass()->GetClassPathName(), AllBlueprintAssets, true);
// Collect graph names
TArray<TSharedPtr<FJsonValue>> GraphNames;
TArray<UEdGraph*> AllGraphs;
NewAnimBP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (Graph)
{
GraphNames.Add(MakeShared<FJsonValueString>(Graph->GetName()));
}
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"),
*Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprintName"), Name);
Result->SetStringField(TEXT("packagePath"), PackagePath);
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
Result->SetStringField(TEXT("targetSkeleton"), Skeleton->GetName());
Result->SetStringField(TEXT("parentClass"), ParentClass->GetName());
Result->SetBoolField(TEXT("isAnimBlueprint"), true);
Result->SetBoolField(TEXT("saved"), bSaved);
Result->SetArrayField(TEXT("graphs"), GraphNames);
}
// ============================================================
// Tier 2: State Machine Mutation
// ============================================================
void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString StateName = Json->GetStringField(TEXT("stateName"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
// Check for duplicate state name
if (MCPUtils::FindStateByName(SMGraph, StateName))
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *GraphName));
}
// Get position
int32 PosX = Json->HasField(TEXT("posX")) ? (int32)Json->GetNumberField(TEXT("posX")) : 200;
int32 PosY = Json->HasField(TEXT("posY")) ? (int32)Json->GetNumberField(TEXT("posY")) : 0;
// Create the state node
UAnimStateNode* NewState = NewObject<UAnimStateNode>(SMGraph);
NewState->CreateNewGuid();
NewState->NodePosX = PosX;
NewState->NodePosY = PosY;
// Set the state name via the bound graph
NewState->PostPlacedNewNode();
NewState->AllocateDefaultPins();
// Rename the bound graph to set the state name
if (NewState->GetBoundGraph())
{
NewState->GetBoundGraph()->Rename(*StateName, nullptr);
}
SMGraph->AddNode(NewState, false, false);
NewState->SetFlags(RF_Transactional);
// Optionally set animation asset
FString AnimAssetName = Json->GetStringField(TEXT("animationAsset"));
if (!AnimAssetName.IsEmpty() && NewState->GetBoundGraph())
{
// Try to find the animation asset and create a sequence player in the state's inner graph
UAnimSequence* AnimSeq = nullptr;
FAssetRegistryModule& ARM2 = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM2.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
if (AnimSeq)
{
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(NewState->GetBoundGraph());
SeqNode->CreateNewGuid();
SeqNode->PostPlacedNewNode();
SeqNode->AllocateDefaultPins();
SeqNode->SetAnimationAsset(AnimSeq);
SeqNode->NodePosX = 0;
SeqNode->NodePosY = 0;
NewState->GetBoundGraph()->AddNode(SeqNode, false, false);
}
}
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("stateName"), StateName);
Result->SetStringField(TEXT("graph"), GraphName);
Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString());
Result->SetBoolField(TEXT("saved"), bSaved);
}
void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString StateName = Json->GetStringField(TEXT("stateName"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName);
if (!StateNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName));
}
// Collect and remove transitions connected to this state
TArray<UAnimStateTransitionNode*> TransitionsToRemove;
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
{
if (TransNode->GetPreviousState() == StateNode || TransNode->GetNextState() == StateNode)
{
TransitionsToRemove.Add(TransNode);
}
}
}
int32 RemovedTransitions = TransitionsToRemove.Num();
for (UAnimStateTransitionNode* Trans : TransitionsToRemove)
{
Trans->BreakAllNodeLinks();
SMGraph->RemoveNode(Trans);
}
// Remove the state
StateNode->BreakAllNodeLinks();
SMGraph->RemoveNode(StateNode);
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("removedState"), StateName);
Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions);
Result->SetBoolField(TEXT("saved"), bSaved);
}
void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString FromStateName = Json->GetStringField(TEXT("fromState"));
FString ToStateName = Json->GetStringField(TEXT("toState"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || FromStateName.IsEmpty() || ToStateName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName);
if (!FromState)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("From state '%s' not found"), *FromStateName));
}
UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName);
if (!ToState)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("To state '%s' not found"), *ToStateName));
}
// Create transition node
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
TransNode->CreateNewGuid();
TransNode->PostPlacedNewNode();
TransNode->AllocateDefaultPins();
// Position between the two states
TransNode->NodePosX = (FromState->NodePosX + ToState->NodePosX) / 2;
TransNode->NodePosY = (FromState->NodePosY + ToState->NodePosY) / 2;
SMGraph->AddNode(TransNode, false, false);
TransNode->SetFlags(RF_Transactional);
// Connect: FromState output -> Transition input, Transition output -> ToState input
TransNode->CreateConnections(FromState, ToState);
// Set optional properties
if (Json->HasField(TEXT("crossfadeDuration")))
{
TransNode->CrossfadeDuration = (float)Json->GetNumberField(TEXT("crossfadeDuration"));
}
if (Json->HasField(TEXT("priority")))
{
TransNode->PriorityOrder = (int32)Json->GetNumberField(TEXT("priority"));
}
if (Json->HasField(TEXT("bBidirectional")))
{
TransNode->Bidirectional = Json->GetBoolField(TEXT("bBidirectional"));
}
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("fromState"), FromStateName);
Result->SetStringField(TEXT("toState"), ToStateName);
Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString());
Result->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
Result->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
Result->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
Result->SetBoolField(TEXT("saved"), bSaved);
}
void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString FromStateName = Json->GetStringField(TEXT("fromState"));
FString ToStateName = Json->GetStringField(TEXT("toState"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || FromStateName.IsEmpty() || ToStateName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromStateName, ToStateName);
if (!TransNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Transition from '%s' to '%s' not found in graph '%s'"),
*FromStateName, *ToStateName, *GraphName));
}
// Update properties
int32 ChangedCount = 0;
if (Json->HasField(TEXT("crossfadeDuration")))
{
TransNode->CrossfadeDuration = (float)Json->GetNumberField(TEXT("crossfadeDuration"));
ChangedCount++;
}
if (Json->HasField(TEXT("blendMode")))
{
TransNode->BlendMode = (EAlphaBlendOption)(int32)Json->GetNumberField(TEXT("blendMode"));
ChangedCount++;
}
if (Json->HasField(TEXT("priorityOrder")))
{
TransNode->PriorityOrder = (int32)Json->GetNumberField(TEXT("priorityOrder"));
ChangedCount++;
}
if (Json->HasField(TEXT("logicType")))
{
TransNode->LogicType = (ETransitionLogicType::Type)(int32)Json->GetNumberField(TEXT("logicType"));
ChangedCount++;
}
if (Json->HasField(TEXT("bBidirectional")))
{
TransNode->Bidirectional = Json->GetBoolField(TEXT("bBidirectional"));
ChangedCount++;
}
if (ChangedCount == 0)
{
return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional"));
}
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("fromState"), FromStateName);
Result->SetStringField(TEXT("toState"), ToStateName);
Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount);
Result->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
Result->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode);
Result->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
Result->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue());
Result->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
Result->SetBoolField(TEXT("saved"), bSaved);
}
// ============================================================
// Tier 3: AnimGraph Blend Tree Mutation
// ============================================================
void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString NodeType = Json->GetStringField(TEXT("nodeType"));
if (BlueprintName.IsEmpty() || NodeType.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeType"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
// Find target graph (default to AnimGraph if not specified)
UEdGraph* TargetGraph = nullptr;
if (GraphName.IsEmpty())
{
GraphName = TEXT("AnimGraph");
}
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (Graph->GetName() == GraphName)
{
TargetGraph = Graph;
break;
}
}
if (!TargetGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *GraphName));
}
int32 PosX = Json->HasField(TEXT("posX")) ? (int32)Json->GetNumberField(TEXT("posX")) : 0;
int32 PosY = Json->HasField(TEXT("posY")) ? (int32)Json->GetNumberField(TEXT("posY")) : 0;
UAnimGraphNode_Base* NewNode = nullptr;
if (NodeType == TEXT("SequencePlayer"))
{
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(TargetGraph);
SeqNode->CreateNewGuid();
SeqNode->PostPlacedNewNode();
SeqNode->AllocateDefaultPins();
// Optionally set animation asset
FString AnimAssetName = Json->GetStringField(TEXT("animationAsset"));
if (!AnimAssetName.IsEmpty())
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName)
{
UAnimSequence* AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
if (AnimSeq)
{
SeqNode->SetAnimationAsset(AnimSeq);
}
break;
}
}
}
NewNode = SeqNode;
}
else if (NodeType == TEXT("BlendSpacePlayer"))
{
UAnimGraphNode_BlendSpacePlayer* BSNode = NewObject<UAnimGraphNode_BlendSpacePlayer>(TargetGraph);
BSNode->CreateNewGuid();
BSNode->PostPlacedNewNode();
BSNode->AllocateDefaultPins();
// Optionally set blend space asset
FString BSAssetName = Json->GetStringField(TEXT("animationAsset"));
if (!BSAssetName.IsEmpty())
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> BSAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == BSAssetName ||
Asset.PackageName.ToString() == BSAssetName)
{
UBlendSpace* BS = Cast<UBlendSpace>(Asset.GetAsset());
if (BS)
{
BSNode->SetAnimationAsset(BS);
}
break;
}
}
}
NewNode = BSNode;
}
else if (NodeType == TEXT("StateMachine"))
{
UAnimGraphNode_StateMachine* SMNode = NewObject<UAnimGraphNode_StateMachine>(TargetGraph);
SMNode->CreateNewGuid();
SMNode->PostPlacedNewNode();
SMNode->AllocateDefaultPins();
NewNode = SMNode;
}
else
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Unsupported nodeType '%s'. Supported: SequencePlayer, BlendSpacePlayer, StateMachine"),
*NodeType));
}
if (!NewNode)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create anim node"));
}
NewNode->NodePosX = PosX;
NewNode->NodePosY = PosY;
TargetGraph->AddNode(NewNode, false, false);
NewNode->SetFlags(RF_Transactional);
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("nodeType"), NodeType);
Result->SetStringField(TEXT("nodeId"), NewNode->NodeGuid.ToString());
Result->SetStringField(TEXT("graph"), GraphName);
Result->SetBoolField(TEXT("saved"), bSaved);
// For StateMachine, report the sub-graph name
if (NodeType == TEXT("StateMachine"))
{
if (UAnimGraphNode_StateMachine* SMNode = Cast<UAnimGraphNode_StateMachine>(NewNode))
{
if (SMNode->EditorStateMachineGraph)
{
Result->SetStringField(TEXT("stateMachineGraph"), SMNode->EditorStateMachineGraph->GetName());
}
}
}
}
// ============================================================
// Tier 4: Advanced Operations
// ============================================================
void FBlueprintMCPServer::HandleAddStateMachine(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString MachineName = Json->GetStringField(TEXT("name"));
if (BlueprintName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
// Default name
if (MachineName.IsEmpty())
{
MachineName = TEXT("NewStateMachine");
}
// Delegate to HandleAddAnimNode with StateMachine type and AnimGraph as target
TSharedRef<FJsonObject> ForwardJson = MakeShared<FJsonObject>();
ForwardJson->SetStringField(TEXT("blueprint"), BlueprintName);
ForwardJson->SetStringField(TEXT("graph"), TEXT("AnimGraph"));
ForwardJson->SetStringField(TEXT("nodeType"), TEXT("StateMachine"));
if (Json->HasField(TEXT("posX")))
ForwardJson->SetNumberField(TEXT("posX"), Json->GetNumberField(TEXT("posX")));
if (Json->HasField(TEXT("posY")))
ForwardJson->SetNumberField(TEXT("posY"), Json->GetNumberField(TEXT("posY")));
HandleAddAnimNode(&*ForwardJson, Result);
}
void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString StateName = Json->GetStringField(TEXT("stateName"));
FString AnimAssetName = Json->GetStringField(TEXT("animationAsset"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || AnimAssetName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, animationAsset"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName);
if (!StateNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName));
}
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
}
// Find the animation asset
UAnimSequence* AnimSeq = nullptr;
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
if (!AnimSeq)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Animation asset '%s' not found"), *AnimAssetName));
}
// Find existing SequencePlayer or create one
UAnimGraphNode_SequencePlayer* SeqNode = nullptr;
for (UEdGraphNode* Node : InnerGraph->Nodes)
{
SeqNode = Cast<UAnimGraphNode_SequencePlayer>(Node);
if (SeqNode) break;
}
bool bCreatedNew = false;
if (!SeqNode)
{
SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(InnerGraph);
SeqNode->CreateNewGuid();
SeqNode->PostPlacedNewNode();
SeqNode->AllocateDefaultPins();
SeqNode->NodePosX = 0;
SeqNode->NodePosY = 0;
InnerGraph->AddNode(SeqNode, false, false);
bCreatedNew = true;
}
SeqNode->SetAnimationAsset(AnimSeq);
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("stateName"), StateName);
Result->SetStringField(TEXT("animationAsset"), AnimSeq->GetName());
Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew);
Result->SetBoolField(TEXT("saved"), bSaved);
}
void FBlueprintMCPServer::HandleListAnimSlots(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
// Walk all anim nodes to collect slot names
TSet<FString> SlotNames;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UAnimGraphNode_Base* AnimNode = Cast<UAnimGraphNode_Base>(Node))
{
// Check for SlotName property via reflection
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot")))
{
FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!SlotValue.IsNone())
{
SlotNames.Add(SlotValue.ToString());
}
}
}
}
}
}
TArray<TSharedPtr<FJsonValue>> SlotsArr;
for (const FString& Slot : SlotNames)
{
SlotsArr.Add(MakeShared<FJsonValueString>(Slot));
}
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetArrayField(TEXT("slots"), SlotsArr);
Result->SetNumberField(TEXT("count"), SlotsArr.Num());
}
void FBlueprintMCPServer::HandleListSyncGroups(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
if (BlueprintName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
// Walk all anim nodes to collect sync group names
TSet<FString> SyncGroupNames;
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UAnimGraphNode_Base* AnimNode = Cast<UAnimGraphNode_Base>(Node))
{
// Check for SyncGroup/GroupName property via reflection
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName")))
{
FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!GroupValue.IsNone())
{
SyncGroupNames.Add(GroupValue.ToString());
}
}
}
}
}
}
TArray<TSharedPtr<FJsonValue>> GroupsArr;
for (const FString& Group : SyncGroupNames)
{
GroupsArr.Add(MakeShared<FJsonValueString>(Group));
}
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blueprint"), BlueprintName);
Result->SetArrayField(TEXT("syncGroups"), GroupsArr);
Result->SetNumberField(TEXT("count"), GroupsArr.Num());
}
// ============================================================
// HandleCreateBlendSpace — create a new 2D Blend Space asset
// ============================================================
void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonObject* Result)
{
FString Name = Json->GetStringField(TEXT("name"));
FString PackagePath = Json->GetStringField(TEXT("packagePath"));
FString SkeletonName = Json->GetStringField(TEXT("skeleton"));
if (Name.IsEmpty() || PackagePath.IsEmpty() || SkeletonName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton"));
}
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
FString FullAssetPath = PackagePath / Name;
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> ExistingAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), ExistingAssets, true);
for (const FAssetData& Asset : ExistingAssets)
{
if (Asset.AssetName.ToString() == Name || Asset.GetObjectPathString() == FullAssetPath)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."),
*Name));
}
}
}
// Resolve skeleton
USkeleton* Skeleton = nullptr;
if (SkeletonName == TEXT("__create_test_skeleton__"))
{
FString TestSkeletonPath = PackagePath / (Name + TEXT("_TestSkeleton"));
UPackage* SkelPackage = CreatePackage(*TestSkeletonPath);
Skeleton = NewObject<USkeleton>(SkelPackage, FName(*(Name + TEXT("_TestSkeleton"))), RF_Public | RF_Standalone);
if (Skeleton)
{
Skeleton->MarkPackageDirty();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created test skeleton '%s'"), *Skeleton->GetName());
}
}
else
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> SkeletonAssets;
ARM.Get().GetAssetsByClass(USkeleton::StaticClass()->GetClassPathName(), SkeletonAssets, false);
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString() == SkeletonName ||
Asset.PackageName.ToString() == SkeletonName ||
Asset.GetObjectPathString() == SkeletonName)
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
// Case-insensitive fallback
if (!Skeleton)
{
for (const FAssetData& Asset : SkeletonAssets)
{
if (Asset.AssetName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase) ||
Asset.PackageName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase))
{
Skeleton = Cast<USkeleton>(Asset.GetAsset());
break;
}
}
}
}
if (!Skeleton)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(
TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."),
*SkeletonName));
}
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"),
*Name, *PackagePath, *Skeleton->GetName());
// Create the package
FString FullPackagePath = PackagePath / Name;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
}
// Create the Blend Space
UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone);
if (!NewBS)
{
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create Blend Space object"));
}
// Set skeleton
NewBS->SetSkeleton(Skeleton);
// Mark dirty and save
NewBS->MarkPackageDirty();
bool bSaved = MCPUtils::SaveGenericPackage(NewBS);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"),
*Name, bSaved ? TEXT("true") : TEXT("false"));
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
Result->SetStringField(TEXT("skeleton"), Skeleton->GetName());
Result->SetBoolField(TEXT("saved"), bSaved);
}
// ============================================================
// HandleSetBlendSpaceSamples — add animation samples to a Blend Space
// ============================================================
void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJsonObject* Result)
{
FString BlendSpaceName = Json->GetStringField(TEXT("blendSpace"));
if (BlendSpaceName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace"));
}
// Load the blend space
UBlendSpace* BS = nullptr;
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> BSAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == BlendSpaceName ||
Asset.PackageName.ToString() == BlendSpaceName ||
Asset.GetObjectPathString() == BlendSpaceName)
{
BS = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
// Try FName-based path match (e.g. /Game/Folder/BS_Name)
if (!BS)
{
FString LeafName = FPaths::GetCleanFilename(BlendSpaceName);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == LeafName)
{
BS = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
}
}
if (!BS)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName));
}
// Set axis parameters
BS->PreEditChange(nullptr);
FString AxisXName = Json->GetStringField(TEXT("axisXName"));
FString AxisYName = Json->GetStringField(TEXT("axisYName"));
const FBlendParameter& ParamX = BS->GetBlendParameter(0);
const FBlendParameter& ParamY = BS->GetBlendParameter(1);
// We need to modify BlendParameters directly — use const_cast since there's no setter API
FBlendParameter& MutableParamX = const_cast<FBlendParameter&>(ParamX);
FBlendParameter& MutableParamY = const_cast<FBlendParameter&>(ParamY);
if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName;
if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = (float)Json->GetNumberField(TEXT("axisXMin"));
if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = (float)Json->GetNumberField(TEXT("axisXMax"));
if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName;
if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = (float)Json->GetNumberField(TEXT("axisYMin"));
if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = (float)Json->GetNumberField(TEXT("axisYMax"));
// Clear existing samples (delete from end to start)
int32 NumExisting = BS->GetNumberOfBlendSamples();
for (int32 i = NumExisting - 1; i >= 0; --i)
{
BS->DeleteSample(i);
}
// Add new samples
const TArray<TSharedPtr<FJsonValue>>* SamplesArray = nullptr;
int32 SamplesSet = 0;
if (Json->TryGetArrayField(TEXT("samples"), SamplesArray) && SamplesArray)
{
// Pre-load all animation sequences for lookup
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AnimAssets;
ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false);
for (const TSharedPtr<FJsonValue>& SampleVal : *SamplesArray)
{
const TSharedPtr<FJsonObject>& SampleObj = SampleVal->AsObject();
if (!SampleObj.IsValid()) continue;
FString AnimAssetName = SampleObj->GetStringField(TEXT("animationAsset"));
float X = (float)SampleObj->GetNumberField(TEXT("x"));
float Y = (float)SampleObj->GetNumberField(TEXT("y"));
UAnimSequence* AnimSeq = nullptr;
if (!AnimAssetName.IsEmpty())
{
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == AnimAssetName ||
Asset.PackageName.ToString() == AnimAssetName ||
Asset.GetObjectPathString() == AnimAssetName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
// Also try extracting leaf name from path
if (!AnimSeq)
{
FString LeafName = FPaths::GetCleanFilename(AnimAssetName);
for (const FAssetData& Asset : AnimAssets)
{
if (Asset.AssetName.ToString() == LeafName)
{
AnimSeq = Cast<UAnimSequence>(Asset.GetAsset());
break;
}
}
}
}
FVector SampleValue(X, Y, 0.0f);
if (AnimSeq)
{
BS->AddSample(AnimSeq, SampleValue);
}
else
{
BS->AddSample(SampleValue);
}
SamplesSet++;
}
}
BS->ValidateSampleData();
BS->PostEditChange();
// Save
BS->MarkPackageDirty();
bool bSaved = MCPUtils::SaveGenericPackage(BS);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set %d samples on Blend Space '%s' (saved: %s)"),
SamplesSet, *BS->GetName(), bSaved ? TEXT("true") : TEXT("false"));
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("blendSpace"), BS->GetPathName());
Result->SetNumberField(TEXT("samplesSet"), SamplesSet);
Result->SetBoolField(TEXT("saved"), bSaved);
}
// ============================================================
// HandleSetStateBlendSpace — place a BlendSpacePlayer in a state
// ============================================================
void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJsonObject* Result)
{
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
FString GraphName = Json->GetStringField(TEXT("graph"));
FString StateName = Json->GetStringField(TEXT("stateName"));
FString BlendSpaceName = Json->GetStringField(TEXT("blendSpace"));
FString XVariable = Json->GetStringField(TEXT("xVariable"));
FString YVariable = Json->GetStringField(TEXT("yVariable"));
if (BlueprintName.IsEmpty() || GraphName.IsEmpty() || StateName.IsEmpty() || BlendSpaceName.IsEmpty())
{
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, blendSpace"));
}
FString LoadError;
UBlueprint* BP = LoadBlueprintByName(BlueprintName, LoadError);
if (!BP)
{
return MCPUtils::MakeErrorJson(Result, LoadError);
}
UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP);
if (!AnimBP)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName));
}
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName);
if (!SMGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName));
}
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName);
if (!StateNode)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName));
}
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
}
// Find the blend space asset
UBlendSpace* BlendSpaceAsset = nullptr;
{
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> BSAssets;
ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == BlendSpaceName ||
Asset.PackageName.ToString() == BlendSpaceName ||
Asset.GetObjectPathString() == BlendSpaceName)
{
BlendSpaceAsset = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
// Leaf name fallback
if (!BlendSpaceAsset)
{
FString LeafName = FPaths::GetCleanFilename(BlendSpaceName);
for (const FAssetData& Asset : BSAssets)
{
if (Asset.AssetName.ToString() == LeafName)
{
BlendSpaceAsset = Cast<UBlendSpace>(Asset.GetAsset());
break;
}
}
}
}
if (!BlendSpaceAsset)
{
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName));
}
// Find existing BlendSpacePlayer or create one
UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr;
for (UEdGraphNode* Node : InnerGraph->Nodes)
{
BSNode = Cast<UAnimGraphNode_BlendSpacePlayer>(Node);
if (BSNode) break;
}
if (!BSNode)
{
BSNode = NewObject<UAnimGraphNode_BlendSpacePlayer>(InnerGraph);
BSNode->CreateNewGuid();
BSNode->PostPlacedNewNode();
BSNode->AllocateDefaultPins();
BSNode->NodePosX = 0;
BSNode->NodePosY = 0;
InnerGraph->AddNode(BSNode, false, false);
}
BSNode->SetAnimationAsset(BlendSpaceAsset);
// Connect BlendSpacePlayer output to the Output Animation Pose node
{
// Find the AnimGraphNode_Root (Output Pose) in the inner graph
UEdGraphNode* ResultNode = nullptr;
for (UEdGraphNode* Node : InnerGraph->Nodes)
{
if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) ||
Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult")))
{
ResultNode = Node;
break;
}
}
if (ResultNode)
{
// Find output pose pin on BlendSpacePlayer and input pose pin on result node
UEdGraphPin* BSOutputPin = nullptr;
for (UEdGraphPin* Pin : BSNode->Pins)
{
if (Pin && Pin->Direction == EGPD_Output && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
{
BSOutputPin = Pin;
break;
}
}
UEdGraphPin* ResultInputPin = nullptr;
for (UEdGraphPin* Pin : ResultNode->Pins)
{
if (Pin && Pin->Direction == EGPD_Input && Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
{
ResultInputPin = Pin;
break;
}
}
if (BSOutputPin && ResultInputPin)
{
// Break existing connections on the result input
ResultInputPin->BreakAllPinLinks();
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
if (Schema)
{
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
}
}
}
}
// Wire X and Y variables if provided
auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool
{
if (VarName.IsEmpty()) return false;
// Verify the variable exists in the blueprint
FName VarFName(*VarName);
bool bVarFound = false;
for (FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == VarFName)
{
bVarFound = true;
break;
}
}
if (!bVarFound)
{
// Also check parent class properties
if (UClass* GenClass = BP->SkeletonGeneratedClass)
{
if (FProperty* Prop = GenClass->FindPropertyByName(VarFName))
{
bVarFound = true;
}
}
}
if (!bVarFound)
{
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"),
*VarName, *BlueprintName);
return false;
}
// Create a VariableGet node
UK2Node_VariableGet* GetNode = NewObject<UK2Node_VariableGet>(InnerGraph);
GetNode->VariableReference.SetSelfMember(VarFName);
GetNode->NodePosX = BSNode->NodePosX - 250;
GetNode->NodePosY = BSNode->NodePosY;
InnerGraph->AddNode(GetNode, false, false);
GetNode->AllocateDefaultPins();
// Find the variable output pin
UEdGraphPin* VarOutPin = nullptr;
for (UEdGraphPin* Pin : GetNode->Pins)
{
if (Pin && Pin->Direction == EGPD_Output && Pin->PinName == VarFName)
{
VarOutPin = Pin;
break;
}
}
// Find the target pin on the BlendSpacePlayer
UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName));
if (VarOutPin && TargetPin)
{
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
if (Schema)
{
Schema->TryCreateConnection(VarOutPin, TargetPin);
return true;
}
}
return false;
};
WireVariable(XVariable, TEXT("X"));
WireVariable(YVariable, TEXT("Y"));
// Compile and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
Result->SetBoolField(TEXT("success"), true);
Result->SetStringField(TEXT("stateName"), StateName);
Result->SetStringField(TEXT("blendSpace"), BlendSpaceAsset->GetName());
Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString());
Result->SetBoolField(TEXT("saved"), bSaved);
}