diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp index ae1cd3c5..d4b4453f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPAssetFinder.cpp @@ -7,6 +7,12 @@ #include "Materials/Material.h" #include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialFunction.h" +#include "Animation/Skeleton.h" +#include "Animation/AnimSequence.h" +#include "Animation/BlendSpace.h" +#include "Animation/AnimBlueprint.h" +#include "AnimationStateMachineGraph.h" +#include "MCPUtils.h" #include "AssetRegistry/AssetRegistryModule.h" #include "AssetRegistry/IAssetRegistry.h" @@ -79,6 +85,10 @@ void UMCPAssetFinder::Refresh() Self->CacheAssets(AR, UMaterialFunction::StaticClass(), false); Self->CacheAssets(AR, UUserDefinedStruct::StaticClass(), false); Self->CacheAssets(AR, UUserDefinedEnum::StaticClass(), false); + Self->CacheAssets(AR, USkeleton::StaticClass(), false); + Self->CacheAssets(AR, UAnimSequence::StaticClass(), false); + Self->CacheAssets(AR, UBlendSpace::StaticClass(), false); + Self->CacheAssets(AR, UAnimBlueprint::StaticClass(), false); // Combined list: blueprints + maps TArray& Combined = Self->AssetCache.Add(BlueprintsAndMaps); @@ -233,3 +243,15 @@ UBlueprint* UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(const FString& NameOr return LoadBlueprintOrLevelBlueprint(*Asset, Error); } +UAnimationStateMachineGraph* UMCPAssetFinder::LoadAnimStateMachineGraph( + const FString& BlueprintName, const FString& GraphName, MCPErrorCallback Error) +{ + UAnimBlueprint* AnimBP = LoadAsset(BlueprintName, Error); + if (!AnimBP) return nullptr; + + UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(AnimBP, GraphName); + if (!SMGraph) + Error.SetError(FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *GraphName, *BlueprintName)); + return SMGraph; +} + diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp index 2a3e9479..aad438df 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPHandlers_AnimMutation.cpp @@ -59,7 +59,7 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs // Check if asset already exists FString FullAssetPath = PackagePath / Name; - if (UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), Name) || UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), FullAssetPath)) + if (UMCPAssetFinder::FindAsset(UBlueprint::StaticClass(), Name)) { return MCPUtils::MakeErrorJson(Result, FString::Printf( TEXT("Blueprint '%s' already exists. Use a different name or delete the existing asset first."), @@ -67,59 +67,8 @@ void FBlueprintMCPServer::HandleCreateAnimBlueprint(const FJsonObject* Json, FJs } // Resolve skeleton - USkeleton* Skeleton = nullptr; - - if (SkeletonName == TEXT("__create_test_skeleton__")) - { - // Create a minimal in-memory skeleton for tests - FString TestSkeletonPath = PackagePath / (Name + TEXT("_TestSkeleton")); - UPackage* SkelPackage = CreatePackage(*TestSkeletonPath); - Skeleton = NewObject(SkelPackage, FName(*(Name + TEXT("_TestSkeleton"))), RF_Public | RF_Standalone); - if (Skeleton) - { - Skeleton->MarkPackageDirty(); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created test skeleton '%s'"), *Skeleton->GetName()); - } - } - else - { - // Search asset registry for the skeleton - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray SkeletonAssets; - ARM.Get().GetAssetsByClass(USkeleton::StaticClass()->GetClassPathName(), SkeletonAssets, false); - - for (const FAssetData& Asset : SkeletonAssets) - { - if (Asset.AssetName.ToString() == SkeletonName || - Asset.PackageName.ToString() == SkeletonName || - Asset.GetObjectPathString() == SkeletonName) - { - Skeleton = Cast(Asset.GetAsset()); - break; - } - } - - // Case-insensitive fallback - if (!Skeleton) - { - for (const FAssetData& Asset : SkeletonAssets) - { - if (Asset.AssetName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase) || - Asset.PackageName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase)) - { - Skeleton = Cast(Asset.GetAsset()); - break; - } - } - } - } - - if (!Skeleton) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."), - *SkeletonName)); - } + USkeleton* Skeleton = UMCPAssetFinder::LoadAsset(SkeletonName, Result); + if (!Skeleton) return; // Resolve parent class (default: UAnimInstance) UClass* ParentClass = UAnimInstance::StaticClass(); @@ -213,27 +162,12 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } - - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } - - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); - if (!SMGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); - } + UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result); + if (!SMGraph) return; + UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter(); // Check for duplicate state name - if (MCPUtils::FindStateByName(SMGraph, StateName)) + if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr)) { return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *GraphName)); } @@ -266,19 +200,8 @@ void FBlueprintMCPServer::HandleAddAnimState(const FJsonObject* Json, FJsonObjec if (!AnimAssetName.IsEmpty() && NewState->GetBoundGraph()) { // Try to find the animation asset and create a sequence player in the state's inner graph - UAnimSequence* AnimSeq = nullptr; - FAssetRegistryModule& ARM2 = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray AnimAssets; - ARM2.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false); - for (const FAssetData& Asset : AnimAssets) - { - if (Asset.AssetName.ToString() == AnimAssetName || - Asset.PackageName.ToString() == AnimAssetName) - { - AnimSeq = Cast(Asset.GetAsset()); - break; - } - } + FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName); + UAnimSequence* AnimSeq = FoundAnimAsset ? Cast(FoundAnimAsset->GetAsset()) : nullptr; if (AnimSeq) { @@ -315,30 +238,12 @@ void FBlueprintMCPServer::HandleRemoveAnimState(const FJsonObject* Json, FJsonOb return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result); + if (!SMGraph) return; + UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter(); - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } - - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); - if (!SMGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); - } - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName); - if (!StateNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); - } + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; // Collect and remove transitions connected to this state TArray TransitionsToRemove; @@ -386,36 +291,15 @@ void FBlueprintMCPServer::HandleAddAnimTransition(const FJsonObject* Json, FJson return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result); + if (!SMGraph) return; + UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter(); - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } + UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName, Result); + if (!FromState) return; - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); - if (!SMGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); - } - - UAnimStateNode* FromState = MCPUtils::FindStateByName(SMGraph, FromStateName); - if (!FromState) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("From state '%s' not found"), *FromStateName)); - } - - UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName); - if (!ToState) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("To state '%s' not found"), *ToStateName)); - } + UAnimStateNode* ToState = MCPUtils::FindStateByName(SMGraph, ToStateName, Result); + if (!ToState) return; // Create transition node UAnimStateTransitionNode* TransNode = NewObject(SMGraph); @@ -473,24 +357,9 @@ void FBlueprintMCPServer::HandleSetTransitionRule(const FJsonObject* Json, FJson return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, fromState, toState")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } - - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } - - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); - if (!SMGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); - } + UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result); + if (!SMGraph) return; + UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter(); UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromStateName, ToStateName); if (!TransNode) @@ -565,18 +434,8 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, nodeType")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } - - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } + UAnimBlueprint* AnimBP = UMCPAssetFinder::LoadAsset(BlueprintName, Result); + if (!AnimBP) return; // Find target graph (default to AnimGraph if not specified) UEdGraph* TargetGraph = nullptr; @@ -586,7 +445,7 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject } TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); + AnimBP->GetAllGraphs(AllGraphs); for (UEdGraph* Graph : AllGraphs) { if (Graph->GetName() == GraphName) @@ -617,20 +476,13 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject FString AnimAssetName = Json->GetStringField(TEXT("animationAsset")); if (!AnimAssetName.IsEmpty()) { - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray AnimAssets; - ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false); - for (const FAssetData& Asset : AnimAssets) + FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName); + if (FoundAnimAsset) { - if (Asset.AssetName.ToString() == AnimAssetName || - Asset.PackageName.ToString() == AnimAssetName) + UAnimSequence* AnimSeq = Cast(FoundAnimAsset->GetAsset()); + if (AnimSeq) { - UAnimSequence* AnimSeq = Cast(Asset.GetAsset()); - if (AnimSeq) - { - SeqNode->SetAnimationAsset(AnimSeq); - } - break; + SeqNode->SetAnimationAsset(AnimSeq); } } } @@ -648,20 +500,13 @@ void FBlueprintMCPServer::HandleAddAnimNode(const FJsonObject* Json, FJsonObject FString BSAssetName = Json->GetStringField(TEXT("animationAsset")); if (!BSAssetName.IsEmpty()) { - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray BSAssets; - ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true); - for (const FAssetData& Asset : BSAssets) + FAssetData* FoundBSAsset = UMCPAssetFinder::FindAsset(UBlendSpace::StaticClass(), BSAssetName); + if (FoundBSAsset) { - if (Asset.AssetName.ToString() == BSAssetName || - Asset.PackageName.ToString() == BSAssetName) + UBlendSpace* BS = Cast(FoundBSAsset->GetAsset()); + if (BS) { - UBlendSpace* BS = Cast(Asset.GetAsset()); - if (BS) - { - BSNode->SetAnimationAsset(BS); - } - break; + BSNode->SetAnimationAsset(BS); } } } @@ -761,30 +606,12 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, animationAsset")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result); + if (!SMGraph) return; + UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter(); - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } - - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); - if (!SMGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); - } - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName); - if (!StateNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); - } + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; UEdGraph* InnerGraph = StateNode->GetBoundGraph(); if (!InnerGraph) @@ -793,24 +620,8 @@ void FBlueprintMCPServer::HandleSetStateAnimation(const FJsonObject* Json, FJson } // Find the animation asset - UAnimSequence* AnimSeq = nullptr; - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray AnimAssets; - ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false); - for (const FAssetData& Asset : AnimAssets) - { - if (Asset.AssetName.ToString() == AnimAssetName || - Asset.PackageName.ToString() == AnimAssetName) - { - AnimSeq = Cast(Asset.GetAsset()); - break; - } - } - - if (!AnimSeq) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Animation asset '%s' not found"), *AnimAssetName)); - } + UAnimSequence* AnimSeq = UMCPAssetFinder::LoadAsset(AnimAssetName, Result); + if (!AnimSeq) return; // Find existing SequencePlayer or create one UAnimGraphNode_SequencePlayer* SeqNode = nullptr; @@ -854,23 +665,13 @@ void FBlueprintMCPServer::HandleListAnimSlots(const FJsonObject* Json, FJsonObje return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } - - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } + UAnimBlueprint* AnimBP = UMCPAssetFinder::LoadAsset(BlueprintName, Result); + if (!AnimBP) return; // Walk all anim nodes to collect slot names TSet SlotNames; TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); + AnimBP->GetAllGraphs(AllGraphs); for (UEdGraph* Graph : AllGraphs) { for (UEdGraphNode* Node : Graph->Nodes) @@ -913,23 +714,13 @@ void FBlueprintMCPServer::HandleListSyncGroups(const FJsonObject* Json, FJsonObj return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } - - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } + UAnimBlueprint* AnimBP = UMCPAssetFinder::LoadAsset(BlueprintName, Result); + if (!AnimBP) return; // Walk all anim nodes to collect sync group names TSet SyncGroupNames; TArray AllGraphs; - BP->GetAllGraphs(AllGraphs); + AnimBP->GetAllGraphs(AllGraphs); for (UEdGraph* Graph : AllGraphs) { for (UEdGraphNode* Node : Graph->Nodes) @@ -986,73 +777,16 @@ void FBlueprintMCPServer::HandleCreateBlendSpace(const FJsonObject* Json, FJsonO // Check if asset already exists FString FullAssetPath = PackagePath / Name; + if (UMCPAssetFinder::FindAsset(UBlendSpace::StaticClass(), Name)) { - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray ExistingAssets; - ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), ExistingAssets, true); - for (const FAssetData& Asset : ExistingAssets) - { - if (Asset.AssetName.ToString() == Name || Asset.GetObjectPathString() == FullAssetPath) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."), - *Name)); - } - } + return MCPUtils::MakeErrorJson(Result, FString::Printf( + TEXT("Blend Space '%s' already exists. Use a different name or delete the existing asset first."), + *Name)); } // Resolve skeleton - USkeleton* Skeleton = nullptr; - - if (SkeletonName == TEXT("__create_test_skeleton__")) - { - FString TestSkeletonPath = PackagePath / (Name + TEXT("_TestSkeleton")); - UPackage* SkelPackage = CreatePackage(*TestSkeletonPath); - Skeleton = NewObject(SkelPackage, FName(*(Name + TEXT("_TestSkeleton"))), RF_Public | RF_Standalone); - if (Skeleton) - { - Skeleton->MarkPackageDirty(); - UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created test skeleton '%s'"), *Skeleton->GetName()); - } - } - else - { - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray SkeletonAssets; - ARM.Get().GetAssetsByClass(USkeleton::StaticClass()->GetClassPathName(), SkeletonAssets, false); - - for (const FAssetData& Asset : SkeletonAssets) - { - if (Asset.AssetName.ToString() == SkeletonName || - Asset.PackageName.ToString() == SkeletonName || - Asset.GetObjectPathString() == SkeletonName) - { - Skeleton = Cast(Asset.GetAsset()); - break; - } - } - - // Case-insensitive fallback - if (!Skeleton) - { - for (const FAssetData& Asset : SkeletonAssets) - { - if (Asset.AssetName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase) || - Asset.PackageName.ToString().Equals(SkeletonName, ESearchCase::IgnoreCase)) - { - Skeleton = Cast(Asset.GetAsset()); - break; - } - } - } - } - - if (!Skeleton) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf( - TEXT("Skeleton '%s' not found. Provide the skeleton asset name or path. Use '__create_test_skeleton__' for testing."), - *SkeletonName)); - } + USkeleton* Skeleton = UMCPAssetFinder::LoadAsset(SkeletonName, Result); + if (!Skeleton) return; UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"), *Name, *PackagePath, *Skeleton->GetName()); @@ -1101,40 +835,8 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ } // Load the blend space - UBlendSpace* BS = nullptr; - { - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray BSAssets; - ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true); - for (const FAssetData& Asset : BSAssets) - { - if (Asset.AssetName.ToString() == BlendSpaceName || - Asset.PackageName.ToString() == BlendSpaceName || - Asset.GetObjectPathString() == BlendSpaceName) - { - BS = Cast(Asset.GetAsset()); - break; - } - } - // Try FName-based path match (e.g. /Game/Folder/BS_Name) - if (!BS) - { - FString LeafName = FPaths::GetCleanFilename(BlendSpaceName); - for (const FAssetData& Asset : BSAssets) - { - if (Asset.AssetName.ToString() == LeafName) - { - BS = Cast(Asset.GetAsset()); - break; - } - } - } - } - - if (!BS) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName)); - } + UBlendSpace* BS = UMCPAssetFinder::LoadAsset(BlendSpaceName, Result); + if (!BS) return; // Set axis parameters BS->PreEditChange(nullptr); @@ -1170,11 +872,6 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ if (Json->TryGetArrayField(TEXT("samples"), SamplesArray) && SamplesArray) { - // Pre-load all animation sequences for lookup - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray AnimAssets; - ARM.Get().GetAssetsByClass(UAnimSequence::StaticClass()->GetClassPathName(), AnimAssets, false); - for (const TSharedPtr& SampleVal : *SamplesArray) { const TSharedPtr& SampleObj = SampleVal->AsObject(); @@ -1187,29 +884,10 @@ void FBlueprintMCPServer::HandleSetBlendSpaceSamples(const FJsonObject* Json, FJ UAnimSequence* AnimSeq = nullptr; if (!AnimAssetName.IsEmpty()) { - for (const FAssetData& Asset : AnimAssets) + FAssetData* FoundAnimAsset = UMCPAssetFinder::FindAsset(UAnimSequence::StaticClass(), AnimAssetName); + if (FoundAnimAsset) { - if (Asset.AssetName.ToString() == AnimAssetName || - Asset.PackageName.ToString() == AnimAssetName || - Asset.GetObjectPathString() == AnimAssetName) - { - AnimSeq = Cast(Asset.GetAsset()); - break; - } - } - - // Also try extracting leaf name from path - if (!AnimSeq) - { - FString LeafName = FPaths::GetCleanFilename(AnimAssetName); - for (const FAssetData& Asset : AnimAssets) - { - if (Asset.AssetName.ToString() == LeafName) - { - AnimSeq = Cast(Asset.GetAsset()); - break; - } - } + AnimSeq = Cast(FoundAnimAsset->GetAsset()); } } @@ -1260,30 +938,12 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: blueprint, graph, stateName, blendSpace")); } - FString LoadError; - UBlueprint* BP = UMCPAssetFinder::LoadBlueprintOrLevelBlueprint(BlueprintName, LoadError); - if (!BP) - { - return MCPUtils::MakeErrorJson(Result, LoadError); - } + UAnimationStateMachineGraph* SMGraph = UMCPAssetFinder::LoadAnimStateMachineGraph(BlueprintName, GraphName, Result); + if (!SMGraph) return; + UAnimBlueprint* AnimBP = SMGraph->GetTypedOuter(); - UAnimBlueprint* AnimBP = Cast(BP); - if (!AnimBP) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' is not an Animation Blueprint"), *BlueprintName)); - } - - UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(BP, GraphName); - if (!SMGraph) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found"), *GraphName)); - } - - UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName); - if (!StateNode) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *GraphName)); - } + UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result); + if (!StateNode) return; UEdGraph* InnerGraph = StateNode->GetBoundGraph(); if (!InnerGraph) @@ -1292,40 +952,8 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso } // Find the blend space asset - UBlendSpace* BlendSpaceAsset = nullptr; - { - FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked("AssetRegistry"); - TArray BSAssets; - ARM.Get().GetAssetsByClass(UBlendSpace::StaticClass()->GetClassPathName(), BSAssets, true); - for (const FAssetData& Asset : BSAssets) - { - if (Asset.AssetName.ToString() == BlendSpaceName || - Asset.PackageName.ToString() == BlendSpaceName || - Asset.GetObjectPathString() == BlendSpaceName) - { - BlendSpaceAsset = Cast(Asset.GetAsset()); - break; - } - } - // Leaf name fallback - if (!BlendSpaceAsset) - { - FString LeafName = FPaths::GetCleanFilename(BlendSpaceName); - for (const FAssetData& Asset : BSAssets) - { - if (Asset.AssetName.ToString() == LeafName) - { - BlendSpaceAsset = Cast(Asset.GetAsset()); - break; - } - } - } - } - - if (!BlendSpaceAsset) - { - return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Blend Space '%s' not found"), *BlendSpaceName)); - } + UBlendSpace* BlendSpaceAsset = UMCPAssetFinder::LoadAsset(BlendSpaceName, Result); + if (!BlendSpaceAsset) return; // Find existing BlendSpacePlayer or create one UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr; @@ -1406,7 +1034,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso // Verify the variable exists in the blueprint FName VarFName(*VarName); bool bVarFound = false; - for (FBPVariableDescription& Var : BP->NewVariables) + for (FBPVariableDescription& Var : AnimBP->NewVariables) { if (Var.VarName == VarFName) { @@ -1417,7 +1045,7 @@ void FBlueprintMCPServer::HandleSetStateBlendSpace(const FJsonObject* Json, FJso if (!bVarFound) { // Also check parent class properties - if (UClass* GenClass = BP->SkeletonGeneratedClass) + if (UClass* GenClass = AnimBP->SkeletonGeneratedClass) { if (FProperty* Prop = GenClass->FindPropertyByName(VarFName)) { diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp index b6407848..262948da 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp @@ -1136,7 +1136,7 @@ UAnimationStateMachineGraph* MCPUtils::FindStateMachineGraph(UBlueprint* BP, con return nullptr; } -UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName) +UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error) { for (UEdGraphNode* Node : SMGraph->Nodes) { @@ -1148,6 +1148,7 @@ UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, } } } + Error.SetError(FString::Printf(TEXT("State '%s' not found in graph '%s'"), *StateName, *SMGraph->GetName())); return nullptr; } diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md index 8ced78d4..f3ffbb7b 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/TODO.md @@ -3,3 +3,5 @@ - When compiling blueprints, always check for errors properly. Many handlers call `FKismetEditorUtilities::CompileBlueprint` and assume success without checking. The validation handler (`MCPHandlers_Validation.h`) already has proper error collection logic — all compile sites should follow that pattern. - Add `FindBlueprintGraph` to `UMCPAssetFinder`: load blueprint, URL-decode graph name, search all graphs by name, return `UEdGraph*` or nullptr with error. Currently ~10 handlers duplicate this 25-line pattern (see `MCPHandlers_Mutation.h:186-210` for a typical example). + +- Look for all uses of `GetAllGraphs`. Every place this is used is terrible — replace with a proper graph lookup helper. diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h index b2a1ab23..857f9cf1 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPAssetFinder.h @@ -12,6 +12,7 @@ class UBlueprint; class UMaterial; class UMaterialInstanceConstant; class UMaterialFunction; +class UAnimationStateMachineGraph; class IAssetRegistry; /** @@ -79,6 +80,11 @@ public: static UBlueprint* LoadBlueprintOrLevelBlueprint(FAssetData& Asset, MCPErrorCallback Error); static UBlueprint* LoadBlueprintOrLevelBlueprint(const FString& NameOrPath, MCPErrorCallback Error); + // Load an anim blueprint and find a state machine graph within it. + // Callers can recover the AnimBP via SMGraph->GetTypedOuter(). + static UAnimationStateMachineGraph* LoadAnimStateMachineGraph( + const FString& BlueprintName, const FString& GraphName, MCPErrorCallback Error); + private: // Fetch assets from the Unreal asset registry and store them locally. void CacheAssets(IAssetRegistry &Registry, UClass *Class, bool IncludeSubclasses); diff --git a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h index e4a0208c..e53b396f 100644 --- a/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h +++ b/Plugins/BlueprintMCP/Source/BlueprintMCP/Public/MCPUtils.h @@ -152,7 +152,7 @@ public: // ----- Anim blueprint helpers ----- static UAnimationStateMachineGraph* FindStateMachineGraph(UBlueprint* BP, const FString& GraphName); - static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName); + static UAnimStateNode* FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName, MCPErrorCallback Error); static UAnimStateTransitionNode* FindTransition(UAnimationStateMachineGraph* SMGraph, const FString& FromStateName, const FString& ToStateName); // ----- Node spawners -----