Files
integration/Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp
2026-03-15 05:52:11 -04:00

1196 lines
36 KiB
C++

#include "MCPUtils.h"
#include "MCPTypes.h"
#include "MCPServer.h"
#include "MCPHandler.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Engine/Blueprint.h"
#include "Engine/MemberReference.h"
#include "Engine/World.h"
#include "Components/ActorComponent.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "Misc/Paths.h"
#include "Misc/PackageName.h"
// Animation Blueprint support
#include "Animation/AnimBlueprint.h"
#include "Animation/Skeleton.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_AssetPlayerBase.h"
#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 "AnimationGraph.h"
#include "AnimationTransitionGraph.h"
// Material support
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
#include "Materials/MaterialExpressionConstant4Vector.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionComponentMask.h"
#include "Materials/MaterialExpressionCustom.h"
#include "Materials/MaterialExpressionFunctionInput.h"
#include "Materials/MaterialExpressionFunctionOutput.h"
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialInstanceConstant.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "IMaterialEditor.h"
#include "MaterialEditingLibrary.h"
#include "Subsystems/AssetEditorSubsystem.h"
// Mesh, animation, texture support
#include "Engine/StaticMesh.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "Engine/Texture.h"
// SEH support (Windows only) — defined in BlueprintMCPServer.cpp
#if PLATFORM_WINDOWS
extern int32 TryCompileBlueprintSEH(UBlueprint* BP, EBlueprintCompileOptions Opts);
extern int32 TrySavePackageSEH(
UPackage* Package, UObject* Asset, const TCHAR* Filename,
FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult);
#endif
// ============================================================
// Name Formatting
// ============================================================
void MCPUtils::SanitizeNameInPlace(FString &Name)
{
int32 Dst = 0;
for (int32 Src = 0; Src < Name.Len(); Src++)
{
TCHAR c = Name[Src];
if (c <= 0x20 || c == '_' || c == 0x7F) continue;
if (c >= 0x21 && c <= 0x7E && !FChar::IsAlnum(c))
Name[Dst++] = '_';
else
Name[Dst++] = c;
}
Name.LeftInline(Dst);
if (Name.IsEmpty()) Name = TEXT("_");
}
FString MCPUtils::FormatName(const UWorld *World)
{
return World->GetPathName();
}
FString MCPUtils::FormatName(const UBlueprint *BP)
{
return BP->GetPathName();
}
FString MCPUtils::FormatName(const UActorComponent *C)
{
return C->GetName();
}
FString MCPUtils::FormatName(const UEdGraph *Graph)
{
FString Name = Graph->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const UEdGraphNode* Node)
{
return Node->GetName();
}
FString MCPUtils::FormatName(const UEdGraphPin *Pin)
{
FString Name = Pin->PinName.ToString();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const FMemberReference &Ref)
{
FString Name = Ref.GetMemberName().ToString();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const FBPVariableDescription &Var)
{
FString Name = Var.VarName.ToString();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const UStruct *Struct)
{
FString Name = Struct->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const UMaterial *Material)
{
return Material->GetPathName();
}
FString MCPUtils::FormatName(const UMaterialInstance *MaterialInstance)
{
return MaterialInstance->GetPathName();
}
FString MCPUtils::FormatName(const UMaterialFunction *MaterialFunction)
{
return MaterialFunction->GetPathName();
}
FString MCPUtils::FormatName(const UMaterialExpression *Expression)
{
FString Name = Expression->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const UStaticMesh *Mesh)
{
return Mesh->GetPathName();
}
FString MCPUtils::FormatName(const USkeletalMesh *Mesh)
{
return Mesh->GetPathName();
}
FString MCPUtils::FormatName(const UAnimSequence *Anim)
{
return Anim->GetPathName();
}
FString MCPUtils::FormatName(const UBlendSpace *BlendSpace)
{
return BlendSpace->GetPathName();
}
FString MCPUtils::FormatName(const UTexture *Texture)
{
return Texture->GetPathName();
}
FString MCPUtils::FormatName(const UScriptStruct *Struct)
{
FString Name = Struct->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const UEnum *Enum)
{
FString Name = Enum->GetName();
SanitizeNameInPlace(Name);
return Name;
}
FString MCPUtils::FormatName(const FProperty *Prop)
{
return Prop->GetName();
}
// ============================================================
// Identifies
// ============================================================
// Most types are handled by the template in MCPUtils.h.
// UEdGraphNode also matches by GUID:
bool MCPUtils::Identifies(const FString &Name, const UEdGraphNode* Node)
{
if (Node->NodeGuid.ToString().Equals(Name, ESearchCase::IgnoreCase))
return true;
return FormatName(Node).Equals(Name, ESearchCase::IgnoreCase);
}
// ============================================================
// Formatting other things
// ============================================================
FString MCPUtils::FormatNodeTitle(const UEdGraphNode *Node)
{
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
int32 NewlineIdx;
if (Title.FindChar(TEXT('\n'), NewlineIdx))
Title.LeftInline(NewlineIdx);
return Title;
}
// ============================================================
// JSON helpers
// ============================================================
// ============================================================
// Text formatting
// ============================================================
FString MCPUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix)
{
FString Clean = Text;
Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n"));
TArray<FString> Words;
Clean.ParseIntoArrayWS(Words);
TStringBuilder<1024> Result;
int32 Col = 0;
for (const FString& Word : Words)
{
if (Col > 0 && Col + 1 + Word.Len() > ColLimit)
{
Result.Append(TEXT("\n"));
Col = 0;
}
if (Col == 0)
{
Result.Append(Prefix);
Col = Prefix.Len();
}
else
{
Result.Append(TEXT(" "));
Col += 1;
}
Result.Append(Word);
Col += Word.Len();
}
return Result.ToString();
}
// ============================================================
// Enum helpers
// ============================================================
FString MCPUtils::EnumToString(UEnum* Enum, int64 Value, const FString& Prefix)
{
FString Full = Enum->GetNameStringByValue(Value);
if (!Prefix.IsEmpty() && Full.StartsWith(Prefix))
return Full.Mid(Prefix.Len());
return Full;
}
bool MCPUtils::StringToEnum(UEnum* Enum, const FString& Str, int64& OutValue, const FString& Prefix)
{
OutValue = Enum->GetValueByNameString(Prefix + Str);
if (OutValue == INDEX_NONE)
{
UMCPServer::Printf(TEXT("ERROR: Invalid value '%s' for %s\n"), *Str, *Enum->GetName());
return false;
}
return true;
}
// ============================================================
// Blueprint helpers
// ============================================================
TArray<UEdGraph*> MCPUtils::AllGraphs(UBlueprint* BP)
{
TArray<UEdGraph*> Graphs;
BP->GetAllGraphs(Graphs);
return Graphs;
}
TArray<UEdGraph*> MCPUtils::AllGraphsNamed(UBlueprint* BP, const FString& Name)
{
TArray<UEdGraph*> Result;
for (UEdGraph* Graph : AllGraphs(BP))
if (Identifies(Name, Graph))
Result.Add(Graph);
return Result;
}
TArray<UEdGraphNode*> MCPUtils::AllNodes(UBlueprint* BP)
{
TArray<UEdGraphNode*> Nodes;
for (UEdGraph* Graph : AllGraphs(BP))
Nodes.Append(Graph->Nodes);
return Nodes;
}
bool MCPUtils::SaveBlueprintPackage(UBlueprint* BP)
{
UPackage* Package = BP->GetPackage();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveBlueprintPackage — begin for '%s'"), *BP->GetName());
// 1. Build absolute package filename — use .umap for map packages, .uasset otherwise
FString PackageExtension = Package->ContainsMap()
? FPackageName::GetMapPackageExtension()
: FPackageName::GetAssetPackageExtension();
FString PackageFilename = FPackageName::LongPackageNameToFilename(
Package->GetName(), PackageExtension);
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Save target: %s"), *PackageFilename);
// 2. Phase 1: Try explicit compilation (same flags as UCompileAllBlueprintsCommandlet)
bool bCompiled = false;
{
EBlueprintCompileOptions CompileOpts =
EBlueprintCompileOptions::SkipSave |
EBlueprintCompileOptions::BatchCompile |
EBlueprintCompileOptions::SkipGarbageCollection |
EBlueprintCompileOptions::SkipFiBSearchMetaUpdate;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Phase 1: Attempting explicit compilation..."));
#if PLATFORM_WINDOWS
int32 CompileResult = TryCompileBlueprintSEH(BP, CompileOpts);
if (CompileResult == 0)
{
bCompiled = (BP->Status == BS_UpToDate);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Compilation %s (status=%d)"),
bCompiled ? TEXT("succeeded") : TEXT("completed with warnings"), (int32)BP->Status);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Compilation crashed (SEH), proceeding uncompiled"));
}
#else
FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr);
bCompiled = (BP->Status == BS_UpToDate);
#endif
}
// 3. Phase 2: Set guards for save
uint8 OldRegen = BP->bIsRegeneratingOnLoad;
BP->bIsRegeneratingOnLoad = true;
EBlueprintStatus OldStatus = (EBlueprintStatus)(uint8)BP->Status;
if (!bCompiled)
{
// Tell PreSave the BP is up-to-date so it doesn't try to compile
BP->Status = BS_UpToDate;
}
// 4. Clear read-only attribute if present (source control or LFS may set this)
if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename))
{
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Clearing read-only attribute on %s"), *PackageFilename);
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false);
}
// 5. Phase 3: Save with SAVE_NoError + SEH protection
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.SaveFlags = SAVE_NoError;
// For level blueprints (map packages), the base object should be the UWorld, not the BP
bool bIsMapPackage = Package->ContainsMap();
UObject* BaseObject = BP;
if (bIsMapPackage)
{
// Find the UWorld in this package — it's the actual asset for .umap files
UWorld* World = FindObject<UWorld>(Package, *Package->GetName().Mid(Package->GetName().Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromEnd) + 1));
if (!World)
{
// Fallback: iterate the package to find any UWorld
ForEachObjectWithPackage(Package, [&World](UObject* Obj) {
if (UWorld* W = Cast<UWorld>(Obj))
{
World = W;
return false; // stop
}
return true; // continue
});
}
if (World)
{
BaseObject = World;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Map package detected — saving UWorld '%s'"), *World->GetName());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Map package detected but no UWorld found — saving with BP as base"));
}
}
ESavePackageResult SaveResult = ESavePackageResult::Error;
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Phase 3: Calling UPackage::Save (compiled=%s, isMap=%s)..."),
bCompiled ? TEXT("yes") : TEXT("no"), bIsMapPackage ? TEXT("yes") : TEXT("no"));
#if PLATFORM_WINDOWS
int32 SEHCode = TrySavePackageSEH(Package, BaseObject, *PackageFilename, &SaveArgs, &SaveResult);
if (SEHCode != 0)
{
UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: UPackage::Save CRASHED (SEH exception caught)"));
}
#else
FSavePackageResultStruct Result = UPackage::Save(Package, BaseObject, *PackageFilename, SaveArgs);
SaveResult = Result.Result;
#endif
// 6. Restore guards
BP->bIsRegeneratingOnLoad = OldRegen;
if (!bCompiled)
{
BP->Status = (TEnumAsByte<EBlueprintStatus>)OldStatus;
}
bool bSuccess = (SaveResult == ESavePackageResult::Success);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveBlueprintPackage — %s for '%s' (compiled=%s, result=%d)"),
bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"),
*BP->GetName(), bCompiled ? TEXT("yes") : TEXT("no"), (int32)SaveResult);
return bSuccess;
}// ============================================================
// FindClassByName
// ============================================================
UClass* MCPUtils::FindClassByName(const FString& ClassName)
{
// Exact match first (handles both C++ classes and Blueprint _C classes)
for (TObjectIterator<UClass> It; It; ++It)
{
FString Name = It->GetName();
if (Name == ClassName || Name == ClassName + TEXT("_C"))
{
return *It;
}
}
// Case-insensitive fallback
for (TObjectIterator<UClass> It; It; ++It)
{
FString Name = It->GetName();
if (Name.Equals(ClassName, ESearchCase::IgnoreCase) ||
Name.Equals(ClassName + TEXT("_C"), ESearchCase::IgnoreCase))
{
return *It;
}
}
return nullptr;
}
// ============================================================
// Material helpers
// ============================================================
void MCPUtils::EnsureMaterialGraph(UMaterial* Material)
{
if (!Material) return;
if (!Material->MaterialGraph)
{
// In commandlet/headless mode the MaterialGraph is not auto-created.
// Replicate what the Material Editor does on open (MaterialEditor.cpp:619).
Material->MaterialGraph = CastChecked<UMaterialGraph>(
FBlueprintEditorUtils::CreateNewGraph(
Material, NAME_None,
UMaterialGraph::StaticClass(),
UMaterialGraphSchema::StaticClass()));
Material->MaterialGraph->Material = Material;
Material->MaterialGraph->RebuildGraph();
}
}
UMaterial* MCPUtils::ReplaceMaterialWithTransientCopy(UMaterial* Material)
{
if (!Material) return nullptr;
// Already a preview material — nothing to do.
if (Material->GetOutermost() == GetTransientPackage())
return Material;
// If the material editor has a transient preview copy open, get it
// via the editor API. This follows the same pattern as Epic's
// MaterialEditingLibrary (FindMaterialEditorForAsset).
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
IAssetEditorInstance* EditorInstance = Sub ? Sub->FindEditorForAsset(Material, false) : nullptr;
if (EditorInstance)
{
// This is a weird hack. We know that the IAssetEditorInstance for a material
// is always going to be an FMaterialEditor, which conforms to IMaterialEditor.
// If that weren't the case, this unsafe code would crash hard. However,
// lots of places in unreal use this same unsafe pattern.
IMaterialEditor* MatEditor = static_cast<IMaterialEditor*>(EditorInstance);
UMaterialInterface* Edited = MatEditor->GetMaterialInterface();
if (UMaterial* EditedMat = Cast<UMaterial>(Edited))
return EditedMat;
}
return Material;
}
TMap<FMaterialParameterInfo, FMaterialParameterMetadata> MCPUtils::GetMaterialParameters(UMaterialInterface* Material)
{
TMap<FMaterialParameterInfo, FMaterialParameterMetadata> Result;
if (!Material) return Result;
TMap<FMaterialParameterInfo, FMaterialParameterMetadata> Temp;
for (int32 i = 0; i < (int32)EMaterialParameterType::NumRuntime; i++)
{
Material->GetAllParametersOfType((EMaterialParameterType)i, Temp);
Result.Append(Temp);
}
return Result;
}
bool MCPUtils::ParseMaterialParameterAssociation(const FString& Str, EMaterialParameterAssociation& OutAssociation)
{
if (Str.Equals(TEXT("Global"), ESearchCase::IgnoreCase))
OutAssociation = GlobalParameter;
else if (Str.Equals(TEXT("Layer"), ESearchCase::IgnoreCase))
OutAssociation = LayerParameter;
else if (Str.Equals(TEXT("Blend"), ESearchCase::IgnoreCase))
OutAssociation = BlendParameter;
else
{
UMCPServer::Printf(TEXT("ERROR: Invalid ParameterAssociation '%s' (expected 'Global', 'Layer', or 'Blend')\n"), *Str);
return false;
}
return true;
}
void MCPUtils::FormatMaterialParameter(const FMaterialParameterInfo& Info, const FMaterialParameterMetadata& Meta)
{
// Association prefix for layer/blend parameters.
FString Prefix;
if (Info.Association == LayerParameter)
Prefix = FString::Printf(TEXT("[Layer %d] "), Info.Index);
else if (Info.Association == BlendParameter)
Prefix = FString::Printf(TEXT("[Blend %d] "), Info.Index);
switch (Meta.Value.Type)
{
case EMaterialParameterType::Scalar:
UMCPServer::Printf(TEXT(" %sScalar \"%s\" = %g\n"), *Prefix, *Info.Name.ToString(), Meta.Value.AsScalar());
break;
case EMaterialParameterType::Vector:
{
FLinearColor C = Meta.Value.AsLinearColor();
UMCPServer::Printf(TEXT(" %sVector \"%s\" = (R=%.3f, G=%.3f, B=%.3f, A=%.3f)\n"),
*Prefix, *Info.Name.ToString(), C.R, C.G, C.B, C.A);
break;
}
case EMaterialParameterType::DoubleVector:
{
FVector4d V = Meta.Value.AsVector4d();
UMCPServer::Printf(TEXT(" %sDoubleVector \"%s\" = (%.3f, %.3f, %.3f, %.3f)\n"),
*Prefix, *Info.Name.ToString(), V.X, V.Y, V.Z, V.W);
break;
}
case EMaterialParameterType::Texture:
{
UTexture* Tex = Cast<UTexture>(Meta.Value.AsTextureObject());
UMCPServer::Printf(TEXT(" %sTexture \"%s\" = %s\n"),
*Prefix, *Info.Name.ToString(), Tex ? *MCPUtils::FormatName(Tex) : TEXT("None"));
break;
}
case EMaterialParameterType::StaticSwitch:
UMCPServer::Printf(TEXT(" %sStaticSwitch \"%s\" = %s\n"),
*Prefix, *Info.Name.ToString(), Meta.Value.AsStaticSwitch() ? TEXT("true") : TEXT("false"));
break;
default:
UMCPServer::Printf(TEXT(" %sType%d \"%s\"\n"), *Prefix, (int)Meta.Value.Type, *Info.Name.ToString());
break;
}
}
bool MCPUtils::SaveGenericPackage(UObject* Asset)
{
if (!Asset) return false;
UPackage* Package = Asset->GetPackage();
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveGenericPackage — begin for '%s'"), *Asset->GetName());
FString PackageFilename = FPackageName::LongPackageNameToFilename(
Package->GetName(), FPackageName::GetAssetPackageExtension());
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename))
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false);
}
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.SaveFlags = SAVE_NoError;
ESavePackageResult SaveResult = ESavePackageResult::Error;
#if PLATFORM_WINDOWS
int32 SEHCode = TrySavePackageSEH(Package, Asset, *PackageFilename, &SaveArgs, &SaveResult);
if (SEHCode != 0)
{
UE_LOG(LogTemp, Error, TEXT("BlueprintMCP: SaveGenericPackage CRASHED (SEH exception)"));
}
#else
FSavePackageResultStruct Result = UPackage::Save(Package, Asset, *PackageFilename, SaveArgs);
SaveResult = Result.Result;
#endif
bool bSuccess = (SaveResult == ESavePackageResult::Success);
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SaveGenericPackage — %s for '%s'"),
bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"), *Asset->GetName());
return bSuccess;
}
// ============================================================
// Anim blueprint helpers
// ============================================================
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
UAnimationStateMachineGraph* MCPUtils::FindStateMachineGraph(UBlueprint* BP, const FString& GraphName)
{
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (UAnimationStateMachineGraph* SMGraph = Cast<UAnimationStateMachineGraph>(Graph))
{
if (SMGraph->GetName() == GraphName)
{
return SMGraph;
}
}
}
return nullptr;
}
UAnimStateNode* MCPUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateNode* StateNode = Cast<UAnimStateNode>(Node))
{
if (StateNode->GetStateName() == StateName)
{
return StateNode;
}
}
}
UMCPServer::Printf(TEXT("ERROR: State '%s' not found in graph '%s'\n"), *StateName, *SMGraph->GetName());
return nullptr;
}
UAnimStateTransitionNode* MCPUtils::FindTransition(UAnimationStateMachineGraph* SMGraph,
const FString& FromStateName, const FString& ToStateName)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
{
UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState());
UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState());
if (FromState && ToState &&
(FromState->GetStateName() == FromStateName) &&
(ToState->GetStateName() == ToStateName))
{
return TransNode;
}
}
}
return nullptr;
}
// ============================================================
// Graph actions (node spawning)
// ============================================================
#include "EdGraph/EdGraphSchema.h"
FString MCPUtils::ActionFullName(const TSharedPtr<FEdGraphSchemaAction>& Action)
{
FString Category = Action->GetCategory().ToString();
FString MenuName = Action->GetMenuDescription().ToString();
if (Category.IsEmpty())
return MenuName;
return Category + TEXT("|") + MenuName;
}
TArray<TSharedPtr<FEdGraphSchemaAction>> MCPUtils::SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults, bool ExactMatch)
{
FString QueryLower = Query.ToLower();
TArray<TSharedPtr<FEdGraphSchemaAction>> Result;
FGraphContextMenuBuilder ContextMenuBuilder(Graph);
Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder);
for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++)
{
TSharedPtr<FEdGraphSchemaAction> Action = ContextMenuBuilder.GetSchemaAction(i);
if (!Action.IsValid()) continue;
FString FullName = ActionFullName(Action);
if (FullName.IsEmpty()) continue;
if (ExactMatch)
{
if (FullName.ToLower() != QueryLower)
continue;
}
else
{
FString Keywords = Action->GetKeywords().ToString();
if (!FullName.ToLower().Contains(QueryLower) && !Keywords.ToLower().Contains(QueryLower))
continue;
}
Result.Add(Action);
if ((MaxResults > 0) && (Result.Num() >= MaxResults))
break;
}
return Result;
}
// ============================================================
// PopulateFromJson — fill a USTRUCT from a JSON object
// ============================================================
#include "UObject/UnrealType.h"
#include "UObject/EnumProperty.h"
FString MCPUtils::PropertyNameToJsonKey(const FString& PropName)
{
if (PropName.IsEmpty())
{
return PropName;
}
FString Result = PropName;
Result[0] = FChar::ToLower(Result[0]);
return Result;
}
FString MCPUtils::SetPropertyFromJson(
void* Container, FProperty* Prop, const FString& FieldName, const FJsonObject* Json)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
// FString
if (FStrProperty* StrProp = CastField<FStrProperty>(Prop))
{
if (!Json->HasTypedField<EJson::String>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
}
StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName));
return FString();
}
// int32
if (FIntProperty* IntProp = CastField<FIntProperty>(Prop))
{
if (!Json->HasTypedField<EJson::Number>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
}
IntProp->SetPropertyValue(ValuePtr, (int32)Json->GetNumberField(FieldName));
return FString();
}
// float
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(Prop))
{
if (!Json->HasTypedField<EJson::Number>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
}
FloatProp->SetPropertyValue(ValuePtr, (float)Json->GetNumberField(FieldName));
return FString();
}
// double
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(Prop))
{
if (!Json->HasTypedField<EJson::Number>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
}
DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName));
return FString();
}
// bool
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(Prop))
{
if (!Json->HasTypedField<EJson::Boolean>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a boolean"), *FieldName);
}
BoolProp->SetPropertyValue(ValuePtr, Json->GetBoolField(FieldName));
return FString();
}
// Enum (FEnumProperty — C++ enum class)
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
{
if (!Json->HasTypedField<EJson::String>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
}
FString ValueStr = Json->GetStringField(FieldName);
UEnum* Enum = EnumProp->GetEnum();
int64 EnumVal = Enum->GetValueByNameString(ValueStr);
if (EnumVal == INDEX_NONE)
{
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
}
FNumericProperty* UnderlyingProp = EnumProp->GetUnderlyingProperty();
UnderlyingProp->SetIntPropertyValue(ValuePtr, EnumVal);
return FString();
}
// Enum (FByteProperty with Enum — old-style UENUM)
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
{
if (ByteProp->Enum)
{
if (!Json->HasTypedField<EJson::String>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a string"), *FieldName);
}
FString ValueStr = Json->GetStringField(FieldName);
int64 EnumVal = ByteProp->Enum->GetValueByNameString(ValueStr);
if (EnumVal == INDEX_NONE)
{
return FString::Printf(TEXT("'%s': unknown enum value '%s'"), *FieldName, *ValueStr);
}
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumVal);
return FString();
}
// Plain byte without enum — treat as number
if (!Json->HasTypedField<EJson::Number>(FieldName))
{
return FString::Printf(TEXT("'%s' must be a number"), *FieldName);
}
ByteProp->SetPropertyValue(ValuePtr, (uint8)Json->GetNumberField(FieldName));
return FString();
}
// FMCPJsonObject — stash a JSON object into the struct
if (FStructProperty* StructProp = CastField<FStructProperty>(Prop))
{
if (StructProp->Struct == FMCPJsonObject::StaticStruct())
{
if (!Json->HasTypedField<EJson::Object>(FieldName))
{
return FString::Printf(TEXT("'%s' must be an object"), *FieldName);
}
FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr<FMCPJsonObject>(Container);
Obj->Json = Json->GetObjectField(FieldName);
return FString();
}
// FMCPJsonArray — stash a JSON array into the struct
if (StructProp->Struct == FMCPJsonArray::StaticStruct())
{
if (!Json->HasTypedField<EJson::Array>(FieldName))
{
return FString::Printf(TEXT("'%s' must be an array"), *FieldName);
}
FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr<FMCPJsonArray>(Container);
Arr->Array = Json->GetArrayField(FieldName);
return FString();
}
}
return FString::Printf(TEXT("'%s': unsupported property type '%s'"),
*FieldName, *Prop->GetCPPType());
}
bool MCPUtils::PopulateFromJson(
UStruct* StructType,
void* Container,
const TSharedPtr<FJsonValue>& JsonValue)
{
if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
{
UMCPServer::Print(TEXT("ERROR: Expected a JSON object\n"));
return false;
}
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get());
}
bool MCPUtils::PopulateFromJson(
UStruct* StructType,
void* Container,
const FJsonObject* Json)
{
// Build a set of known property names (as JSON keys) for the unknown-field check.
TSet<FString> KnownKeys;
TArray<FProperty*> Properties;
for (TFieldIterator<FProperty> It(StructType, EFieldIterationFlags::None); It; ++It)
{
FProperty* Prop = *It;
Properties.Add(Prop);
KnownKeys.Add(PropertyNameToJsonKey(Prop->GetName()));
}
// Check for unknown fields in the JSON
for (const auto& KV : Json->Values)
{
if (!KnownKeys.Contains(KV.Key))
{
UMCPServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
return false;
}
}
// Populate each property from JSON
for (FProperty* Prop : Properties)
{
FString JsonKey = PropertyNameToJsonKey(Prop->GetName());
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
if (!Json->HasField(JsonKey))
{
if (!bOptional)
{
UMCPServer::Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
return false;
}
continue;
}
FString PropError = SetPropertyFromJson(Container, Prop, JsonKey, Json);
if (!PropError.IsEmpty())
{
UMCPServer::Printf(TEXT("ERROR: %s\n"), *PropError);
return false;
}
}
return true;
}
// ============================================================
// CollectHandlerClasses — find all concrete IMCPHandler classes
// ============================================================
TArray<UClass*> MCPUtils::CollectHandlerClasses()
{
TArray<UClass*> Result;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
if (!Class->ImplementsInterface(UMCPHandler::StaticClass())) continue;
Result.Add(Class);
}
Result.Sort([](UClass& A, UClass& B) { return GetHandlerName(&A) < GetHandlerName(&B); });
return Result;
}
// ============================================================
// GetHandlerName — derive tool name from handler class name
// ============================================================
FString MCPUtils::GetHandlerName(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
// Strip "MCP_" prefix
if (Name.StartsWith(TEXT("MCP_")))
Name = Name.Mid(4);
return Name;
}
// ============================================================
// GetHandlerGroup — derive group name from handler class name
// ============================================================
FString MCPUtils::GetHandlerGroup(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
// Strip "MCP_" prefix
if (Name.StartsWith(TEXT("MCP_")))
Name = Name.Mid(4);
// Everything before the underscore is the group
int32 UnderscoreIdx;
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
return Name.Left(UnderscoreIdx);
return Name;
}
// ============================================================
// GetTemplate
// ============================================================
// ============================================================
// FindPropertyByName
// ============================================================
FProperty* MCPUtils::FindPropertyByName(UObject* Obj, const FString& Name)
{
if (!Obj)
{
UMCPServer::Print(TEXT("ERROR: Object is null\n"));
return nullptr;
}
FProperty* Found = nullptr;
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
{
if (!Identifies(Name, *PropIt)) continue;
if (Found)
{
UMCPServer::Printf(TEXT("ERROR: Ambiguous property '%s' on %s\n"), *Name, *FormatName(Obj->GetClass()));
return nullptr;
}
Found = *PropIt;
}
if (!Found)
UMCPServer::Printf(TEXT("ERROR: Property '%s' not found on %s\n"), *Name, *FormatName(Obj->GetClass()));
return Found;
}
// ============================================================
// GetPropertyValueText
// ============================================================
FString MCPUtils::GetPropertyValueText(UObject* Container, FProperty* Prop)
{
FString Result;
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, Container, PPF_None);
return Result;
}
// ============================================================
// SetPropertyValueText
// ============================================================
bool MCPUtils::SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Container, PPF_None);
if (!ImportResult)
{
UMCPServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
bool MCPUtils::SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Owner, PPF_None);
if (!ImportResult)
{
UMCPServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
// ============================================================
// SearchProperties
// ============================================================
TArray<FProperty*> MCPUtils::SearchProperties(UObject* Obj, const FString& Query, EPropertyFlags Flags, bool bLocal)
{
TArray<FProperty*> Result;
if (!Obj) return Result;
UClass* ObjClass = Obj->GetClass();
for (TFieldIterator<FProperty> PropIt(ObjClass); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
if (Flags != 0 && !Prop->HasAnyPropertyFlags(Flags)) continue;
if (bLocal && Prop->GetOwnerStruct() != ObjClass) continue;
if (!Query.IsEmpty() && !FormatName(Prop).Contains(Query, ESearchCase::IgnoreCase))
continue;
Result.Add(Prop);
}
return Result;
}
// ============================================================
// FormatCommandHelp — verbose description of one handler command
// ============================================================
void MCPUtils::FormatCommandHelp(UClass* HandlerClass)
{
const IMCPHandler* Handler = Cast<IMCPHandler>(HandlerClass->GetDefaultObject());
if (!Handler) return;
FString ToolName = GetHandlerName(HandlerClass);
UMCPServer::Print(TEXT("\n"));
UMCPServer::Print(WrapText(Handler->GetDescription(), 80, TEXT("// ")));
UMCPServer::Print(TEXT("\n"));
// Command signature line
UMCPServer::Print(ToolName);
UMCPServer::Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) UMCPServer::Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) UMCPServer::Print(TEXT("?"));
UMCPServer::Print(PropertyNameToJsonKey(PropIt->GetName()));
}
UMCPServer::Print(TEXT(")\n"));
// parameter details
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
FString Name = PropertyNameToJsonKey(Prop->GetName());
FString Type = UMCPTypes::TypeToText(Prop);
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
UMCPServer::Printf(TEXT(" %s %s%s"),
*Type, *Name, bOptional ? TEXT(" (optional)") : TEXT(""));
if (!Desc.IsEmpty())
UMCPServer::Printf(TEXT(" — %s"), *Desc);
UMCPServer::Print(TEXT("\n"));
}
}