Split AnimMutation MCP handlers:

This commit is contained in:
2026-03-07 02:51:39 -05:00
parent 114464dbfe
commit ef0a0cd2cd
2 changed files with 540 additions and 533 deletions

View File

@@ -23,18 +23,8 @@
#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
@@ -147,278 +137,6 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
// Check for duplicate state name
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
{
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
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName);
UAnimSequence* AnimSeq = FoundAnimAsset ? Cast<UAnimSequence>(FoundAnimAsset->GetAsset()) : nullptr;
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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
// 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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName, Result);
if (!FromState) return;
UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName, Result);
if (!ToState) return;
// 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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
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
// ============================================================
@@ -594,69 +312,6 @@ void FBlueprintMCPServer::HandleAddStateMachine(const FJsonObject* Json, FJsonOb
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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
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 = UMCPAssetFinder::LoadAsset<UAnimSequence>(AnimAssetName, Result);
if (!AnimSeq) return;
// 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"));
@@ -920,191 +575,3 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
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 = UMCPAssetFinder::LoadAsset<UBlendSpace>(BlendSpaceName, Result);
if (!BlendSpaceAsset) return;
// 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 : AnimBP->NewVariables)
{
if (Var.VarName == VarFName)
{
bVarFound = true;
break;
}
}
if (!bVarFound)
{
// Also check parent class properties
if (UClass* GenClass = AnimBP->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);
}

View File

@@ -0,0 +1,540 @@
#include "MCPAssetFinder.h"
#include "MCPServer.h"
#include "MCPUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_BlendSpacePlayer.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "K2Node_VariableGet.h"
// ============================================================
// 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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
// Check for duplicate state name
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
{
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
FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName);
UAnimSequence* AnimSeq = FoundAnimAsset ? Cast<UAnimSequence>(FoundAnimAsset->GetAsset()) : nullptr;
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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
// 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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName, Result);
if (!FromState) return;
UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName, Result);
if (!ToState) return;
// 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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
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);
}
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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
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 = UMCPAssetFinder::LoadAsset<UAnimSequence>(AnimAssetName, Result);
if (!AnimSeq) return;
// 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);
}
// ============================================================
// 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"));
}
UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result);
if (!SMGraph) return;
UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter<UAnimBlueprint>();
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
if (!StateNode) return;
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 = UMCPAssetFinder::LoadAsset<UBlendSpace>(BlendSpaceName, Result);
if (!BlendSpaceAsset) return;
// 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 : AnimBP->NewVariables)
{
if (Var.VarName == VarFName)
{
bVarFound = true;
break;
}
}
if (!bVarFound)
{
// Also check parent class properties
if (UClass* GenClass = AnimBP->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);
}