diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp index aad438df..f8d0723e 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp @@ -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(); - - // 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(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(FoundAnimAsset->GetAsset()) : nullptr; - - if (AnimSeq) - { - UAnimGraphNode_SequencePlayer* SeqNode = NewObject(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(); - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); - if (!StateNode) return; - - // Collect and remove transitions connected to this state - TArray TransitionsToRemove; - for (UEdGraphNode* Node : SMGraph->Nodes) - { - if (UAnimStateTransitionNode* TransNode = Cast(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(); - - 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(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(); - - 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(); - - 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(AnimAssetName, Result); - if (!AnimSeq) return; - - // Find existing SequencePlayer or create one - UAnimGraphNode_SequencePlayer* SeqNode = nullptr; - for (UEdGraphNode* Node : InnerGraph->Nodes) - { - SeqNode = Cast(Node); - if (SeqNode) break; - } - - bool bCreatedNew = false; - if (!SeqNode) - { - SeqNode = NewObject(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(); - - 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(BlendSpaceName, Result); - if (!BlendSpaceAsset) return; - - // Find existing BlendSpacePlayer or create one - UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr; - for (UEdGraphNode* Node : InnerGraph->Nodes) - { - BSNode = Cast(Node); - if (BSNode) break; - } - - if (!BSNode) - { - BSNode = NewObject(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(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); -} diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.cpp new file mode 100644 index 00000000..396fad91 --- /dev/null +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_StateMachine.cpp @@ -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(); + + // 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(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(FoundAnimAsset->GetAsset()) : nullptr; + + if (AnimSeq) + { + UAnimGraphNode_SequencePlayer* SeqNode = NewObject(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(); + + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; + + // Collect and remove transitions connected to this state + TArray TransitionsToRemove; + for (UEdGraphNode* Node : SMGraph->Nodes) + { + if (UAnimStateTransitionNode* TransNode = Cast(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(); + + 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(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(); + + 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(); + + 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(AnimAssetName, Result); + if (!AnimSeq) return; + + // Find existing SequencePlayer or create one + UAnimGraphNode_SequencePlayer* SeqNode = nullptr; + for (UEdGraphNode* Node : InnerGraph->Nodes) + { + SeqNode = Cast(Node); + if (SeqNode) break; + } + + bool bCreatedNew = false; + if (!SeqNode) + { + SeqNode = NewObject(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(); + + 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(BlendSpaceName, Result); + if (!BlendSpaceAsset) return; + + // Find existing BlendSpacePlayer or create one + UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr; + for (UEdGraphNode* Node : InnerGraph->Nodes) + { + BSNode = Cast(Node); + if (BSNode) break; + } + + if (!BSNode) + { + BSNode = NewObject(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(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); +}