Split AnimMutation MCP handlers:
This commit is contained in:
@@ -23,18 +23,8 @@
|
|||||||
#include "AnimGraphNode_SequencePlayer.h"
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
#include "AnimGraphNode_BlendSpacePlayer.h"
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
#include "AnimGraphNode_Base.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/AnimSequence.h"
|
||||||
#include "Animation/BlendSpace.h"
|
#include "Animation/BlendSpace.h"
|
||||||
#include "K2Node_VariableGet.h"
|
|
||||||
|
|
||||||
// ============================================================
|
// ============================================================
|
||||||
// HandleCreateAnimBlueprint — create a new Animation Blueprint
|
// HandleCreateAnimBlueprint — create a new Animation Blueprint
|
||||||
@@ -147,278 +137,6 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs
|
|||||||
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
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
|
// Tier 3: AnimGraph Blend Tree Mutation
|
||||||
// ============================================================
|
// ============================================================
|
||||||
@@ -594,69 +312,6 @@ void FBlueprintMCPServer::HandleAddStateMachine(const FJsonObject* Json, FJsonOb
|
|||||||
HandleAddAnimNode(&*ForwardJson, Result);
|
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)
|
void FBlueprintMCPServer::HandleListAnimSlots(const FJsonObject* Json, FJsonObject* Result)
|
||||||
{
|
{
|
||||||
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
FString BlueprintName = Json->GetStringField(TEXT("blueprint"));
|
||||||
@@ -920,191 +575,3 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ
|
|||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
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