Split AnimMutation MCP handlers:
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user