1958 lines
63 KiB
C++
1958 lines
63 KiB
C++
#include "MCPUtils.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
|
|
|
|
// ============================================================
|
|
// MCPErrorCallback
|
|
// ============================================================
|
|
|
|
MCPErrorCallback::MCPErrorCallback(std::nullptr_t)
|
|
: Func([](const FString&) {})
|
|
{}
|
|
|
|
MCPErrorCallback::MCPErrorCallback(FString& OutError)
|
|
: Func([&OutError](const FString& Msg) { OutError = Msg; })
|
|
{}
|
|
|
|
MCPErrorCallback::MCPErrorCallback(FStringBuilderBase& OutResult)
|
|
: Func([&OutResult](const FString& Msg) { OutResult.Appendf(TEXT("ERROR: %s\n"), *Msg); })
|
|
{}
|
|
|
|
// ============================================================
|
|
// 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::FormatPinType(const FEdGraphPinType& PinType)
|
|
{
|
|
if (UObject* SubObj = PinType.PinSubCategoryObject.Get())
|
|
{
|
|
return SubObj->GetName();
|
|
}
|
|
FString Type = PinType.PinCategory.ToString();
|
|
Type[0] = FChar::ToUpper(Type[0]);
|
|
return Type;
|
|
}
|
|
|
|
FString MCPUtils::FormatPinType(UEdGraphPin* Pin)
|
|
{
|
|
return FormatPinType(Pin->PinType);
|
|
}
|
|
|
|
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
|
|
// ============================================================
|
|
|
|
TSharedPtr<FJsonObject> MCPUtils::ParseBodyJson(const FString& Body)
|
|
{
|
|
TSharedPtr<FJsonObject> JsonObj;
|
|
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Body);
|
|
FJsonSerializer::Deserialize(Reader, JsonObj);
|
|
return JsonObj;
|
|
}
|
|
|
|
|
|
FString MCPUtils::UrlDecode(const FString& EncodedString)
|
|
{
|
|
FString Result;
|
|
Result.Reserve(EncodedString.Len());
|
|
|
|
for (int32 i = 0; i < EncodedString.Len(); ++i)
|
|
{
|
|
TCHAR C = EncodedString[i];
|
|
if (C == TEXT('+'))
|
|
{
|
|
Result += TEXT(' ');
|
|
}
|
|
else if (C == TEXT('%') && i + 2 < EncodedString.Len())
|
|
{
|
|
FString HexStr = EncodedString.Mid(i + 1, 2);
|
|
int32 HexVal = 0;
|
|
bool bValid = true;
|
|
for (TCHAR H : HexStr)
|
|
{
|
|
HexVal <<= 4;
|
|
if (H >= TEXT('0') && H <= TEXT('9'))
|
|
HexVal += H - TEXT('0');
|
|
else if (H >= TEXT('a') && H <= TEXT('f'))
|
|
HexVal += 10 + H - TEXT('a');
|
|
else if (H >= TEXT('A') && H <= TEXT('F'))
|
|
HexVal += 10 + H - TEXT('A');
|
|
else
|
|
{
|
|
bValid = false;
|
|
break;
|
|
}
|
|
}
|
|
if (bValid)
|
|
{
|
|
Result += (TCHAR)HexVal;
|
|
i += 2;
|
|
}
|
|
else
|
|
{
|
|
Result += C;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Result += C;
|
|
}
|
|
}
|
|
return Result;
|
|
}
|
|
|
|
// ============================================================
|
|
// 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;
|
|
}
|
|
|
|
TArray<TSharedPtr<FJsonValue>> MCPUtils::AllGraphNamesJson(UBlueprint* BP)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> Result;
|
|
for (UEdGraph* Graph : AllGraphs(BP))
|
|
Result.Add(MakeShared<FJsonValueString>(FormatName(Graph)));
|
|
return Result;
|
|
}
|
|
|
|
UEdGraphNode* MCPUtils::FindNodeByGuid(
|
|
UBlueprint* BP, const FString& GuidString, UEdGraph** OutGraph)
|
|
{
|
|
FGuid TargetGuid;
|
|
FGuid::Parse(GuidString, TargetGuid);
|
|
|
|
TArray<UEdGraph*> AllGraphs;
|
|
BP->GetAllGraphs(AllGraphs);
|
|
|
|
for (UEdGraph* Graph : AllGraphs)
|
|
{
|
|
for (UEdGraphNode* Node : Graph->Nodes)
|
|
{
|
|
if (Node && Node->NodeGuid == TargetGuid)
|
|
{
|
|
if (OutGraph) *OutGraph = Graph;
|
|
return Node;
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
// ============================================================
|
|
// Serialization
|
|
// ============================================================
|
|
|
|
TSharedRef<FJsonObject> MCPUtils::SerializeBlueprint(UBlueprint* BP)
|
|
{
|
|
TSharedRef<FJsonObject> J = MakeShared<FJsonObject>();
|
|
J->SetStringField(TEXT("name"), BP->GetName());
|
|
J->SetStringField(TEXT("path"), BP->GetPackage()->GetName());
|
|
J->SetStringField(TEXT("parentClass"), BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None"));
|
|
J->SetStringField(TEXT("blueprintType"),
|
|
StaticEnum<EBlueprintType>()->GetNameStringByValue((int64)BP->BlueprintType));
|
|
|
|
// Animation Blueprint detection
|
|
if (UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP))
|
|
{
|
|
J->SetBoolField(TEXT("isAnimBlueprint"), true);
|
|
if (AnimBP->TargetSkeleton)
|
|
{
|
|
J->SetStringField(TEXT("targetSkeleton"), AnimBP->TargetSkeleton->GetName());
|
|
J->SetStringField(TEXT("targetSkeletonPath"), AnimBP->TargetSkeleton->GetPathName());
|
|
}
|
|
}
|
|
|
|
// Variables
|
|
TArray<TSharedPtr<FJsonValue>> Vars;
|
|
for (const FBPVariableDescription& V : BP->NewVariables)
|
|
{
|
|
TSharedRef<FJsonObject> VJ = MakeShared<FJsonObject>();
|
|
VJ->SetStringField(TEXT("name"), V.VarName.ToString());
|
|
VJ->SetStringField(TEXT("type"), V.VarType.PinCategory.ToString());
|
|
if (V.VarType.PinSubCategoryObject.IsValid())
|
|
VJ->SetStringField(TEXT("subtype"), V.VarType.PinSubCategoryObject->GetName());
|
|
VJ->SetBoolField(TEXT("isArray"), V.VarType.IsArray());
|
|
VJ->SetBoolField(TEXT("isSet"), V.VarType.IsSet());
|
|
VJ->SetBoolField(TEXT("isMap"), V.VarType.IsMap());
|
|
VJ->SetStringField(TEXT("category"), V.Category.ToString());
|
|
VJ->SetStringField(TEXT("defaultValue"), V.DefaultValue);
|
|
Vars.Add(MakeShared<FJsonValueObject>(VJ));
|
|
}
|
|
J->SetArrayField(TEXT("variables"), Vars);
|
|
|
|
// Interfaces
|
|
TArray<TSharedPtr<FJsonValue>> Ifaces;
|
|
for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces)
|
|
{
|
|
if (I.Interface)
|
|
Ifaces.Add(MakeShared<FJsonValueString>(I.Interface->GetName()));
|
|
}
|
|
J->SetArrayField(TEXT("interfaces"), Ifaces);
|
|
|
|
return J;
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> MCPUtils::SerializeNode(UEdGraphNode* Node)
|
|
{
|
|
TSharedRef<FJsonObject> NJ = MakeShared<FJsonObject>();
|
|
NJ->SetStringField(TEXT("id"), Node->NodeGuid.ToString());
|
|
NJ->SetStringField(TEXT("class"), Node->GetClass()->GetName());
|
|
NJ->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
|
if (!Node->NodeComment.IsEmpty())
|
|
NJ->SetStringField(TEXT("comment"), Node->NodeComment);
|
|
NJ->SetNumberField(TEXT("posX"), Node->NodePosX);
|
|
NJ->SetNumberField(TEXT("posY"), Node->NodePosY);
|
|
|
|
// Material graph node — extract UMaterialExpression data
|
|
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("MaterialExpression"));
|
|
if (MatNode->MaterialExpression)
|
|
{
|
|
TSharedPtr<FJsonObject> ExprJson = SerializeMaterialExpression(MatNode->MaterialExpression);
|
|
if (ExprJson.IsValid())
|
|
{
|
|
NJ->SetObjectField(TEXT("expression"), ExprJson);
|
|
}
|
|
}
|
|
}
|
|
// Animation Blueprint node types
|
|
else if (auto* SMNode = Cast<UAnimGraphNode_StateMachine>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateMachine"));
|
|
if (SMNode->EditorStateMachineGraph)
|
|
{
|
|
NJ->SetStringField(TEXT("stateMachineName"), SMNode->EditorStateMachineGraph->GetName());
|
|
int32 StateCount = 0, TransitionCount = 0;
|
|
for (UEdGraphNode* SubNode : SMNode->EditorStateMachineGraph->Nodes)
|
|
{
|
|
if (Cast<UAnimStateNode>(SubNode)) StateCount++;
|
|
else if (Cast<UAnimStateTransitionNode>(SubNode)) TransitionCount++;
|
|
}
|
|
NJ->SetNumberField(TEXT("stateCount"), StateCount);
|
|
NJ->SetNumberField(TEXT("transitionCount"), TransitionCount);
|
|
}
|
|
}
|
|
else if (auto* SeqPlayer = Cast<UAnimGraphNode_SequencePlayer>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimSequencePlayer"));
|
|
if (UAnimationAsset* Asset = SeqPlayer->GetAnimationAsset())
|
|
{
|
|
NJ->SetStringField(TEXT("animationAsset"), Asset->GetName());
|
|
NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName());
|
|
}
|
|
}
|
|
else if (auto* BSPlayer = Cast<UAnimGraphNode_BlendSpacePlayer>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimBlendSpacePlayer"));
|
|
if (UAnimationAsset* Asset = BSPlayer->GetAnimationAsset())
|
|
{
|
|
NJ->SetStringField(TEXT("blendSpaceAsset"), Asset->GetName());
|
|
NJ->SetStringField(TEXT("blendSpaceAssetPath"), Asset->GetPathName());
|
|
}
|
|
}
|
|
else if (auto* AssetPlayer = Cast<UAnimGraphNode_AssetPlayerBase>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimAssetPlayer"));
|
|
if (UAnimationAsset* Asset = AssetPlayer->GetAnimationAsset())
|
|
{
|
|
NJ->SetStringField(TEXT("animationAsset"), Asset->GetName());
|
|
NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName());
|
|
}
|
|
}
|
|
else if (Cast<UAnimGraphNode_Base>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimNode"));
|
|
}
|
|
else if (auto* StateNode = Cast<UAnimStateNode>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimState"));
|
|
NJ->SetStringField(TEXT("stateName"), StateNode->GetStateName());
|
|
NJ->SetBoolField(TEXT("bAlwaysResetOnEntry"), StateNode->bAlwaysResetOnEntry);
|
|
}
|
|
else if (auto* TransNode = Cast<UAnimStateTransitionNode>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimTransition"));
|
|
if (UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState()))
|
|
{
|
|
NJ->SetStringField(TEXT("fromState"), FromState->GetStateName());
|
|
}
|
|
if (UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState()))
|
|
{
|
|
NJ->SetStringField(TEXT("toState"), ToState->GetStateName());
|
|
}
|
|
NJ->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
|
|
NJ->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode);
|
|
NJ->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
|
|
NJ->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue());
|
|
NJ->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
|
|
}
|
|
else if (Cast<UAnimStateConduitNode>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimConduit"));
|
|
}
|
|
else if (Cast<UAnimStateEntryNode>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateEntry"));
|
|
}
|
|
// K2Node specifics — check CallParentFunction before CallFunction (inheritance)
|
|
else if (auto* CPF = Cast<UK2Node_CallParentFunction>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("functionName"), CPF->FunctionReference.GetMemberName().ToString());
|
|
if (CPF->FunctionReference.GetMemberParentClass())
|
|
NJ->SetStringField(TEXT("targetClass"), CPF->FunctionReference.GetMemberParentClass()->GetName());
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("CallParentFunction"));
|
|
}
|
|
else if (auto* CF = Cast<UK2Node_CallFunction>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("functionName"), CF->FunctionReference.GetMemberName().ToString());
|
|
if (CF->FunctionReference.GetMemberParentClass())
|
|
NJ->SetStringField(TEXT("targetClass"), CF->FunctionReference.GetMemberParentClass()->GetName());
|
|
}
|
|
else if (auto* FE = Cast<UK2Node_FunctionEntry>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("FunctionEntry"));
|
|
|
|
// Serialize UserDefinedPins (parameter names and types)
|
|
TArray<TSharedPtr<FJsonValue>> ParamArr;
|
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
|
|
{
|
|
if (!PinInfo.IsValid()) continue;
|
|
TSharedRef<FJsonObject> ParamJ = MakeShared<FJsonObject>();
|
|
ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
|
|
FString ParamType = PinInfo->PinType.PinCategory.ToString();
|
|
ParamJ->SetStringField(TEXT("type"), ParamType);
|
|
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
|
ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName());
|
|
else if (ParamType == TEXT("None") || ParamType.IsEmpty())
|
|
ParamJ->SetBoolField(TEXT("typeUnknown"), true);
|
|
ParamArr.Add(MakeShared<FJsonValueObject>(ParamJ));
|
|
}
|
|
NJ->SetArrayField(TEXT("parameters"), ParamArr);
|
|
}
|
|
else if (auto* Ev = Cast<UK2Node_Event>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("eventName"), Ev->EventReference.GetMemberName().ToString());
|
|
NJ->SetStringField(TEXT("nodeType"), Ev->bOverrideFunction ? TEXT("OverrideEvent") : TEXT("Event"));
|
|
}
|
|
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("eventName"), CE->CustomFunctionName.ToString());
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("CustomEvent"));
|
|
|
|
// Serialize UserDefinedPins (parameter names and types)
|
|
TArray<TSharedPtr<FJsonValue>> ParamArr;
|
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : CE->UserDefinedPins)
|
|
{
|
|
if (!PinInfo.IsValid()) continue;
|
|
TSharedRef<FJsonObject> ParamJ = MakeShared<FJsonObject>();
|
|
ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
|
|
FString ParamType = PinInfo->PinType.PinCategory.ToString();
|
|
ParamJ->SetStringField(TEXT("type"), ParamType);
|
|
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
|
ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName());
|
|
else if (ParamType == TEXT("None") || ParamType.IsEmpty())
|
|
ParamJ->SetBoolField(TEXT("typeUnknown"), true);
|
|
ParamArr.Add(MakeShared<FJsonValueObject>(ParamJ));
|
|
}
|
|
NJ->SetArrayField(TEXT("parameters"), ParamArr);
|
|
}
|
|
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("variableName"), VG->GetVarName().ToString());
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
|
|
}
|
|
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("variableName"), VS->GetVarName().ToString());
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
|
|
}
|
|
else if (auto* MI = Cast<UK2Node_MacroInstance>(Node))
|
|
{
|
|
if (MI->GetMacroGraph())
|
|
NJ->SetStringField(TEXT("macroName"), MI->GetMacroGraph()->GetName());
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("MacroInstance"));
|
|
}
|
|
else if (auto* DC = Cast<UK2Node_DynamicCast>(Node))
|
|
{
|
|
if (DC->TargetType)
|
|
NJ->SetStringField(TEXT("castTarget"), DC->TargetType->GetName());
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("DynamicCast"));
|
|
}
|
|
else if (Cast<UK2Node_IfThenElse>(Node))
|
|
{
|
|
NJ->SetStringField(TEXT("nodeType"), TEXT("Branch"));
|
|
}
|
|
|
|
// Pins
|
|
TArray<TSharedPtr<FJsonValue>> Pins;
|
|
for (UEdGraphPin* Pin : Node->Pins)
|
|
{
|
|
if (!Pin || Pin->bHidden) continue;
|
|
TSharedPtr<FJsonObject> PJ = SerializePin(Pin);
|
|
if (PJ.IsValid())
|
|
Pins.Add(MakeShared<FJsonValueObject>(PJ.ToSharedRef()));
|
|
}
|
|
NJ->SetArrayField(TEXT("pins"), Pins);
|
|
return NJ;
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> MCPUtils::SerializePin(UEdGraphPin* Pin)
|
|
{
|
|
TSharedRef<FJsonObject> PJ = MakeShared<FJsonObject>();
|
|
PJ->SetStringField(TEXT("name"), Pin->PinName.ToString());
|
|
PJ->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
|
|
PJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
|
|
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
|
PJ->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
|
if (!Pin->DefaultValue.IsEmpty())
|
|
PJ->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
|
|
|
|
if (Pin->LinkedTo.Num() > 0)
|
|
{
|
|
TArray<TSharedPtr<FJsonValue>> Conns;
|
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
{
|
|
if (!Linked || !Linked->GetOwningNode()) continue;
|
|
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();
|
|
CJ->SetStringField(TEXT("nodeId"), Linked->GetOwningNode()->NodeGuid.ToString());
|
|
CJ->SetStringField(TEXT("pinName"), Linked->PinName.ToString());
|
|
Conns.Add(MakeShared<FJsonValueObject>(CJ));
|
|
}
|
|
PJ->SetArrayField(TEXT("connections"), Conns);
|
|
}
|
|
return PJ;
|
|
}
|
|
|
|
// ============================================================
|
|
// FindClassByName / ResolveTypeFromString
|
|
// ============================================================
|
|
|
|
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;
|
|
}
|
|
|
|
bool MCPUtils::ResolveTypeFromString(
|
|
const FString& TypeName, FEdGraphPinType& OutPinType, MCPErrorCallback Error)
|
|
{
|
|
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<FVector>::Get();
|
|
}
|
|
else if (TypeLower == TEXT("rotator") || TypeLower == TEXT("frotator"))
|
|
{
|
|
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
|
|
OutPinType.PinSubCategoryObject = TBaseStructure<FRotator>::Get();
|
|
}
|
|
else if (TypeLower == TEXT("transform") || TypeLower == TEXT("ftransform"))
|
|
{
|
|
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
|
|
OutPinType.PinSubCategoryObject = TBaseStructure<FTransform>::Get();
|
|
}
|
|
else if (TypeLower == TEXT("linearcolor") || TypeLower == TEXT("flinearcolor"))
|
|
{
|
|
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
|
|
OutPinType.PinSubCategoryObject = TBaseStructure<FLinearColor>::Get();
|
|
}
|
|
else if (TypeLower == TEXT("vector2d") || TypeLower == TEXT("fvector2d"))
|
|
{
|
|
OutPinType.PinCategory = UEdGraphSchema_K2::PC_Struct;
|
|
OutPinType.PinSubCategoryObject = TBaseStructure<FVector2D>::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)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Class '%s' not found for object reference type"), *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)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Class '%s' not found for soft object reference type"), *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)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Class '%s' not found for class reference type (TSubclassOf)"), *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)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Class '%s' not found for soft class reference type"), *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)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Class '%s' not found for interface reference type"), *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<UScriptStruct>(*InternalName);
|
|
if (!FoundStruct)
|
|
{
|
|
for (TObjectIterator<UScriptStruct> 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<UEnum>(*EnumInternalName);
|
|
if (!FoundEnum)
|
|
{
|
|
for (TObjectIterator<UEnum> 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
|
|
{
|
|
Error.SetError(FString::Printf(
|
|
TEXT("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)"),
|
|
*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<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;
|
|
}
|
|
|
|
// ============================================================
|
|
// PreEdit / PostEdit
|
|
// ============================================================
|
|
|
|
void MCPUtils::PreEdit(const TArray<UObject*>& Objects)
|
|
{
|
|
for (UObject* Obj : Objects)
|
|
Obj->PreEditChange(nullptr);
|
|
}
|
|
|
|
void MCPUtils::PostEdit(const TArray<UObject*>& Objects)
|
|
{
|
|
TSet<UMaterial*> Materials;
|
|
TSet<UBlueprint*> Blueprints;
|
|
for (int32 i = Objects.Num() - 1; i >= 0; --i)
|
|
{
|
|
UObject* Obj = Objects[i];
|
|
Obj->PostEditChange();
|
|
Obj->MarkPackageDirty();
|
|
|
|
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
|
|
Blueprints.Add(BP);
|
|
|
|
if (UMaterialInterface* MatIface = Cast<UMaterialInterface>(Obj))
|
|
if (UMaterial* BaseMat = MatIface->GetMaterial())
|
|
Materials.Add(BaseMat);
|
|
}
|
|
for (UMaterial *Material : Materials)
|
|
UMaterialEditingLibrary::RebuildMaterialInstanceEditors(Material);
|
|
for (UBlueprint *Blueprint : Blueprints)
|
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
|
|
|
|
if (GEditor)
|
|
GEditor->RedrawAllViewports();
|
|
}
|
|
|
|
|
|
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, MCPErrorCallback Error)
|
|
{
|
|
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
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Invalid ParameterAssociation '%s' (expected 'Global', 'Layer', or 'Blend')"), *Str));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void MCPUtils::FormatMaterialParameter(FStringBuilderBase& Result, 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:
|
|
Result.Appendf(TEXT(" %sScalar \"%s\" = %g\n"), *Prefix, *Info.Name.ToString(), Meta.Value.AsScalar());
|
|
break;
|
|
case EMaterialParameterType::Vector:
|
|
{
|
|
FLinearColor C = Meta.Value.AsLinearColor();
|
|
Result.Appendf(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();
|
|
Result.Appendf(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());
|
|
Result.Appendf(TEXT(" %sTexture \"%s\" = %s\n"),
|
|
*Prefix, *Info.Name.ToString(), Tex ? *MCPUtils::FormatName(Tex) : TEXT("None"));
|
|
break;
|
|
}
|
|
case EMaterialParameterType::StaticSwitch:
|
|
Result.Appendf(TEXT(" %sStaticSwitch \"%s\" = %s\n"),
|
|
*Prefix, *Info.Name.ToString(), Meta.Value.AsStaticSwitch() ? TEXT("true") : TEXT("false"));
|
|
break;
|
|
default:
|
|
Result.Appendf(TEXT(" %sType%d \"%s\"\n"), *Prefix, (int)Meta.Value.Type, *Info.Name.ToString());
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool MCPUtils::SaveMaterialPackage(UMaterial* Material)
|
|
{
|
|
if (!Material) return false;
|
|
return SaveGenericPackage(Material);
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
TSharedPtr<FJsonObject> MCPUtils::SerializeMaterialExpression(UMaterialExpression* Expression)
|
|
{
|
|
if (!Expression) return nullptr;
|
|
|
|
TSharedRef<FJsonObject> EJ = MakeShared<FJsonObject>();
|
|
EJ->SetStringField(TEXT("class"), Expression->GetClass()->GetName());
|
|
EJ->SetStringField(TEXT("name"), Expression->GetName());
|
|
EJ->SetStringField(TEXT("description"), Expression->GetDescription());
|
|
EJ->SetNumberField(TEXT("posX"), Expression->MaterialExpressionEditorX);
|
|
EJ->SetNumberField(TEXT("posY"), Expression->MaterialExpressionEditorY);
|
|
|
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("ScalarParameter"));
|
|
EJ->SetStringField(TEXT("parameterName"), SP->ParameterName.ToString());
|
|
EJ->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
|
EJ->SetStringField(TEXT("group"), SP->Group.ToString());
|
|
}
|
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("VectorParameter"));
|
|
EJ->SetStringField(TEXT("parameterName"), VP->ParameterName.ToString());
|
|
TSharedRef<FJsonObject> DefVal = MakeShared<FJsonObject>();
|
|
DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
|
DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
|
DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
|
DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
|
EJ->SetObjectField(TEXT("defaultValue"), DefVal);
|
|
EJ->SetStringField(TEXT("group"), VP->Group.ToString());
|
|
}
|
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSampleParameter2D"));
|
|
EJ->SetStringField(TEXT("parameterName"), TP->ParameterName.ToString());
|
|
if (TP->Texture)
|
|
EJ->SetStringField(TEXT("texture"), TP->Texture->GetPathName());
|
|
EJ->SetStringField(TEXT("group"), TP->Group.ToString());
|
|
}
|
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("StaticSwitchParameter"));
|
|
EJ->SetStringField(TEXT("parameterName"), SSP->ParameterName.ToString());
|
|
EJ->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
|
EJ->SetStringField(TEXT("group"), SSP->Group.ToString());
|
|
}
|
|
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("Constant"));
|
|
EJ->SetNumberField(TEXT("value"), SC->R);
|
|
}
|
|
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("Constant3Vector"));
|
|
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
|
|
Val->SetNumberField(TEXT("r"), C3->Constant.R);
|
|
Val->SetNumberField(TEXT("g"), C3->Constant.G);
|
|
Val->SetNumberField(TEXT("b"), C3->Constant.B);
|
|
EJ->SetObjectField(TEXT("value"), Val);
|
|
}
|
|
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("Constant4Vector"));
|
|
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
|
|
Val->SetNumberField(TEXT("r"), C4->Constant.R);
|
|
Val->SetNumberField(TEXT("g"), C4->Constant.G);
|
|
Val->SetNumberField(TEXT("b"), C4->Constant.B);
|
|
Val->SetNumberField(TEXT("a"), C4->Constant.A);
|
|
EJ->SetObjectField(TEXT("value"), Val);
|
|
}
|
|
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSample"));
|
|
if (TS->Texture)
|
|
EJ->SetStringField(TEXT("texture"), TS->Texture->GetPathName());
|
|
}
|
|
else if (auto* TC = Cast<UMaterialExpressionTextureCoordinate>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("TextureCoordinate"));
|
|
EJ->SetNumberField(TEXT("coordinateIndex"), TC->CoordinateIndex);
|
|
EJ->SetNumberField(TEXT("uTiling"), TC->UTiling);
|
|
EJ->SetNumberField(TEXT("vTiling"), TC->VTiling);
|
|
}
|
|
else if (auto* CM = Cast<UMaterialExpressionComponentMask>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("ComponentMask"));
|
|
EJ->SetBoolField(TEXT("r"), CM->R != 0);
|
|
EJ->SetBoolField(TEXT("g"), CM->G != 0);
|
|
EJ->SetBoolField(TEXT("b"), CM->B != 0);
|
|
EJ->SetBoolField(TEXT("a"), CM->A != 0);
|
|
}
|
|
else if (auto* Custom = Cast<UMaterialExpressionCustom>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("Custom"));
|
|
EJ->SetStringField(TEXT("code"), Custom->Code);
|
|
EJ->SetStringField(TEXT("outputType"), StaticEnum<ECustomMaterialOutputType>()->GetNameStringByValue((int64)Custom->OutputType));
|
|
}
|
|
else if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionInput"));
|
|
EJ->SetStringField(TEXT("inputName"), FI->InputName.ToString());
|
|
}
|
|
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionOutput"));
|
|
EJ->SetStringField(TEXT("outputName"), FO->OutputName.ToString());
|
|
}
|
|
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expression))
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), TEXT("MaterialFunctionCall"));
|
|
if (MFC->MaterialFunction)
|
|
EJ->SetStringField(TEXT("functionName"), MFC->MaterialFunction->GetName());
|
|
}
|
|
else
|
|
{
|
|
EJ->SetStringField(TEXT("expressionType"), Expression->GetClass()->GetName());
|
|
}
|
|
|
|
return EJ;
|
|
}
|
|
|
|
// ============================================================
|
|
// 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, MCPErrorCallback Error)
|
|
{
|
|
for (UEdGraphNode* Node : SMGraph->Nodes)
|
|
{
|
|
if (UAnimStateNode* StateNode = Cast<UAnimStateNode>(Node))
|
|
{
|
|
if (StateNode->GetStateName() == StateName)
|
|
{
|
|
return StateNode;
|
|
}
|
|
}
|
|
}
|
|
Error.SetError(FString::Printf(TEXT("State '%s' not found in graph '%s'"), *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,
|
|
MCPErrorCallback Error)
|
|
{
|
|
if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
|
|
{
|
|
Error.SetError(TEXT("Expected a JSON object"));
|
|
return false;
|
|
}
|
|
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get(), Error);
|
|
}
|
|
|
|
bool MCPUtils::PopulateFromJson(
|
|
UStruct* StructType,
|
|
void* Container,
|
|
const FJsonObject* Json,
|
|
MCPErrorCallback Error)
|
|
{
|
|
// 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))
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Unknown parameter '%s'"), *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)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Missing required parameter '%s'"), *JsonKey));
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
FString PropError = SetPropertyFromJson(Container, Prop, JsonKey, Json);
|
|
if (!PropError.IsEmpty())
|
|
{
|
|
Error.SetError(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 GetToolName(&A) < GetToolName(&B); });
|
|
return Result;
|
|
}
|
|
|
|
// ============================================================
|
|
// GetToolName — derive tool name from handler class name
|
|
// ============================================================
|
|
|
|
FString MCPUtils::GetToolName(UClass* HandlerClass)
|
|
{
|
|
FString Name = HandlerClass->GetName();
|
|
// Strip "MCP_" prefix
|
|
if (Name.StartsWith(TEXT("MCP_")))
|
|
Name = Name.Mid(4);
|
|
// Strip the remaining underscore between group and action (e.g. "Blueprint_Create" -> "BlueprintCreate")
|
|
int32 UnderscoreIdx;
|
|
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
|
|
Name = Name.Left(UnderscoreIdx) + Name.Mid(UnderscoreIdx + 1);
|
|
return Name;
|
|
}
|
|
|
|
// ============================================================
|
|
// GetToolGroup — derive group name from handler class name
|
|
// ============================================================
|
|
|
|
FString MCPUtils::GetToolGroup(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<FStrProperty>(Prop)) return TEXT("string");
|
|
if (CastField<FIntProperty>(Prop)) return TEXT("integer");
|
|
if (CastField<FFloatProperty>(Prop) || CastField<FDoubleProperty>(Prop)) return TEXT("number");
|
|
if (CastField<FBoolProperty>(Prop)) return TEXT("boolean");
|
|
if (FStructProperty* SP = CastField<FStructProperty>(Prop))
|
|
{
|
|
FString StructName = SP->Struct->GetName();
|
|
StructName.ReplaceInline(TEXT("MCP"), TEXT(""));
|
|
return StructName;
|
|
}
|
|
return TEXT("string");
|
|
}
|
|
|
|
// ============================================================
|
|
// GetTemplate
|
|
// ============================================================
|
|
|
|
UObject* MCPUtils::GetTemplate(UObject* Obj, MCPErrorCallback Error)
|
|
{
|
|
if (!Obj)
|
|
{
|
|
Error.SetError(TEXT("Object is null"));
|
|
return nullptr;
|
|
}
|
|
|
|
// Blueprint → operate on the Class Default Object.
|
|
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
|
|
{
|
|
if (!BP->GeneratedClass)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Blueprint '%s' has no GeneratedClass"), *Obj->GetName()));
|
|
return nullptr;
|
|
}
|
|
return BP->GeneratedClass->GetDefaultObject();
|
|
}
|
|
|
|
return Obj;
|
|
}
|
|
|
|
// ============================================================
|
|
// FindPropertyByName
|
|
// ============================================================
|
|
|
|
FProperty* MCPUtils::FindPropertyByName(UObject* Obj, const FString& Name, MCPErrorCallback Error)
|
|
{
|
|
if (!Obj)
|
|
{
|
|
Error.SetError(TEXT("Object is null"));
|
|
return nullptr;
|
|
}
|
|
|
|
FProperty* Found = nullptr;
|
|
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
|
|
{
|
|
if (!Identifies(Name, *PropIt)) continue;
|
|
if (Found)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Ambiguous property '%s' on %s"), *Name, *FormatName(Obj->GetClass())));
|
|
return nullptr;
|
|
}
|
|
Found = *PropIt;
|
|
}
|
|
|
|
if (!Found)
|
|
Error.SetError(FString::Printf(TEXT("Property '%s' not found on %s"), *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, MCPErrorCallback Error)
|
|
{
|
|
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
|
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Container, PPF_None);
|
|
if (!ImportResult)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Failed to parse '%s' for property '%s' (type: %s)"),
|
|
*Value, *FormatName(Prop), *Prop->GetCPPType()));
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool MCPUtils::SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner, MCPErrorCallback Error)
|
|
{
|
|
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
|
|
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Owner, PPF_None);
|
|
if (!ImportResult)
|
|
{
|
|
Error.SetError(FString::Printf(TEXT("Failed to parse '%s' for property '%s' (type: %s)"),
|
|
*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, FStringBuilderBase& Result)
|
|
{
|
|
const IMCPHandler* Handler = Cast<IMCPHandler>(HandlerClass->GetDefaultObject());
|
|
if (!Handler) return;
|
|
|
|
FString ToolName = GetToolName(HandlerClass);
|
|
|
|
// Command signature line
|
|
Result.Append(ToolName);
|
|
Result.Append(TEXT("("));
|
|
bool bFirst = true;
|
|
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
|
|
{
|
|
if (!bFirst) Result.Append(TEXT(","));
|
|
bFirst = false;
|
|
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
|
|
Result.Append(PropertyNameToJsonKey(PropIt->GetName()));
|
|
}
|
|
Result.Append(TEXT(")\n"));
|
|
|
|
// Description and parameter details
|
|
Result.Appendf(TEXT(" %s\n"), *Handler->GetDescription());
|
|
for (TFieldIterator<FProperty> 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"));
|
|
|
|
Result.Appendf(TEXT(" %s %s%s"),
|
|
*Type, *Name, bOptional ? TEXT(" (optional)") : TEXT(""));
|
|
if (!Desc.IsEmpty())
|
|
Result.Appendf(TEXT(" — %s"), *Desc);
|
|
Result.Append(TEXT("\n"));
|
|
}
|
|
}
|