#include "MCPUtils.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 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 MCPUtils::AllGraphs(UBlueprint* BP) { TArray Graphs; BP->GetAllGraphs(Graphs); return Graphs; } TArray MCPUtils::AllGraphsNamed(UBlueprint* BP, const FString& Name) { TArray Result; for (UEdGraph* Graph : AllGraphs(BP)) if (Identifies(Name, Graph)) Result.Add(Graph); return Result; } TArray MCPUtils::AllNodes(UBlueprint* BP) { TArray 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(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(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)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 / ResolveTypeFromString // ============================================================ UClass* MCPUtils::FindClassByName(const FString& ClassName) { // Exact match first (handles both C++ classes and Blueprint _C classes) for (TObjectIterator It; It; ++It) { FString Name = It->GetName(); if (Name == ClassName || Name == ClassName + TEXT("_C")) { return *It; } } // Case-insensitive fallback for (TObjectIterator It; It; ++It) { FString Name = It->GetName(); if (Name.Equals(ClassName, ESearchCase::IgnoreCase) || Name.Equals(ClassName + TEXT("_C"), ESearchCase::IgnoreCase)) { return *It; } } return nullptr; } bool MCPUtils::ResolveTypeFromString( const FString& TypeName, FEdGraphPinType& OutPinType) { FString TypeLower = TypeName.ToLower(); if (TypeLower == TEXT("bool") || TypeLower == TEXT("boolean")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Boolean; } else if (TypeLower == TEXT("int") || TypeLower == TEXT("int32") || TypeLower == TEXT("integer")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int; } else if (TypeLower == TEXT("int64")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Int64; } else if (TypeLower == TEXT("float") || TypeLower == TEXT("double") || TypeLower == TEXT("real")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Real; OutPinType.PinSubCategory = TEXT("double"); } else if (TypeLower == TEXT("string")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_String; } else if (TypeLower == TEXT("name")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Name; } else if (TypeLower == TEXT("text")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Text; } else if (TypeLower == TEXT("byte")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Byte; } else if (TypeLower == TEXT("vector") || TypeLower == TEXT("fvector")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; OutPinType.PinSubCategoryObject = TBaseStructure::Get(); } else if (TypeLower == TEXT("rotator") || TypeLower == TEXT("frotator")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; OutPinType.PinSubCategoryObject = TBaseStructure::Get(); } else if (TypeLower == TEXT("transform") || TypeLower == TEXT("ftransform")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; OutPinType.PinSubCategoryObject = TBaseStructure::Get(); } else if (TypeLower == TEXT("linearcolor") || TypeLower == TEXT("flinearcolor")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; OutPinType.PinSubCategoryObject = TBaseStructure::Get(); } else if (TypeLower == TEXT("vector2d") || TypeLower == TEXT("fvector2d")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; OutPinType.PinSubCategoryObject = TBaseStructure::Get(); } else if (TypeLower == TEXT("object")) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; OutPinType.PinSubCategoryObject = UObject::StaticClass(); } else if (TypeName.StartsWith(TEXT("object:"), ESearchCase::IgnoreCase)) { FString ClassName = TypeName.Mid(7); // after "object:" UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { UMCPServer::Printf(TEXT("ERROR: Class '%s' not found for object reference type\n"), *ClassName); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_Object; OutPinType.PinSubCategoryObject = FoundClass; } else if (TypeName.StartsWith(TEXT("softobject:"), ESearchCase::IgnoreCase)) { FString ClassName = TypeName.Mid(11); // after "softobject:" UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { UMCPServer::Printf(TEXT("ERROR: Class '%s' not found for soft object reference type\n"), *ClassName); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftObject; OutPinType.PinSubCategoryObject = FoundClass; } else if (TypeName.StartsWith(TEXT("class:"), ESearchCase::IgnoreCase)) { FString ClassName = TypeName.Mid(6); // after "class:" UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { UMCPServer::Printf(TEXT("ERROR: Class '%s' not found for class reference type (TSubclassOf)\n"), *ClassName); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_Class; OutPinType.PinSubCategoryObject = FoundClass; } else if (TypeName.StartsWith(TEXT("softclass:"), ESearchCase::IgnoreCase)) { FString ClassName = TypeName.Mid(10); // after "softclass:" UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { UMCPServer::Printf(TEXT("ERROR: Class '%s' not found for soft class reference type\n"), *ClassName); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_SoftClass; OutPinType.PinSubCategoryObject = FoundClass; } else if (TypeName.StartsWith(TEXT("interface:"), ESearchCase::IgnoreCase)) { FString ClassName = TypeName.Mid(10); // after "interface:" UClass* FoundClass = FindClassByName(ClassName); if (!FoundClass) { UMCPServer::Printf(TEXT("ERROR: Class '%s' not found for interface reference type\n"), *ClassName); return false; } OutPinType.PinCategory = UEdGraphSchema_K2::PC_Interface; OutPinType.PinSubCategoryObject = FoundClass; } else { // Try as a struct (F-prefix or raw name) FString InternalName = TypeName; bool bTriedAsStruct = false; if (TypeName.StartsWith(TEXT("F")) || TypeName.StartsWith(TEXT("S_")) || (!TypeName.StartsWith(TEXT("E")))) { if (TypeName.StartsWith(TEXT("F"))) { InternalName = TypeName.Mid(1); } UScriptStruct* FoundStruct = FindFirstObject(*InternalName); if (!FoundStruct) { for (TObjectIterator It; It; ++It) { if (It->GetName() == InternalName || It->GetName() == TypeName) { FoundStruct = *It; break; } } } if (FoundStruct) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct; OutPinType.PinSubCategoryObject = FoundStruct; bTriedAsStruct = true; } } if (!bTriedAsStruct) { // Try as an enum (E-prefix or raw name) FString EnumInternalName = TypeName; if (TypeName.StartsWith(TEXT("E"))) { EnumInternalName = TypeName.Mid(1); } UEnum* FoundEnum = FindFirstObject(*EnumInternalName); if (!FoundEnum) { for (TObjectIterator It; It; ++It) { if (It->GetName() == EnumInternalName || It->GetName() == TypeName) { FoundEnum = *It; break; } } } if (FoundEnum) { if (FoundEnum->GetCppForm() == UEnum::ECppForm::EnumClass) { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Enum; } else { OutPinType.PinCategory = UEdGraphSchema_K2::PC_Byte; } OutPinType.PinSubCategoryObject = FoundEnum; } else { UMCPServer::Printf( TEXT("ERROR: Unknown type '%s'. Use: bool, int, float, string, name, text, byte, vector, rotator, transform, object, a struct/enum name (e.g. FVector, EMyEnum), or colon syntax for references (object:Actor, softobject:Actor, class:Actor, softclass:Actor, interface:MyInterface)\n"), *TypeName); return false; } } } return true; } // ============================================================ // 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( 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(); 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(EditorInstance); UMaterialInterface* Edited = MatEditor->GetMaterialInterface(); if (UMaterial* EditedMat = Cast(Edited)) return EditedMat; } return Material; } TMap MCPUtils::GetMaterialParameters(UMaterialInterface* Material) { TMap Result; if (!Material) return Result; TMap 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(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 AllGraphs; BP->GetAllGraphs(AllGraphs); for (UEdGraph* Graph : AllGraphs) { if (UAnimationStateMachineGraph* SMGraph = Cast(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(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(Node)) { UAnimStateNode* FromState = Cast(TransNode->GetPreviousState()); UAnimStateNode* ToState = Cast(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& Action) { FString Category = Action->GetCategory().ToString(); FString MenuName = Action->GetMenuDescription().ToString(); if (Category.IsEmpty()) return MenuName; return Category + TEXT("|") + MenuName; } TArray> MCPUtils::SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults, bool ExactMatch) { FString QueryLower = Query.ToLower(); TArray> Result; FGraphContextMenuBuilder ContextMenuBuilder(Graph); Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder); for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++) { TSharedPtr 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(Container); // FString if (FStrProperty* StrProp = CastField(Prop)) { if (!Json->HasTypedField(FieldName)) { return FString::Printf(TEXT("'%s' must be a string"), *FieldName); } StrProp->SetPropertyValue(ValuePtr, Json->GetStringField(FieldName)); return FString(); } // int32 if (FIntProperty* IntProp = CastField(Prop)) { if (!Json->HasTypedField(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(Prop)) { if (!Json->HasTypedField(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(Prop)) { if (!Json->HasTypedField(FieldName)) { return FString::Printf(TEXT("'%s' must be a number"), *FieldName); } DoubleProp->SetPropertyValue(ValuePtr, Json->GetNumberField(FieldName)); return FString(); } // bool if (FBoolProperty* BoolProp = CastField(Prop)) { if (!Json->HasTypedField(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(Prop)) { if (!Json->HasTypedField(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(Prop)) { if (ByteProp->Enum) { if (!Json->HasTypedField(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(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(Prop)) { if (StructProp->Struct == FMCPJsonObject::StaticStruct()) { if (!Json->HasTypedField(FieldName)) { return FString::Printf(TEXT("'%s' must be an object"), *FieldName); } FMCPJsonObject* Obj = StructProp->ContainerPtrToValuePtr(Container); Obj->Json = Json->GetObjectField(FieldName); return FString(); } // FMCPJsonArray — stash a JSON array into the struct if (StructProp->Struct == FMCPJsonArray::StaticStruct()) { if (!Json->HasTypedField(FieldName)) { return FString::Printf(TEXT("'%s' must be an array"), *FieldName); } FMCPJsonArray* Arr = StructProp->ContainerPtrToValuePtr(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& 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 KnownKeys; TArray Properties; for (TFieldIterator 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 MCPUtils::CollectHandlerClasses() { TArray Result; for (TObjectIterator 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; } // ============================================================ // FormatPropertyType — human-readable type name for a UPROPERTY // ============================================================ FString MCPUtils::FormatPropertyType(FProperty* Prop) { if (CastField(Prop)) return TEXT("string"); if (CastField(Prop)) return TEXT("integer"); if (CastField(Prop) || CastField(Prop)) return TEXT("number"); if (CastField(Prop)) return TEXT("boolean"); if (FStructProperty* SP = CastField(Prop)) { FString StructName = SP->Struct->GetName(); StructName.ReplaceInline(TEXT("MCP"), TEXT("")); return StructName; } return TEXT("string"); } // ============================================================ // 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 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(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(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(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 MCPUtils::SearchProperties(UObject* Obj, const FString& Query, EPropertyFlags Flags, bool bLocal) { TArray Result; if (!Obj) return Result; UClass* ObjClass = Obj->GetClass(); for (TFieldIterator 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(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 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 PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt) { FProperty* Prop = *PropIt; FString Name = PropertyNameToJsonKey(Prop->GetName()); FString Type = FormatPropertyType(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")); } }