Split MCP handlers
This commit is contained in:
@@ -1,466 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "Kismet2/KismetEditorUtilities.h"
|
|
||||||
#include "Dom/JsonValue.h"
|
|
||||||
#include "Animation/AnimBlueprint.h"
|
|
||||||
#include "Animation/AnimBlueprintGeneratedClass.h"
|
|
||||||
#include "Animation/Skeleton.h"
|
|
||||||
#include "AnimGraphNode_Base.h"
|
|
||||||
#include "Animation/AnimSequence.h"
|
|
||||||
#include "Animation/BlendSpace.h"
|
|
||||||
#include "MCPHandlers_AnimMutation.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateAnimBlueprintAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Name for the new Animation Blueprint asset"))
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
|
||||||
FString PackagePath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
|
|
||||||
FString Skeleton;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Parent class name (default: AnimInstance)"))
|
|
||||||
FString ParentClass;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new Animation Blueprint asset with a specified skeleton.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if asset already exists
|
|
||||||
FString FullAssetPath = PackagePath / Name;
|
|
||||||
MCPAssets<UBlueprint> ExistCheck;
|
|
||||||
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
|
||||||
|
|
||||||
// Resolve skeleton
|
|
||||||
MCPAssets<USkeleton> SkeletonAssets;
|
|
||||||
if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
USkeleton* SkeletonObj = SkeletonAssets.Object();
|
|
||||||
|
|
||||||
// Resolve parent class (default: UAnimInstance)
|
|
||||||
UClass* ParentClassObj = UAnimInstance::StaticClass();
|
|
||||||
if (!ParentClass.IsEmpty() && ParentClass != TEXT("AnimInstance"))
|
|
||||||
{
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (It->GetName() == ParentClass && It->IsChildOf(UAnimInstance::StaticClass()))
|
|
||||||
{
|
|
||||||
ParentClassObj = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating AnimBlueprint '%s' in '%s' with skeleton '%s'"),
|
|
||||||
*Name, *PackagePath, *SkeletonObj->GetName());
|
|
||||||
|
|
||||||
// Create the package
|
|
||||||
FString FullPackagePath = PackagePath / Name;
|
|
||||||
UPackage* Package = CreatePackage(*FullPackagePath);
|
|
||||||
if (!Package)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Animation Blueprint
|
|
||||||
UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>(
|
|
||||||
FKismetEditorUtilities::CreateBlueprint(
|
|
||||||
ParentClassObj,
|
|
||||||
Package,
|
|
||||||
FName(*Name),
|
|
||||||
BPTYPE_Normal,
|
|
||||||
UAnimBlueprint::StaticClass(),
|
|
||||||
UAnimBlueprintGeneratedClass::StaticClass()
|
|
||||||
));
|
|
||||||
|
|
||||||
if (!NewAnimBP)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set target skeleton
|
|
||||||
NewAnimBP->TargetSkeleton = SkeletonObj;
|
|
||||||
|
|
||||||
// Compile
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
|
|
||||||
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> GraphNames = MCPUtils::AllGraphNamesJson(NewAnimBP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"),
|
|
||||||
*Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
|
||||||
Result->SetStringField(TEXT("targetSkeleton"), SkeletonObj->GetName());
|
|
||||||
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListAnimSlotNames : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all animation slot names used in an Animation Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
if (Blueprint.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
// Walk all anim nodes to collect slot names
|
|
||||||
TSet<FString> SlotNames;
|
|
||||||
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
|
|
||||||
{
|
|
||||||
// Check for SlotName property via reflection
|
|
||||||
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
|
|
||||||
{
|
|
||||||
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot")))
|
|
||||||
{
|
|
||||||
FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
|
|
||||||
if (!SlotValue.IsNone())
|
|
||||||
{
|
|
||||||
SlotNames.Add(SlotValue.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> SlotsArr;
|
|
||||||
for (const FString& Slot : SlotNames)
|
|
||||||
{
|
|
||||||
SlotsArr.Add(MakeShared<FJsonValueString>(Slot));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetArrayField(TEXT("slots"), SlotsArr);
|
|
||||||
Result->SetNumberField(TEXT("count"), SlotsArr.Num());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListAnimSyncGroups : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all sync group names used in an Animation Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
if (Blueprint.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
// Walk all anim nodes to collect sync group names
|
|
||||||
TSet<FString> SyncGroupNames;
|
|
||||||
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
|
|
||||||
{
|
|
||||||
// Check for SyncGroup/GroupName property via reflection
|
|
||||||
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
|
|
||||||
{
|
|
||||||
if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName")))
|
|
||||||
{
|
|
||||||
FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode);
|
|
||||||
if (!GroupValue.IsNone())
|
|
||||||
{
|
|
||||||
SyncGroupNames.Add(GroupValue.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> GroupsArr;
|
|
||||||
for (const FString& Group : SyncGroupNames)
|
|
||||||
{
|
|
||||||
GroupsArr.Add(MakeShared<FJsonValueString>(Group));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetArrayField(TEXT("syncGroups"), GroupsArr);
|
|
||||||
Result->SetNumberField(TEXT("count"), GroupsArr.Num());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateBlendSpaceAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Name for the new Blend Space asset"))
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
|
||||||
FString PackagePath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
|
|
||||||
FString Skeleton;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new 2D Blend Space asset with a specified skeleton.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if asset already exists
|
|
||||||
FString FullAssetPath = PackagePath / Name;
|
|
||||||
MCPAssets<UBlendSpace> ExistCheck;
|
|
||||||
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
|
||||||
|
|
||||||
// Resolve skeleton
|
|
||||||
MCPAssets<USkeleton> SkeletonAssets;
|
|
||||||
if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
USkeleton* SkeletonObj = SkeletonAssets.Object();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"),
|
|
||||||
*Name, *PackagePath, *SkeletonObj->GetName());
|
|
||||||
|
|
||||||
// Create the package
|
|
||||||
FString FullPackagePath = PackagePath / Name;
|
|
||||||
UPackage* Package = CreatePackage(*FullPackagePath);
|
|
||||||
if (!Package)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Blend Space
|
|
||||||
UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone);
|
|
||||||
if (!NewBS)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create Blend Space object"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set skeleton
|
|
||||||
NewBS->SetSkeleton(SkeletonObj);
|
|
||||||
|
|
||||||
// Mark dirty and save
|
|
||||||
NewBS->MarkPackageDirty();
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(NewBS);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"),
|
|
||||||
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
|
||||||
Result->SetStringField(TEXT("skeleton"), SkeletonObj->GetName());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FBlendSpaceSampleEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString AnimationAsset;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
float X = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
float Y = 0.0f;
|
|
||||||
};
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetBlendSpaceSamplePoints : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blend Space asset name or package path"))
|
|
||||||
FString BlendSpace;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Display name for the X axis"))
|
|
||||||
FString AxisXName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Display name for the Y axis"))
|
|
||||||
FString AxisYName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Minimum value for X axis"))
|
|
||||||
float AxisXMin = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Maximum value for X axis"))
|
|
||||||
float AxisXMax = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Minimum value for Y axis"))
|
|
||||||
float AxisYMin = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Maximum value for Y axis"))
|
|
||||||
float AxisYMax = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Array of sample points, each with animationAsset, x, y"))
|
|
||||||
FMCPJsonArray Samples;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Set axis parameters and animation sample points on a Blend Space. "
|
|
||||||
"Replaces all existing samples.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
if (BlendSpace.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the blend space
|
|
||||||
MCPAssets<UBlendSpace> Assets;
|
|
||||||
if (!Assets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlendSpace* BS = Assets.Object();
|
|
||||||
|
|
||||||
// Set axis parameters
|
|
||||||
BS->PreEditChange(nullptr);
|
|
||||||
|
|
||||||
const FBlendParameter& ParamX = BS->GetBlendParameter(0);
|
|
||||||
const FBlendParameter& ParamY = BS->GetBlendParameter(1);
|
|
||||||
|
|
||||||
// We need to modify BlendParameters directly — use const_cast since there's no setter API
|
|
||||||
FBlendParameter& MutableParamX = const_cast<FBlendParameter&>(ParamX);
|
|
||||||
FBlendParameter& MutableParamY = const_cast<FBlendParameter&>(ParamY);
|
|
||||||
|
|
||||||
if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName;
|
|
||||||
if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = AxisXMin;
|
|
||||||
if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = AxisXMax;
|
|
||||||
|
|
||||||
if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName;
|
|
||||||
if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = AxisYMin;
|
|
||||||
if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = AxisYMax;
|
|
||||||
|
|
||||||
// Clear existing samples (delete from end to start)
|
|
||||||
int32 NumExisting = BS->GetNumberOfBlendSamples();
|
|
||||||
for (int32 i = NumExisting - 1; i >= 0; --i)
|
|
||||||
{
|
|
||||||
BS->DeleteSample(i);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add new samples
|
|
||||||
int32 SamplesSet = 0;
|
|
||||||
|
|
||||||
for (const TSharedPtr<FJsonValue>& SampleVal : Samples.Array)
|
|
||||||
{
|
|
||||||
FBlendSpaceSampleEntry Entry;
|
|
||||||
if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return;
|
|
||||||
|
|
||||||
UAnimSequence* AnimSeq = nullptr;
|
|
||||||
if (!Entry.AnimationAsset.IsEmpty())
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimSequence> AnimAssets;
|
|
||||||
if (AnimAssets.Exact(Entry.AnimationAsset).Load())
|
|
||||||
AnimSeq = AnimAssets.Object();
|
|
||||||
}
|
|
||||||
|
|
||||||
FVector SampleValue(Entry.X, Entry.Y, 0.0f);
|
|
||||||
if (AnimSeq)
|
|
||||||
{
|
|
||||||
BS->AddSample(AnimSeq, SampleValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
BS->AddSample(SampleValue);
|
|
||||||
}
|
|
||||||
SamplesSet++;
|
|
||||||
}
|
|
||||||
|
|
||||||
BS->ValidateSampleData();
|
|
||||||
BS->PostEditChange();
|
|
||||||
|
|
||||||
// Save
|
|
||||||
BS->MarkPackageDirty();
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(BS);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set %d samples on Blend Space '%s' (saved: %s)"),
|
|
||||||
SamplesSet, *BS->GetName(), bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("blendSpace"), BS->GetPathName());
|
|
||||||
Result->SetNumberField(TEXT("samplesSet"), SamplesSet);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
@@ -1,338 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "UObject/SavePackage.h"
|
|
||||||
#include "Misc/PackageName.h"
|
|
||||||
#include "AssetRegistry/AssetRegistryModule.h"
|
|
||||||
#include "AssetRegistry/IAssetRegistry.h"
|
|
||||||
#include "AssetToolsModule.h"
|
|
||||||
#include "IAssetTools.h"
|
|
||||||
#include "FileHelpers.h"
|
|
||||||
#include "MCPHandlers_AssetMutation.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DeleteAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Package path of the asset to delete"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete"))
|
|
||||||
bool Force = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Delete a .uasset after verifying no references. "
|
|
||||||
"Use force=true to skip the reference check.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
// Check if asset file exists on disk
|
|
||||||
FString PackageFilename = FPackageName::LongPackageNameToFilename(
|
|
||||||
AssetPath, FPackageName::GetAssetPackageExtension());
|
|
||||||
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
|
|
||||||
|
|
||||||
if (!IFileManager::Get().FileExists(*PackageFilename))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check references
|
|
||||||
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
|
||||||
TArray<FName> Referencers;
|
|
||||||
Registry.GetReferencers(FName(*AssetPath), Referencers);
|
|
||||||
|
|
||||||
// Filter out self-references
|
|
||||||
Referencers.RemoveAll([this](const FName& Ref) {
|
|
||||||
return Ref.ToString() == AssetPath;
|
|
||||||
});
|
|
||||||
|
|
||||||
if ((Referencers.Num() > 0) && !Force)
|
|
||||||
{
|
|
||||||
// Classify references as "live" (loaded in memory) vs "stale" (only on disk)
|
|
||||||
TArray<TSharedPtr<FJsonValue>> LiveRefs;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> StaleRefs;
|
|
||||||
for (const FName& Ref : Referencers)
|
|
||||||
{
|
|
||||||
FString RefStr = Ref.ToString();
|
|
||||||
UPackage* RefPackage = FindPackage(nullptr, *RefStr);
|
|
||||||
if (RefPackage)
|
|
||||||
{
|
|
||||||
LiveRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
StaleRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first."));
|
|
||||||
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
|
||||||
Result->SetNumberField(TEXT("referencerCount"), Referencers.Num());
|
|
||||||
Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num());
|
|
||||||
Result->SetArrayField(TEXT("liveReferencers"), LiveRefs);
|
|
||||||
Result->SetNumberField(TEXT("staleReferencerCount"), StaleRefs.Num());
|
|
||||||
Result->SetArrayField(TEXT("staleReferencers"), StaleRefs);
|
|
||||||
Result->SetStringField(TEXT("suggestion"),
|
|
||||||
StaleRefs.Num() > 0
|
|
||||||
? TEXT("Some references may be stale. Consider force=true to skip the reference check, or use change_variable_type to migrate references first.")
|
|
||||||
: TEXT("All references are live. Migrate with change_variable_type or replace_function_calls before deleting."));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Force delete: unload the package from memory first
|
|
||||||
TArray<TSharedPtr<FJsonValue>> RefWarnings;
|
|
||||||
if (Force)
|
|
||||||
{
|
|
||||||
// Collect reference warnings when force-deleting with existing references
|
|
||||||
for (const FName& Ref : Referencers)
|
|
||||||
{
|
|
||||||
RefWarnings.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("Warning: '%s' still references this asset"), *Ref.ToString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
UPackage* Package = FindPackage(nullptr, *AssetPath);
|
|
||||||
if (Package)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Force-unloading package '%s' from memory"), *AssetPath);
|
|
||||||
|
|
||||||
// Collect all objects in this package
|
|
||||||
TArray<UObject*> ObjectsInPackage;
|
|
||||||
GetObjectsWithPackage(Package, ObjectsInPackage);
|
|
||||||
|
|
||||||
// Clear flags and remove from root to allow GC
|
|
||||||
for (UObject* Obj : ObjectsInPackage)
|
|
||||||
{
|
|
||||||
if (Obj)
|
|
||||||
{
|
|
||||||
Obj->ClearFlags(RF_Standalone | RF_Public);
|
|
||||||
Obj->RemoveFromRoot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Package->ClearFlags(RF_Standalone | RF_Public);
|
|
||||||
Package->RemoveFromRoot();
|
|
||||||
|
|
||||||
// Reset loaders to release file handles
|
|
||||||
ResetLoaders(Package);
|
|
||||||
// Force garbage collection to free the objects
|
|
||||||
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting asset '%s' (%s)%s"),
|
|
||||||
*AssetPath, *PackageFilename, Force ? TEXT(" [FORCE]") : TEXT(""));
|
|
||||||
|
|
||||||
// Delete the file on disk
|
|
||||||
bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true);
|
|
||||||
|
|
||||||
if (bDeleted)
|
|
||||||
{
|
|
||||||
// Trigger an asset registry rescan so it notices the deletion
|
|
||||||
TArray<FString> PathsToScan;
|
|
||||||
int32 LastSlash;
|
|
||||||
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
|
|
||||||
{
|
|
||||||
PathsToScan.Add(AssetPath.Left(LastSlash));
|
|
||||||
}
|
|
||||||
if (PathsToScan.Num() > 0)
|
|
||||||
{
|
|
||||||
Registry.ScanPathsSynchronous(PathsToScan, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("filename"), PackageFilename);
|
|
||||||
if (!bDeleted)
|
|
||||||
{
|
|
||||||
MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk"));
|
|
||||||
}
|
|
||||||
if (RefWarnings.Num() > 0)
|
|
||||||
{
|
|
||||||
Result->SetArrayField(TEXT("warnings"), RefWarnings);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RenameAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Current package path of the asset"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="New package path or new asset name"))
|
|
||||||
FString NewPath;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Rename or move an asset with reference fixup.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming asset '%s' -> '%s'"), *AssetPath, *NewPath);
|
|
||||||
|
|
||||||
// Use FAssetToolsModule to perform the rename with reference fixup
|
|
||||||
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
||||||
IAssetTools& AssetTools = AssetToolsModule.Get();
|
|
||||||
|
|
||||||
// Build the source/dest arrays
|
|
||||||
TArray<FAssetRenameData> RenameData;
|
|
||||||
|
|
||||||
// We need to load the asset to get the object
|
|
||||||
MCPAssets<UObject> Assets;
|
|
||||||
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UObject* AssetObj = Assets.Object();
|
|
||||||
|
|
||||||
// Parse new path into package path and asset name
|
|
||||||
FString NewPackagePath, NewAssetName;
|
|
||||||
int32 LastSlash;
|
|
||||||
if (NewPath.FindLastChar(TEXT('/'), LastSlash))
|
|
||||||
{
|
|
||||||
NewPackagePath = NewPath.Left(LastSlash);
|
|
||||||
NewAssetName = NewPath.Mid(LastSlash + 1);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// If no slash, assume same directory with new name
|
|
||||||
FString OldPackagePath;
|
|
||||||
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
|
|
||||||
{
|
|
||||||
OldPackagePath = AssetPath.Left(LastSlash);
|
|
||||||
}
|
|
||||||
NewPackagePath = OldPackagePath;
|
|
||||||
NewAssetName = NewPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
FAssetRenameData RenameEntry(AssetObj, NewPackagePath, NewAssetName);
|
|
||||||
RenameData.Add(RenameEntry);
|
|
||||||
|
|
||||||
bool bSuccess = AssetTools.RenameAssets(RenameData);
|
|
||||||
|
|
||||||
if (bSuccess)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rename %s"), bSuccess ? TEXT("succeeded") : TEXT("failed"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("newPackagePath"), NewPackagePath);
|
|
||||||
Result->SetStringField(TEXT("newAssetName"), NewAssetName);
|
|
||||||
if (!bSuccess)
|
|
||||||
{
|
|
||||||
MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist."));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_BackupAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Copy an asset's .uasset file to a .uasset.bak backup.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString Filename = FPaths::ConvertRelativePathToFull(
|
|
||||||
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
|
|
||||||
|
|
||||||
if (!IFileManager::Get().FileExists(*Filename))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found: %s"), *Filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
FString BackupFilename = Filename + TEXT(".bak");
|
|
||||||
uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true);
|
|
||||||
if (CopyResult != COPY_OK)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to back up %s"), *Filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("backupFile"), BackupFilename);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RestoreAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString Filename = FPaths::ConvertRelativePathToFull(
|
|
||||||
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
|
|
||||||
FString BackupFilename = Filename + TEXT(".bak");
|
|
||||||
|
|
||||||
if (!IFileManager::Get().FileExists(*BackupFilename))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Backup file not found: %s"), *BackupFilename));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Release file handles if the package is loaded
|
|
||||||
UPackage* Package = FindPackage(nullptr, *AssetPath);
|
|
||||||
if (Package)
|
|
||||||
{
|
|
||||||
ResetLoaders(Package);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Copy backup over the original
|
|
||||||
uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true);
|
|
||||||
if (CopyResult != COPY_OK)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to restore %s"), *Filename));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload the package if it was loaded
|
|
||||||
if (Package)
|
|
||||||
{
|
|
||||||
bool bReloaded = false;
|
|
||||||
FText ErrorMessage;
|
|
||||||
UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("restoredFrom"), BackupFilename);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,361 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "Engine/SimpleConstructionScript.h"
|
|
||||||
#include "Engine/SCS_Node.h"
|
|
||||||
#include "Components/ActorComponent.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "UObject/UObjectIterator.h"
|
|
||||||
#include "MCPHandlers_Components.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListBlueprintComponents : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all components in a Blueprint's SimpleConstructionScript, "
|
|
||||||
"including hierarchy and scene root information.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
|
||||||
if (!SCS)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
|
||||||
*Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ComponentsArr;
|
|
||||||
for (USCS_Node* Node : AllNodes)
|
|
||||||
{
|
|
||||||
if (!Node)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> CompObj = MakeShared<FJsonObject>();
|
|
||||||
CompObj->SetStringField(TEXT("name"), Node->GetVariableName().ToString());
|
|
||||||
|
|
||||||
if (Node->ComponentClass)
|
|
||||||
{
|
|
||||||
CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
CompObj->SetStringField(TEXT("componentClass"), TEXT("None"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parent component info
|
|
||||||
USCS_Node* ParentNode = nullptr;
|
|
||||||
for (USCS_Node* Candidate : AllNodes)
|
|
||||||
{
|
|
||||||
if (Candidate && Candidate->GetChildNodes().Contains(Node))
|
|
||||||
{
|
|
||||||
ParentNode = Candidate;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ParentNode)
|
|
||||||
{
|
|
||||||
CompObj->SetStringField(TEXT("parentComponent"), ParentNode->GetVariableName().ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if this is a default scene root (first root node with SceneComponent class)
|
|
||||||
bool bIsSceneRoot = false;
|
|
||||||
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
|
||||||
if (RootNodes.Num() > 0 && RootNodes[0] == Node)
|
|
||||||
{
|
|
||||||
bIsSceneRoot = true;
|
|
||||||
}
|
|
||||||
CompObj->SetBoolField(TEXT("isSceneRoot"), bIsSceneRoot);
|
|
||||||
|
|
||||||
// List child count for informational purposes
|
|
||||||
CompObj->SetNumberField(TEXT("childCount"), Node->GetChildNodes().Num());
|
|
||||||
|
|
||||||
ComponentsArr.Add(MakeShared<FJsonValueObject>(CompObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("count"), ComponentsArr.Num());
|
|
||||||
Result->SetArrayField(TEXT("components"), ComponentsArr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddBlueprintComponent : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Component class name (e.g. StaticMeshComponent, SceneComponent)"))
|
|
||||||
FString ComponentClass;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Component name for the new component"))
|
|
||||||
FString Component;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Name of the parent component to attach to"))
|
|
||||||
FString ParentComponent;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a component to a Blueprint's SimpleConstructionScript. "
|
|
||||||
"Optionally attach it to an existing parent component.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
|
||||||
if (!SCS)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
|
||||||
*Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate component names
|
|
||||||
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
|
|
||||||
for (USCS_Node* Existing : ExistingNodes)
|
|
||||||
{
|
|
||||||
if (Existing && Existing->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("A component named '%s' already exists in Blueprint '%s'"),
|
|
||||||
*Component, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the component class by name
|
|
||||||
// Try multiple name variants: exact name, with U prefix, without U prefix
|
|
||||||
UClass* ComponentClassObj = nullptr;
|
|
||||||
|
|
||||||
TArray<FString> NamesToTry;
|
|
||||||
NamesToTry.Add(ComponentClass);
|
|
||||||
if (!ComponentClass.StartsWith(TEXT("U")))
|
|
||||||
{
|
|
||||||
NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClass));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Also try without U prefix
|
|
||||||
NamesToTry.Add(ComponentClass.Mid(1));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (!It->IsChildOf(UActorComponent::StaticClass()))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString ClassName = It->GetName();
|
|
||||||
for (const FString& NameToTry : NamesToTry)
|
|
||||||
{
|
|
||||||
if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
ComponentClassObj = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ComponentClassObj)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ComponentClassObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Component class '%s' not found or is not a subclass of UActorComponent. "
|
|
||||||
"Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, "
|
|
||||||
"SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, "
|
|
||||||
"ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, "
|
|
||||||
"WidgetComponent, BillboardComponent"),
|
|
||||||
*ComponentClass));
|
|
||||||
}
|
|
||||||
|
|
||||||
// If parent component specified, find its SCS node
|
|
||||||
USCS_Node* ParentSCSNode = nullptr;
|
|
||||||
if (!ParentComponent.IsEmpty())
|
|
||||||
{
|
|
||||||
for (USCS_Node* Node : ExistingNodes)
|
|
||||||
{
|
|
||||||
if (Node && Node->GetVariableName().ToString().Equals(ParentComponent, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
ParentSCSNode = Node;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParentSCSNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Parent component '%s' not found in Blueprint '%s'"),
|
|
||||||
*ParentComponent, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"),
|
|
||||||
*Component, *ComponentClassObj->GetName(), *Blueprint);
|
|
||||||
|
|
||||||
// Create the SCS node
|
|
||||||
USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component));
|
|
||||||
if (!NewNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Failed to create SCS node for component '%s' with class '%s'"),
|
|
||||||
*Component, *ComponentClassObj->GetName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to the hierarchy
|
|
||||||
if (ParentSCSNode)
|
|
||||||
{
|
|
||||||
ParentSCSNode->AddChildNode(NewNode);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SCS->AddNode(NewNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"),
|
|
||||||
*Component, *ComponentClassObj->GetName(), *Blueprint,
|
|
||||||
ParentSCSNode ? *ParentComponent : TEXT("(root)"),
|
|
||||||
bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("component"), NewNode->GetVariableName().ToString());
|
|
||||||
Result->SetStringField(TEXT("componentClass"), ComponentClassObj->GetName());
|
|
||||||
if (ParentSCSNode)
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString());
|
|
||||||
}
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RemoveBlueprintComponent : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Component to remove"))
|
|
||||||
FString Component;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Remove a component from a Blueprint's SimpleConstructionScript.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
|
||||||
if (!SCS)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
|
||||||
*Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the node to remove
|
|
||||||
USCS_Node* NodeToRemove = nullptr;
|
|
||||||
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
|
||||||
for (USCS_Node* Node : AllNodes)
|
|
||||||
{
|
|
||||||
if (Node && Node->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
NodeToRemove = Node;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NodeToRemove)
|
|
||||||
{
|
|
||||||
// Build list of component names for the error message
|
|
||||||
TArray<TSharedPtr<FJsonValue>> CompList;
|
|
||||||
for (USCS_Node* Node : AllNodes)
|
|
||||||
{
|
|
||||||
if (Node)
|
|
||||||
{
|
|
||||||
CompList.Add(MakeShared<FJsonValueString>(Node->GetVariableName().ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Component '%s' not found in Blueprint '%s'"),
|
|
||||||
*Component, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("existingComponents"), CompList);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent removing the root scene component if it has children
|
|
||||||
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
|
||||||
if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). "
|
|
||||||
"Remove or re-parent the children first."),
|
|
||||||
*Component, NodeToRemove->GetChildNodes().Num()));
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing component '%s' from Blueprint '%s'"),
|
|
||||||
*Component, *Blueprint);
|
|
||||||
|
|
||||||
// Remove the node (promotes children to parent if it has any — but we've guarded root above)
|
|
||||||
SCS->RemoveNodeAndPromoteChildren(NodeToRemove);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"),
|
|
||||||
*Component, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,645 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "EdGraph/EdGraphPin.h"
|
|
||||||
#include "EdGraphSchema_K2.h"
|
|
||||||
#include "UObject/UObjectIterator.h"
|
|
||||||
#include "MCPHandlers_Discovery.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleGetPinInfo — detailed information about a specific pin
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Node to look up (GUID)"))
|
|
||||||
FString Node;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Pin name on the node"))
|
|
||||||
FString PinName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Get detailed information about a specific pin on a blueprint node, "
|
|
||||||
"including type, connections, and default values.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
UEdGraph* Graph = nullptr;
|
|
||||||
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
|
||||||
if (!FoundNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName));
|
|
||||||
if (!Pin)
|
|
||||||
{
|
|
||||||
// List available pins
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AvailPins;
|
|
||||||
for (UEdGraphPin* P : FoundNode->Pins)
|
|
||||||
{
|
|
||||||
if (P)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PinObj = MakeShared<FJsonObject>();
|
|
||||||
PinObj->SetStringField(TEXT("name"), P->PinName.ToString());
|
|
||||||
PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
|
|
||||||
PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString());
|
|
||||||
AvailPins.Add(MakeShared<FJsonValueObject>(PinObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node));
|
|
||||||
Result->SetArrayField(TEXT("availablePins"), AvailPins);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
|
||||||
Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
|
|
||||||
Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
|
|
||||||
|
|
||||||
if (!Pin->PinType.PinSubCategory.IsNone())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString());
|
|
||||||
}
|
|
||||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray());
|
|
||||||
Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet());
|
|
||||||
Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap());
|
|
||||||
Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference);
|
|
||||||
Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst);
|
|
||||||
|
|
||||||
if (!Pin->DefaultValue.IsEmpty())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
|
|
||||||
}
|
|
||||||
if (!Pin->DefaultTextValue.IsEmpty())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString());
|
|
||||||
}
|
|
||||||
if (Pin->DefaultObject)
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Connected pins
|
|
||||||
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());
|
|
||||||
CJ->SetStringField(TEXT("nodeTitle"), Linked->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
|
||||||
Conns.Add(MakeShared<FJsonValueObject>(CJ));
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("connectedTo"), Conns);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleCheckPinCompatibility — pre-flight check for connect_pins
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CheckPinConnectionCompatibility : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Source node (GUID)"))
|
|
||||||
FString SourceNode;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Source pin name"))
|
|
||||||
FString SourcePinName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Target node (GUID)"))
|
|
||||||
FString TargetNode;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Target pin name"))
|
|
||||||
FString TargetPinName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Check whether two pins can be connected, and what kind of connection would result. "
|
|
||||||
"Use as a pre-flight check before connect_blueprint_pins.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
UEdGraph* SourceGraph = nullptr;
|
|
||||||
UEdGraphNode* FoundSourceNode = MCPUtils::FindNodeByGuid(BP, SourceNode, &SourceGraph);
|
|
||||||
if (!FoundSourceNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphNode* FoundTargetNode = MCPUtils::FindNodeByGuid(BP, TargetNode);
|
|
||||||
if (!FoundTargetNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* SourcePin = FoundSourceNode->FindPin(FName(*SourcePinName));
|
|
||||||
if (!SourcePin)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* TargetPin = FoundTargetNode->FindPin(FName(*TargetPinName));
|
|
||||||
if (!TargetPin)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr;
|
|
||||||
if (!Schema)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check compatibility using the schema
|
|
||||||
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
|
|
||||||
|
|
||||||
bool bCompatible = (Response.Response != ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW);
|
|
||||||
Result->SetBoolField(TEXT("compatible"), bCompatible);
|
|
||||||
|
|
||||||
// Decode the response type
|
|
||||||
FString ResponseType;
|
|
||||||
switch (Response.Response)
|
|
||||||
{
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE:
|
|
||||||
ResponseType = TEXT("direct");
|
|
||||||
break;
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_A:
|
|
||||||
ResponseType = TEXT("breakSourceConnections");
|
|
||||||
break;
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_B:
|
|
||||||
ResponseType = TEXT("breakTargetConnections");
|
|
||||||
break;
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_AB:
|
|
||||||
ResponseType = TEXT("breakBothConnections");
|
|
||||||
break;
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE:
|
|
||||||
ResponseType = TEXT("requiresConversion");
|
|
||||||
break;
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_PROMOTION:
|
|
||||||
ResponseType = TEXT("requiresPromotion");
|
|
||||||
break;
|
|
||||||
case ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW:
|
|
||||||
default:
|
|
||||||
ResponseType = TEXT("disallowed");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Result->SetStringField(TEXT("connectionType"), ResponseType);
|
|
||||||
|
|
||||||
if (!Response.Message.IsEmpty())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("message"), Response.Message.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Include pin type info for context
|
|
||||||
Result->SetStringField(TEXT("sourcePinType"), SourcePin->PinType.PinCategory.ToString());
|
|
||||||
if (SourcePin->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
Result->SetStringField(TEXT("sourcePinSubtype"), SourcePin->PinType.PinSubCategoryObject->GetName());
|
|
||||||
Result->SetStringField(TEXT("targetPinType"), TargetPin->PinType.PinCategory.ToString());
|
|
||||||
if (TargetPin->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
Result->SetStringField(TEXT("targetPinSubtype"), TargetPin->PinType.PinSubCategoryObject->GetName());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleListClasses — discover available UClasses
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses"))
|
|
||||||
FString ParentClass;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (1-500, default 100)"))
|
|
||||||
int32 Limit = 100;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Search for available UClasses by name substring and/or parent class. "
|
|
||||||
"Returns class metadata including flags, parent class, and package.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
if (Json->HasField(TEXT("limit")))
|
|
||||||
{
|
|
||||||
Limit = FMath::Clamp(Limit, 1, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
UClass* ParentClassObj = nullptr;
|
|
||||||
if (!ParentClass.IsEmpty())
|
|
||||||
{
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (It->GetName() == ParentClass || It->GetName() == ParentClass + TEXT("_C"))
|
|
||||||
{
|
|
||||||
ParentClassObj = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!ParentClassObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClass));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ClassList;
|
|
||||||
int32 TotalMatched = 0;
|
|
||||||
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
UClass* Class = *It;
|
|
||||||
if (!Class) continue;
|
|
||||||
|
|
||||||
// Skip internal/deprecated classes
|
|
||||||
if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue;
|
|
||||||
|
|
||||||
// Apply parent filter
|
|
||||||
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
|
|
||||||
|
|
||||||
FString ClassName = Class->GetName();
|
|
||||||
|
|
||||||
// Apply name filter
|
|
||||||
if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TotalMatched++;
|
|
||||||
if (ClassList.Num() >= Limit) continue; // Count but don't add beyond limit
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> ClassObj = MakeShared<FJsonObject>();
|
|
||||||
ClassObj->SetStringField(TEXT("name"), ClassName);
|
|
||||||
ClassObj->SetStringField(TEXT("fullPath"), Class->GetPathName());
|
|
||||||
|
|
||||||
// Determine if it's a Blueprint-generated class
|
|
||||||
bool bIsBlueprint = Class->ClassGeneratedBy != nullptr;
|
|
||||||
ClassObj->SetBoolField(TEXT("isBlueprint"), bIsBlueprint);
|
|
||||||
|
|
||||||
// Parent class
|
|
||||||
if (Class->GetSuperClass())
|
|
||||||
{
|
|
||||||
ClassObj->SetStringField(TEXT("parentClass"), Class->GetSuperClass()->GetName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Module/package info
|
|
||||||
UPackage* Package = Class->GetOuterUPackage();
|
|
||||||
if (Package)
|
|
||||||
{
|
|
||||||
ClassObj->SetStringField(TEXT("package"), Package->GetName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Flags
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Flags;
|
|
||||||
if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Abstract")));
|
|
||||||
if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Interface")));
|
|
||||||
if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Add(MakeShared<FJsonValueString>(TEXT("MinimalAPI")));
|
|
||||||
if (Flags.Num() > 0)
|
|
||||||
{
|
|
||||||
ClassObj->SetArrayField(TEXT("flags"), Flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
ClassList.Add(MakeShared<FJsonValueObject>(ClassObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("count"), ClassList.Num());
|
|
||||||
Result->SetNumberField(TEXT("totalMatched"), TotalMatched);
|
|
||||||
if (TotalMatched > Limit)
|
|
||||||
{
|
|
||||||
Result->SetBoolField(TEXT("truncated"), true);
|
|
||||||
Result->SetNumberField(TEXT("limit"), Limit);
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("classes"), ClassList);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleListFunctions — list Blueprint-callable functions on a class
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListClassFunctions : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Class name to list functions for"))
|
|
||||||
FString ClassName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Substring filter for function names"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List Blueprint-callable functions on a UClass, including parameter info and flags.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Find the class
|
|
||||||
UClass* FoundClass = nullptr;
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
|
|
||||||
{
|
|
||||||
FoundClass = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!FoundClass)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName));
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> FuncList;
|
|
||||||
|
|
||||||
for (TFieldIterator<UFunction> FuncIt(FoundClass); FuncIt; ++FuncIt)
|
|
||||||
{
|
|
||||||
UFunction* Func = *FuncIt;
|
|
||||||
if (!Func) continue;
|
|
||||||
|
|
||||||
// Only include Blueprint-callable functions
|
|
||||||
if (!Func->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure | FUNC_BlueprintEvent)) continue;
|
|
||||||
|
|
||||||
FString FuncName = Func->GetName();
|
|
||||||
|
|
||||||
// Apply filter
|
|
||||||
if (!Filter.IsEmpty() && !FuncName.Contains(Filter, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> FuncObj = MakeShared<FJsonObject>();
|
|
||||||
FuncObj->SetStringField(TEXT("name"), FuncName);
|
|
||||||
|
|
||||||
// Determine the owning class
|
|
||||||
UClass* OwnerClass = Func->GetOwnerClass();
|
|
||||||
if (OwnerClass)
|
|
||||||
{
|
|
||||||
FuncObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Function flags
|
|
||||||
FuncObj->SetBoolField(TEXT("isPure"), Func->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
|
||||||
FuncObj->SetBoolField(TEXT("isStatic"), Func->HasAnyFunctionFlags(FUNC_Static));
|
|
||||||
FuncObj->SetBoolField(TEXT("isEvent"), Func->HasAnyFunctionFlags(FUNC_BlueprintEvent));
|
|
||||||
FuncObj->SetBoolField(TEXT("isConst"), Func->HasAnyFunctionFlags(FUNC_Const));
|
|
||||||
|
|
||||||
// Parameters
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Params;
|
|
||||||
FString ReturnType;
|
|
||||||
for (TFieldIterator<FProperty> PropIt(Func); PropIt; ++PropIt)
|
|
||||||
{
|
|
||||||
FProperty* Prop = *PropIt;
|
|
||||||
if (!Prop) continue;
|
|
||||||
|
|
||||||
FString PropType = Prop->GetCPPType();
|
|
||||||
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_ReturnParm))
|
|
||||||
{
|
|
||||||
ReturnType = PropType;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_Parm))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
|
||||||
ParamObj->SetStringField(TEXT("name"), Prop->GetName());
|
|
||||||
ParamObj->SetStringField(TEXT("type"), PropType);
|
|
||||||
ParamObj->SetBoolField(TEXT("isOutput"), Prop->HasAnyPropertyFlags(CPF_OutParm) && !Prop->HasAnyPropertyFlags(CPF_ReferenceParm));
|
|
||||||
ParamObj->SetBoolField(TEXT("isReference"), Prop->HasAnyPropertyFlags(CPF_ReferenceParm));
|
|
||||||
Params.Add(MakeShared<FJsonValueObject>(ParamObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
FuncObj->SetArrayField(TEXT("parameters"), Params);
|
|
||||||
if (!ReturnType.IsEmpty())
|
|
||||||
{
|
|
||||||
FuncObj->SetStringField(TEXT("returnType"), ReturnType);
|
|
||||||
}
|
|
||||||
|
|
||||||
FuncList.Add(MakeShared<FJsonValueObject>(FuncObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("className"), FoundClass->GetName());
|
|
||||||
Result->SetNumberField(TEXT("count"), FuncList.Num());
|
|
||||||
Result->SetArrayField(TEXT("functions"), FuncList);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleListProperties — list properties on a class
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListClassProperties : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Class name to list properties for"))
|
|
||||||
FString ClassName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List properties on a UClass, including type, owning class, and property flags.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Find the class
|
|
||||||
UClass* FoundClass = nullptr;
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
|
|
||||||
{
|
|
||||||
FoundClass = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!FoundClass)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName));
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> PropList;
|
|
||||||
|
|
||||||
for (TFieldIterator<FProperty> PropIt(FoundClass); PropIt; ++PropIt)
|
|
||||||
{
|
|
||||||
FProperty* Prop = *PropIt;
|
|
||||||
if (!Prop) continue;
|
|
||||||
|
|
||||||
FString PropName = Prop->GetName();
|
|
||||||
|
|
||||||
// Apply filter
|
|
||||||
if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> PropObj = MakeShared<FJsonObject>();
|
|
||||||
PropObj->SetStringField(TEXT("name"), PropName);
|
|
||||||
PropObj->SetStringField(TEXT("type"), Prop->GetCPPType());
|
|
||||||
|
|
||||||
// Determine the owning class
|
|
||||||
UClass* OwnerClass = Prop->GetOwnerClass();
|
|
||||||
if (OwnerClass)
|
|
||||||
{
|
|
||||||
PropObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Property flags
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Flags;
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Add(MakeShared<FJsonValueString>(TEXT("BlueprintVisible")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Add(MakeShared<FJsonValueString>(TEXT("BlueprintReadOnly")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Add(MakeShared<FJsonValueString>(TEXT("EditAnywhere")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Add(MakeShared<FJsonValueString>(TEXT("VisibleOnly")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Config")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Add(MakeShared<FJsonValueString>(TEXT("SaveGame")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Transient")));
|
|
||||||
if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Add(MakeShared<FJsonValueString>(TEXT("RepNotify")));
|
|
||||||
if (Flags.Num() > 0)
|
|
||||||
{
|
|
||||||
PropObj->SetArrayField(TEXT("flags"), Flags);
|
|
||||||
}
|
|
||||||
|
|
||||||
PropList.Add(MakeShared<FJsonValueObject>(PropObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("className"), FoundClass->GetName());
|
|
||||||
Result->SetNumberField(TEXT("count"), PropList.Num());
|
|
||||||
Result->SetArrayField(TEXT("properties"), PropList);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ShowCommands : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
|
|
||||||
bool Verbose = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all available commands with their descriptions.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect all handler classes sorted by tool name.
|
|
||||||
TArray<TPair<FString, UClass*>> CollectHandlers() const
|
|
||||||
{
|
|
||||||
TArray<TPair<FString, UClass*>> Handlers;
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
UClass* Class = *It;
|
|
||||||
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
|
|
||||||
const IMCPHandler* Handler = Cast<IMCPHandler>(Class->GetDefaultObject());
|
|
||||||
if (!Handler) continue;
|
|
||||||
FString ToolName = MCPUtils::GetToolName(Class);
|
|
||||||
Handlers.Add({ToolName, Class});
|
|
||||||
}
|
|
||||||
Handlers.Sort();
|
|
||||||
return Handlers;
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
|
||||||
{
|
|
||||||
auto Handlers = CollectHandlers();
|
|
||||||
Result.Appendf(TEXT("%d commands:\n"), Handlers.Num());
|
|
||||||
|
|
||||||
for (const auto& Pair : Handlers)
|
|
||||||
{
|
|
||||||
if (Verbose)
|
|
||||||
{
|
|
||||||
MCPUtils::FormatCommandHelp(Pair.Value, Result);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-verbose: just the signature line
|
|
||||||
UClass* Class = Pair.Value;
|
|
||||||
Result.Append(Pair.Key);
|
|
||||||
Result.Append(TEXT("("));
|
|
||||||
bool bFirst = true;
|
|
||||||
for (TFieldIterator<FProperty> PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt)
|
|
||||||
{
|
|
||||||
if (!bFirst) Result.Append(TEXT(","));
|
|
||||||
bFirst = false;
|
|
||||||
if (PropIt->HasMetaData(TEXT("Optional"))) Result.Append(TEXT("?"));
|
|
||||||
Result.Append(MCPUtils::PropertyNameToJsonKey(PropIt->GetName()));
|
|
||||||
}
|
|
||||||
Result.Append(TEXT(")\n"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,554 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "EdGraphSchema_K2.h"
|
|
||||||
#include "K2Node_CustomEvent.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "Kismet2/KismetEditorUtilities.h"
|
|
||||||
#include "UObject/UObjectIterator.h"
|
|
||||||
#include "MCPHandlers_Graphs.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ReparentBlueprint : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the new parent class (C++ class name or Blueprint name)"))
|
|
||||||
FString NewParentClass;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint names.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Load Blueprint
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
|
|
||||||
|
|
||||||
// Find the new parent class
|
|
||||||
// Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD)
|
|
||||||
UClass* NewParentClassObj = nullptr;
|
|
||||||
|
|
||||||
// Search across all packages for native classes
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (It->GetName() == NewParentClass)
|
|
||||||
{
|
|
||||||
NewParentClassObj = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If not found as C++ class, try loading as a Blueprint asset
|
|
||||||
if (!NewParentClassObj)
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> ParentAssets;
|
|
||||||
if (!ParentAssets.Exact(NewParentClass).AllContent().Errors(Result).ETwo().Load()) return;
|
|
||||||
if (!ParentAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
if (ParentAssets.Object()->GeneratedClass)
|
|
||||||
NewParentClassObj = ParentAssets.Object()->GeneratedClass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NewParentClassObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."),
|
|
||||||
*NewParentClass));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate: new parent must be compatible
|
|
||||||
if (BP->ParentClass && !NewParentClassObj->IsChildOf(BP->ParentClass->GetSuperClass()) &&
|
|
||||||
BP->ParentClass != NewParentClassObj)
|
|
||||||
{
|
|
||||||
// Just warn, don't block — the user may intentionally reparent to a sibling
|
|
||||||
UE_LOG(LogTemp, Warning,
|
|
||||||
TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"),
|
|
||||||
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"),
|
|
||||||
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
|
|
||||||
|
|
||||||
// Perform reparent
|
|
||||||
BP->PreEditChange(nullptr);
|
|
||||||
BP->ParentClass = NewParentClassObj;
|
|
||||||
BP->PostEditChange();
|
|
||||||
|
|
||||||
// Refresh all nodes to pick up new parent's functions/variables
|
|
||||||
FBlueprintEditorUtils::RefreshAllNodes(BP);
|
|
||||||
|
|
||||||
// Compile
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(BP);
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
FString NewParentActualName = NewParentClassObj->GetName();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"),
|
|
||||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("oldParentClass"), OldParentName);
|
|
||||||
Result->SetStringField(TEXT("newParentClass"), NewParentActualName);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateBlueprintAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="New Blueprint asset name"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
|
||||||
FString PackagePath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)"))
|
|
||||||
FString ParentClass;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)"))
|
|
||||||
FString BlueprintType;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new Blueprint asset with a specified parent class and type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Validate packagePath starts with /Game
|
|
||||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if asset already exists
|
|
||||||
FString FullAssetPath = PackagePath / Blueprint;
|
|
||||||
MCPAssets<UBlueprint> ExistCheck;
|
|
||||||
if (!ExistCheck.Exact(Blueprint).Errors(Result).EAny().Info()) return;
|
|
||||||
|
|
||||||
// Resolve parent class — try C++ class first, then Blueprint
|
|
||||||
UClass* ParentClassObj = nullptr;
|
|
||||||
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (It->GetName() == ParentClass)
|
|
||||||
{
|
|
||||||
ParentClassObj = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParentClassObj)
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> ParentAssets;
|
|
||||||
if (!ParentAssets.Exact(ParentClass).AllContent().Errors(Result).ETwo().Load()) return;
|
|
||||||
if (!ParentAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
if (ParentAssets.Object()->GeneratedClass)
|
|
||||||
ParentClassObj = ParentAssets.Object()->GeneratedClass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParentClassObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."),
|
|
||||||
*ParentClass));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Map blueprintType string to EBlueprintType
|
|
||||||
EBlueprintType BlueprintTypeEnum = BPTYPE_Normal;
|
|
||||||
if (!BlueprintType.IsEmpty())
|
|
||||||
{
|
|
||||||
if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, Result, TEXT("BPTYPE_"))) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// For Interface type, parent must be UInterface
|
|
||||||
if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass()))
|
|
||||||
{
|
|
||||||
// Use the engine's standard BlueprintInterface parent
|
|
||||||
ParentClassObj = UInterface::StaticClass();
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"),
|
|
||||||
*Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType);
|
|
||||||
|
|
||||||
// Create the package
|
|
||||||
FString FullPackagePath = PackagePath / Blueprint;
|
|
||||||
UPackage* Package = CreatePackage(*FullPackagePath);
|
|
||||||
if (!Package)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Blueprint
|
|
||||||
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
|
|
||||||
ParentClassObj,
|
|
||||||
Package,
|
|
||||||
FName(*Blueprint),
|
|
||||||
BlueprintTypeEnum,
|
|
||||||
UBlueprint::StaticClass(),
|
|
||||||
UBlueprintGeneratedClass::StaticClass()
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!NewBP)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(NewBP);
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
|
|
||||||
|
|
||||||
|
|
||||||
// Collect graph names
|
|
||||||
TArray<TSharedPtr<FJsonValue>> GraphNames = MCPUtils::AllGraphNamesJson(NewBP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"),
|
|
||||||
*Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
|
||||||
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateBlueprintGraph : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name for the new graph"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent"))
|
|
||||||
FString GraphType;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new function, macro, or custom event graph in a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load Blueprint
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Check graph name uniqueness
|
|
||||||
if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also check for existing custom events with the same name
|
|
||||||
if (GraphType == TEXT("customEvent"))
|
|
||||||
{
|
|
||||||
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
if (CE->CustomFunctionName == FName(*Graph))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"),
|
|
||||||
*GraphType, *Graph, *Blueprint);
|
|
||||||
|
|
||||||
FString CreatedNodeId;
|
|
||||||
|
|
||||||
if (GraphType == TEXT("function"))
|
|
||||||
{
|
|
||||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
|
|
||||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
|
||||||
if (!NewGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph"));
|
|
||||||
}
|
|
||||||
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
|
|
||||||
}
|
|
||||||
else if (GraphType == TEXT("macro"))
|
|
||||||
{
|
|
||||||
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
|
|
||||||
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
|
||||||
if (!NewGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph"));
|
|
||||||
}
|
|
||||||
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
|
|
||||||
}
|
|
||||||
else // customEvent
|
|
||||||
{
|
|
||||||
// Find the EventGraph (first UbergraphPage)
|
|
||||||
UEdGraph* EventGraph = nullptr;
|
|
||||||
if (BP->UbergraphPages.Num() > 0)
|
|
||||||
{
|
|
||||||
EventGraph = BP->UbergraphPages[0];
|
|
||||||
}
|
|
||||||
if (!EventGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a custom event node in the EventGraph
|
|
||||||
UK2Node_CustomEvent* NewEvent = NewObject<UK2Node_CustomEvent>(EventGraph);
|
|
||||||
NewEvent->CustomFunctionName = FName(*Graph);
|
|
||||||
NewEvent->bIsEditable = true;
|
|
||||||
EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false);
|
|
||||||
NewEvent->CreateNewGuid();
|
|
||||||
NewEvent->PostPlacedNewNode();
|
|
||||||
NewEvent->AllocateDefaultPins();
|
|
||||||
CreatedNodeId = NewEvent->NodeGuid.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"),
|
|
||||||
*GraphType, *Graph, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
if (!CreatedNodeId.IsEmpty())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("nodeId"), CreatedNodeId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DeleteBlueprintGraph : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the graph to delete"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Find the graph
|
|
||||||
UEdGraph* TargetGraph = nullptr;
|
|
||||||
FString GraphType;
|
|
||||||
|
|
||||||
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
|
|
||||||
{
|
|
||||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
TargetGraph = CandidateGraph;
|
|
||||||
GraphType = TEXT("function");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!TargetGraph)
|
|
||||||
{
|
|
||||||
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
|
|
||||||
{
|
|
||||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
TargetGraph = CandidateGraph;
|
|
||||||
GraphType = TEXT("macro");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
|
|
||||||
if (!TargetGraph)
|
|
||||||
{
|
|
||||||
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
|
|
||||||
{
|
|
||||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),
|
|
||||||
*Graph));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"),
|
|
||||||
*GraphType, *Graph, *Blueprint);
|
|
||||||
|
|
||||||
// Count nodes for reporting
|
|
||||||
int32 NodeCount = TargetGraph->Nodes.Num();
|
|
||||||
|
|
||||||
// Remove the graph
|
|
||||||
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"),
|
|
||||||
*Graph, NodeCount, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
|
||||||
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RenameBlueprintGraph : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Current name of the graph to rename"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="New name for the graph"))
|
|
||||||
FString NewName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Check if it's an UbergraphPage — disallow rename
|
|
||||||
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
|
|
||||||
{
|
|
||||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
|
|
||||||
*Graph));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the graph in FunctionGraphs or MacroGraphs
|
|
||||||
UEdGraph* TargetGraph = nullptr;
|
|
||||||
FString GraphType;
|
|
||||||
|
|
||||||
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
|
|
||||||
{
|
|
||||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
TargetGraph = CandidateGraph;
|
|
||||||
GraphType = TEXT("function");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!TargetGraph)
|
|
||||||
{
|
|
||||||
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
|
|
||||||
{
|
|
||||||
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
TargetGraph = CandidateGraph;
|
|
||||||
GraphType = TEXT("macro");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TargetGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for name collision
|
|
||||||
for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName))
|
|
||||||
{
|
|
||||||
if (Existing != TargetGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"),
|
|
||||||
*GraphType, *Graph, *NewName, *Blueprint);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"),
|
|
||||||
*Graph, *NewName, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("newName"), TargetGraph->GetName());
|
|
||||||
Result->SetStringField(TEXT("graphType"), GraphType);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,303 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPServer.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "UObject/UObjectIterator.h"
|
|
||||||
#include "MCPHandlers_Interfaces.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all Blueprint Interfaces implemented by a Blueprint, "
|
|
||||||
"including their function graphs.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InterfacesArr;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (!IfaceDesc.Interface)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> IfaceObj = MakeShared<FJsonObject>();
|
|
||||||
IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName());
|
|
||||||
IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName());
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
|
||||||
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
|
||||||
{
|
|
||||||
if (Graph)
|
|
||||||
{
|
|
||||||
FuncArr.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
IfaceObj->SetArrayField(TEXT("functions"), FuncArr);
|
|
||||||
|
|
||||||
InterfacesArr.Add(MakeShared<FJsonValueObject>(IfaceObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("count"), InterfacesArr.Num());
|
|
||||||
Result->SetArrayField(TEXT("interfaces"), InterfacesArr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name"))
|
|
||||||
FString InterfaceName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a Blueprint Interface implementation to a Blueprint. "
|
|
||||||
"Creates stub function graphs for each interface function.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Resolve the interface class
|
|
||||||
UClass* InterfaceClass = nullptr;
|
|
||||||
|
|
||||||
// Strategy 1: Search loaded UInterface classes by name
|
|
||||||
for (TObjectIterator<UClass> It; It; ++It)
|
|
||||||
{
|
|
||||||
if (!It->IsChildOf(UInterface::StaticClass()))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString ClassName = It->GetName();
|
|
||||||
// Match by class name (e.g. "BPI_Foo_C") or by trimmed name (e.g. "BPI_Foo")
|
|
||||||
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
InterfaceClass = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Strip the generated "_C" suffix for comparison
|
|
||||||
FString TrimmedName = ClassName;
|
|
||||||
if (TrimmedName.EndsWith(TEXT("_C")))
|
|
||||||
{
|
|
||||||
TrimmedName = TrimmedName.LeftChop(2);
|
|
||||||
}
|
|
||||||
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
InterfaceClass = *It;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: Try loading as a Blueprint Interface asset
|
|
||||||
if (!InterfaceClass)
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> IfaceAssets;
|
|
||||||
if (!IfaceAssets.Exact(InterfaceName).AllContent().Errors(Result).ETwo().Load()) return;
|
|
||||||
if (!IfaceAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
UClass* GenClass = IfaceAssets.Object()->GeneratedClass;
|
|
||||||
if (GenClass && GenClass->IsChildOf(UInterface::StaticClass()))
|
|
||||||
InterfaceClass = GenClass;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!InterfaceClass)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."),
|
|
||||||
*InterfaceName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicates
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (IfaceDesc.Interface == InterfaceClass)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Interface '%s' is already implemented by Blueprint '%s'"),
|
|
||||||
*InterfaceName, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"),
|
|
||||||
*InterfaceClass->GetName(), *Blueprint);
|
|
||||||
|
|
||||||
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
|
|
||||||
if (!bAdded)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"),
|
|
||||||
*InterfaceName, *Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Collect stub function graph names from the newly added interface entry
|
|
||||||
TArray<FString> AddedFunctions;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (IfaceDesc.Interface == InterfaceClass)
|
|
||||||
{
|
|
||||||
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
|
||||||
{
|
|
||||||
if (Graph)
|
|
||||||
{
|
|
||||||
AddedFunctions.Add(Graph->GetName());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"),
|
|
||||||
*InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num());
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName());
|
|
||||||
Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName());
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
|
||||||
for (const FString& FuncName : AddedFunctions)
|
|
||||||
{
|
|
||||||
FuncArr.Add(MakeShared<FJsonValueString>(FuncName));
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("functionGraphsAdded"), FuncArr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Interface name to remove"))
|
|
||||||
FString InterfaceName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions"))
|
|
||||||
bool PreserveFunctions = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Remove a Blueprint Interface implementation from a Blueprint. "
|
|
||||||
"Optionally preserve the function graphs as regular functions.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Find the interface in ImplementedInterfaces by name (case-insensitive)
|
|
||||||
UClass* FoundInterface = nullptr;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (!IfaceDesc.Interface)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
FString ClassName = IfaceDesc.Interface->GetName();
|
|
||||||
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
FoundInterface = IfaceDesc.Interface;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
// Strip "_C" suffix for comparison
|
|
||||||
FString TrimmedName = ClassName;
|
|
||||||
if (TrimmedName.EndsWith(TEXT("_C")))
|
|
||||||
{
|
|
||||||
TrimmedName = TrimmedName.LeftChop(2);
|
|
||||||
}
|
|
||||||
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
FoundInterface = IfaceDesc.Interface;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!FoundInterface)
|
|
||||||
{
|
|
||||||
// Build helpful error with list of implemented interfaces
|
|
||||||
TArray<TSharedPtr<FJsonValue>> IfaceList;
|
|
||||||
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
|
||||||
{
|
|
||||||
if (IfaceDesc.Interface)
|
|
||||||
{
|
|
||||||
IfaceList.Add(MakeShared<FJsonValueString>(IfaceDesc.Interface->GetName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Interface '%s' is not implemented by Blueprint '%s'"),
|
|
||||||
*InterfaceName, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"),
|
|
||||||
*FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"),
|
|
||||||
*FoundInterface->GetName(), *Blueprint);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,722 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Materials/Material.h"
|
|
||||||
#include "Materials/MaterialInterface.h"
|
|
||||||
#include "Materials/MaterialInstanceConstant.h"
|
|
||||||
#include "Materials/MaterialExpressionScalarParameter.h"
|
|
||||||
#include "Materials/MaterialExpressionVectorParameter.h"
|
|
||||||
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
|
||||||
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
|
||||||
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
|
||||||
#include "AssetToolsModule.h"
|
|
||||||
#include "IAssetTools.h"
|
|
||||||
#include "Engine/Texture.h"
|
|
||||||
#include "MCPHandlers_MaterialInstance.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateMaterialInstanceAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Name for the new Material Instance asset"))
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
|
||||||
FString PackagePath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)"))
|
|
||||||
FString ParentMaterial;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new Material Instance Constant asset with a specified parent material.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Validate packagePath starts with /Game
|
|
||||||
if (!PackagePath.StartsWith(TEXT("/Game")))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if asset already exists
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterialInstanceConstant> ExistCheck;
|
|
||||||
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load parent material — try as Material first, then as Material Instance
|
|
||||||
UMaterialInterface* ParentMaterialObj = nullptr;
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterial> MatAssets;
|
|
||||||
if (MatAssets.Exact(ParentMaterial).ETwo().Load() && !MatAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
ParentMaterialObj = MatAssets.Object();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterialInstanceConstant> MIAssets;
|
|
||||||
if (MIAssets.Exact(ParentMaterial).ETwo().Load() && !MIAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
ParentMaterialObj = MIAssets.Object();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParentMaterialObj)
|
|
||||||
{
|
|
||||||
// Also try LoadObject as a fallback with the raw path
|
|
||||||
ParentMaterialObj = LoadObject<UMaterialInterface>(nullptr, *ParentMaterial);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ParentMaterialObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
|
||||||
*ParentMaterial));
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Instance '%s' in '%s' with parent '%s'"),
|
|
||||||
*Name, *PackagePath, *ParentMaterialObj->GetName());
|
|
||||||
|
|
||||||
// Create via factory + AssetTools
|
|
||||||
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
|
||||||
UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>();
|
|
||||||
|
|
||||||
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
|
|
||||||
if (!NewAsset)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
|
|
||||||
if (!MI)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set parent
|
|
||||||
MI->PreEditChange(nullptr);
|
|
||||||
MI->Parent = ParentMaterialObj;
|
|
||||||
MI->PostEditChange();
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
|
||||||
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Instance '%s' with parent '%s' (saved: %s)"),
|
|
||||||
*Name, *ParentMaterialObj->GetName(), bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
|
||||||
Result->SetStringField(TEXT("parent"), ParentMaterialObj->GetPathName());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetMaterialInstanceParameter : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material Instance name or path"))
|
|
||||||
FString MaterialInstance;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Parameter name to set"))
|
|
||||||
FString ParameterName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Value to set (number for scalar, object with r/g/b/a for vector, string path for texture, bool for staticSwitch)"))
|
|
||||||
FMCPJsonObject Value;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Parameter type: scalar, vector, texture, staticSwitch. Auto-detected from parent if omitted."))
|
|
||||||
FString Type;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, validate without applying changes"))
|
|
||||||
bool DryRun = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Set a parameter override on a Material Instance. "
|
|
||||||
"Supports scalar, vector, texture, and static switch parameter types.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
if (!Json->HasField(TEXT("value")))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load the Material Instance
|
|
||||||
MCPAssets<UMaterialInstanceConstant> Assets;
|
|
||||||
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterialInstanceConstant* MI = Assets.Object();
|
|
||||||
|
|
||||||
// Determine the parameter type — explicit or auto-detect from parent
|
|
||||||
FString TypeStr = Type;
|
|
||||||
|
|
||||||
// Auto-detect type from parent material's parameters if not provided
|
|
||||||
if (TypeStr.IsEmpty())
|
|
||||||
{
|
|
||||||
UMaterialInterface* ParentMat = MI->Parent;
|
|
||||||
while (ParentMat)
|
|
||||||
{
|
|
||||||
UMaterial* BaseMat = ParentMat->GetMaterial();
|
|
||||||
if (BaseMat)
|
|
||||||
{
|
|
||||||
// Check scalar parameters
|
|
||||||
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
|
||||||
{
|
|
||||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
||||||
{
|
|
||||||
if (SP->ParameterName.ToString() == ParameterName)
|
|
||||||
{
|
|
||||||
TypeStr = TEXT("scalar");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
||||||
{
|
|
||||||
if (VP->ParameterName.ToString() == ParameterName)
|
|
||||||
{
|
|
||||||
TypeStr = TEXT("vector");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
||||||
{
|
|
||||||
if (TP->ParameterName.ToString() == ParameterName)
|
|
||||||
{
|
|
||||||
TypeStr = TEXT("texture");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
||||||
{
|
|
||||||
if (SSP->ParameterName.ToString() == ParameterName)
|
|
||||||
{
|
|
||||||
TypeStr = TEXT("staticSwitch");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break; // Only need to check the base material
|
|
||||||
}
|
|
||||||
// Walk up the parent chain if it's an MI parented to another MI
|
|
||||||
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(ParentMat);
|
|
||||||
if (ParentMI)
|
|
||||||
{
|
|
||||||
ParentMat = ParentMI->Parent;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TypeStr.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."),
|
|
||||||
*ParameterName));
|
|
||||||
}
|
|
||||||
|
|
||||||
FString NewValueDescription;
|
|
||||||
FMaterialParameterInfo ParamInfo(*ParameterName);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' (type=%s) on Material Instance '%s'"),
|
|
||||||
DryRun ? TEXT("[DRY RUN] Setting") : TEXT("Setting"),
|
|
||||||
*ParameterName, *TypeStr, *MaterialInstance);
|
|
||||||
|
|
||||||
if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
// Scalar parameter — value is a number
|
|
||||||
double FloatValue = Json->GetNumberField(TEXT("value"));
|
|
||||||
|
|
||||||
if (!DryRun)
|
|
||||||
{
|
|
||||||
MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue);
|
|
||||||
}
|
|
||||||
NewValueDescription = FString::Printf(TEXT("%f"), FloatValue);
|
|
||||||
}
|
|
||||||
else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
// Vector parameter — value is { r, g, b, a? }
|
|
||||||
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
|
||||||
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields."));
|
|
||||||
}
|
|
||||||
|
|
||||||
double R = (*ValueObj)->GetNumberField(TEXT("r"));
|
|
||||||
double G = (*ValueObj)->GetNumberField(TEXT("g"));
|
|
||||||
double B = (*ValueObj)->GetNumberField(TEXT("b"));
|
|
||||||
double A = (*ValueObj)->HasField(TEXT("a")) ? (*ValueObj)->GetNumberField(TEXT("a")) : 1.0;
|
|
||||||
|
|
||||||
FLinearColor Color((float)R, (float)G, (float)B, (float)A);
|
|
||||||
|
|
||||||
if (!DryRun)
|
|
||||||
{
|
|
||||||
MI->SetVectorParameterValueEditorOnly(ParamInfo, Color);
|
|
||||||
}
|
|
||||||
NewValueDescription = FString::Printf(TEXT("(R=%f, G=%f, B=%f, A=%f)"), R, G, B, A);
|
|
||||||
}
|
|
||||||
else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
// Texture parameter — value is a texture path string
|
|
||||||
FString TexturePath = Json->GetStringField(TEXT("value"));
|
|
||||||
if (TexturePath.IsEmpty())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string."));
|
|
||||||
}
|
|
||||||
|
|
||||||
UTexture* TextureObj = LoadObject<UTexture>(nullptr, *TexturePath);
|
|
||||||
if (!TextureObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!DryRun)
|
|
||||||
{
|
|
||||||
MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj);
|
|
||||||
}
|
|
||||||
NewValueDescription = TexturePath;
|
|
||||||
}
|
|
||||||
else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
// Static switch parameter — value is a bool
|
|
||||||
bool bSwitchValue = Json->GetBoolField(TEXT("value"));
|
|
||||||
|
|
||||||
if (!DryRun)
|
|
||||||
{
|
|
||||||
// Modify static parameters
|
|
||||||
FStaticParameterSet StaticParams;
|
|
||||||
MI->GetStaticParameterValues(StaticParams);
|
|
||||||
|
|
||||||
bool bFound = false;
|
|
||||||
for (FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
|
||||||
{
|
|
||||||
if (Param.ParameterInfo.Name == FName(*ParameterName))
|
|
||||||
{
|
|
||||||
Param.Value = bSwitchValue;
|
|
||||||
Param.bOverride = true;
|
|
||||||
bFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bFound)
|
|
||||||
{
|
|
||||||
// Add new static switch parameter entry
|
|
||||||
FStaticSwitchParameter NewParam;
|
|
||||||
NewParam.ParameterInfo.Name = FName(*ParameterName);
|
|
||||||
NewParam.Value = bSwitchValue;
|
|
||||||
NewParam.bOverride = true;
|
|
||||||
StaticParams.StaticSwitchParameters.Add(NewParam);
|
|
||||||
}
|
|
||||||
|
|
||||||
MI->UpdateStaticPermutation(StaticParams);
|
|
||||||
}
|
|
||||||
NewValueDescription = bSwitchValue ? TEXT("true") : TEXT("false");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"),
|
|
||||||
*TypeStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!DryRun)
|
|
||||||
{
|
|
||||||
MI->PreEditChange(nullptr);
|
|
||||||
MI->PostEditChange();
|
|
||||||
MI->MarkPackageDirty();
|
|
||||||
MCPUtils::SaveGenericPackage(MI);
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"),
|
|
||||||
DryRun ? TEXT("[DRY RUN] Would set") : TEXT("Set"),
|
|
||||||
*ParameterName, *NewValueDescription, *MaterialInstance);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("type"), TypeStr);
|
|
||||||
Result->SetStringField(TEXT("newValue"), NewValueDescription);
|
|
||||||
if (DryRun)
|
|
||||||
{
|
|
||||||
Result->SetBoolField(TEXT("dryRun"), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DumpMaterialInstanceParameters : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material Instance name or path to inspect"))
|
|
||||||
FString MaterialInstance;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all parameters on a Material Instance, including overridden and inherited parameters.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterialInstanceConstant> Assets;
|
|
||||||
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterialInstanceConstant* MI = Assets.Object();
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("name"), MI->GetName());
|
|
||||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
|
||||||
|
|
||||||
// Parent info
|
|
||||||
if (MI->Parent)
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("parent"), MI->Parent->GetPathName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build parent chain
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ParentChainArr;
|
|
||||||
{
|
|
||||||
UMaterialInterface* Current = MI->Parent;
|
|
||||||
while (Current)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> ParentObj = MakeShared<FJsonObject>();
|
|
||||||
ParentObj->SetStringField(TEXT("name"), Current->GetName());
|
|
||||||
ParentObj->SetStringField(TEXT("path"), Current->GetPathName());
|
|
||||||
ParentObj->SetStringField(TEXT("class"), Current->GetClass()->GetName());
|
|
||||||
ParentChainArr.Add(MakeShared<FJsonValueObject>(ParentObj));
|
|
||||||
|
|
||||||
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(Current);
|
|
||||||
if (ParentMI)
|
|
||||||
{
|
|
||||||
Current = ParentMI->Parent;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break; // Reached the root Material
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("parentChain"), ParentChainArr);
|
|
||||||
|
|
||||||
// Scalar parameters
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ScalarArr;
|
|
||||||
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), true); // Present in ScalarParameterValues means it's overridden
|
|
||||||
ScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
|
||||||
|
|
||||||
// Vector parameters
|
|
||||||
TArray<TSharedPtr<FJsonValue>> VectorArr;
|
|
||||||
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
|
||||||
PObj->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
|
||||||
PObj->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
|
||||||
PObj->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), true);
|
|
||||||
VectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
|
||||||
|
|
||||||
// Texture parameters
|
|
||||||
TArray<TSharedPtr<FJsonValue>> TextureArr;
|
|
||||||
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
if (Param.ParameterValue)
|
|
||||||
{
|
|
||||||
PObj->SetStringField(TEXT("texture"), Param.ParameterValue->GetPathName());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PObj->SetStringField(TEXT("texture"), TEXT("None"));
|
|
||||||
}
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), true);
|
|
||||||
TextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
|
||||||
|
|
||||||
// Static switch parameters
|
|
||||||
TArray<TSharedPtr<FJsonValue>> StaticSwitchArr;
|
|
||||||
{
|
|
||||||
FStaticParameterSet StaticParams;
|
|
||||||
MI->GetStaticParameterValues(StaticParams);
|
|
||||||
|
|
||||||
for (const FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetBoolField(TEXT("value"), Param.Value);
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), Param.bOverride);
|
|
||||||
StaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
|
||||||
|
|
||||||
// Also report inherited parameters from the parent material for discoverability
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InheritedScalarArr;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InheritedVectorArr;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InheritedTextureArr;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InheritedStaticSwitchArr;
|
|
||||||
{
|
|
||||||
UMaterial* BaseMat = MI->GetMaterial();
|
|
||||||
if (BaseMat)
|
|
||||||
{
|
|
||||||
// Collect names of already-overridden parameters for filtering
|
|
||||||
TSet<FString> OverriddenScalars;
|
|
||||||
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
|
|
||||||
{
|
|
||||||
OverriddenScalars.Add(P.ParameterInfo.Name.ToString());
|
|
||||||
}
|
|
||||||
TSet<FString> OverriddenVectors;
|
|
||||||
for (const FVectorParameterValue& P : MI->VectorParameterValues)
|
|
||||||
{
|
|
||||||
OverriddenVectors.Add(P.ParameterInfo.Name.ToString());
|
|
||||||
}
|
|
||||||
TSet<FString> OverriddenTextures;
|
|
||||||
for (const FTextureParameterValue& P : MI->TextureParameterValues)
|
|
||||||
{
|
|
||||||
OverriddenTextures.Add(P.ParameterInfo.Name.ToString());
|
|
||||||
}
|
|
||||||
TSet<FString> OverriddenStaticSwitches;
|
|
||||||
{
|
|
||||||
FStaticParameterSet SP;
|
|
||||||
MI->GetStaticParameterValues(SP);
|
|
||||||
for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters)
|
|
||||||
{
|
|
||||||
if (P.bOverride)
|
|
||||||
{
|
|
||||||
OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
|
||||||
{
|
|
||||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
||||||
{
|
|
||||||
if (!OverriddenScalars.Contains(SP->ParameterName.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
|
||||||
PObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
|
||||||
InheritedScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
||||||
{
|
|
||||||
if (!OverriddenVectors.Contains(VP->ParameterName.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
|
||||||
PObj->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
|
||||||
PObj->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
|
||||||
PObj->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
|
||||||
PObj->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
|
||||||
InheritedVectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
||||||
{
|
|
||||||
if (!OverriddenTextures.Contains(TP->ParameterName.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
|
||||||
if (TP->Texture)
|
|
||||||
{
|
|
||||||
PObj->SetStringField(TEXT("defaultTexture"), TP->Texture->GetPathName());
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
PObj->SetStringField(TEXT("defaultTexture"), TEXT("None"));
|
|
||||||
}
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
|
||||||
InheritedTextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
||||||
{
|
|
||||||
if (!OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
|
||||||
PObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
|
||||||
PObj->SetBoolField(TEXT("isOverridden"), false);
|
|
||||||
InheritedStaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge inherited (non-overridden) params into the arrays
|
|
||||||
for (const TSharedPtr<FJsonValue>& V : InheritedScalarArr)
|
|
||||||
{
|
|
||||||
ScalarArr.Add(V);
|
|
||||||
}
|
|
||||||
for (const TSharedPtr<FJsonValue>& V : InheritedVectorArr)
|
|
||||||
{
|
|
||||||
VectorArr.Add(V);
|
|
||||||
}
|
|
||||||
for (const TSharedPtr<FJsonValue>& V : InheritedTextureArr)
|
|
||||||
{
|
|
||||||
TextureArr.Add(V);
|
|
||||||
}
|
|
||||||
for (const TSharedPtr<FJsonValue>& V : InheritedStaticSwitchArr)
|
|
||||||
{
|
|
||||||
StaticSwitchArr.Add(V);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update arrays with merged data
|
|
||||||
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
|
||||||
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
|
||||||
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
|
||||||
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ReparentMaterialInstance : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material Instance name or path to reparent"))
|
|
||||||
FString MaterialInstance;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="New parent material name or path (Material or Material Instance)"))
|
|
||||||
FString NewParent;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, validate without applying changes"))
|
|
||||||
bool DryRun = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Change the parent material of a Material Instance. "
|
|
||||||
"Validates against circular parent chains.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Load the Material Instance
|
|
||||||
MCPAssets<UMaterialInstanceConstant> Assets;
|
|
||||||
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterialInstanceConstant* MI = Assets.Object();
|
|
||||||
|
|
||||||
// Capture old parent
|
|
||||||
FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None");
|
|
||||||
|
|
||||||
// Load new parent — try as Material first, then as Material Instance
|
|
||||||
UMaterialInterface* NewParentObj = nullptr;
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterial> MatAssets;
|
|
||||||
if (MatAssets.Exact(NewParent).ETwo().Load() && !MatAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
NewParentObj = MatAssets.Object();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterialInstanceConstant> MIAssets;
|
|
||||||
if (MIAssets.Exact(NewParent).ETwo().Load() && !MIAssets.Objects().IsEmpty())
|
|
||||||
{
|
|
||||||
NewParentObj = MIAssets.Object();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NewParentObj)
|
|
||||||
{
|
|
||||||
// Try LoadObject as a fallback
|
|
||||||
NewParentObj = LoadObject<UMaterialInterface>(nullptr, *NewParent);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!NewParentObj)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
|
||||||
*NewParent));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent circular parenting — check if NewParent is this MI or has this MI in its chain
|
|
||||||
{
|
|
||||||
UMaterialInterface* Check = NewParentObj;
|
|
||||||
while (Check)
|
|
||||||
{
|
|
||||||
if (Check == MI)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."),
|
|
||||||
*MaterialInstance, *NewParent));
|
|
||||||
}
|
|
||||||
UMaterialInstanceConstant* CheckMI = Cast<UMaterialInstanceConstant>(Check);
|
|
||||||
if (CheckMI)
|
|
||||||
{
|
|
||||||
Check = CheckMI->Parent;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s Material Instance '%s': parent '%s' -> '%s'"),
|
|
||||||
DryRun ? TEXT("[DRY RUN] Reparenting") : TEXT("Reparenting"),
|
|
||||||
*MaterialInstance, *OldParentPath, *NewParentObj->GetPathName());
|
|
||||||
|
|
||||||
if (!DryRun)
|
|
||||||
{
|
|
||||||
MI->PreEditChange(nullptr);
|
|
||||||
MI->Parent = NewParentObj;
|
|
||||||
MI->PostEditChange();
|
|
||||||
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"),
|
|
||||||
*MaterialInstance, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("oldParent"), OldParentPath);
|
|
||||||
Result->SetStringField(TEXT("newParent"), NewParentObj->GetPathName());
|
|
||||||
if (DryRun)
|
|
||||||
{
|
|
||||||
Result->SetBoolField(TEXT("dryRun"), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,962 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Materials/Material.h"
|
|
||||||
#include "MaterialDomain.h"
|
|
||||||
#include "Materials/MaterialInstanceConstant.h"
|
|
||||||
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
|
||||||
#include "MaterialGraph/MaterialGraphNode.h"
|
|
||||||
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
|
||||||
#include "MaterialGraph/MaterialGraphSchema.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "AssetRegistry/AssetRegistryModule.h"
|
|
||||||
#include "AssetRegistry/IAssetRegistry.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "MCPHandlers_MaterialRead.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListMaterialAssets : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Optional, Description="Filter string to match against material name or path"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Type filter: 'all', 'material', or 'instance'"))
|
|
||||||
FString Type;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List Material and MaterialInstance assets, optionally filtered by name and type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
bool bIncludeMaterials = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("material");
|
|
||||||
bool bIncludeInstances = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("instance");
|
|
||||||
|
|
||||||
MCPAssets<UMaterial> Assets;
|
|
||||||
if (bIncludeMaterials) Assets.Scan(UMaterial::StaticClass());
|
|
||||||
if (bIncludeInstances) Assets.Scan(UMaterialInstanceConstant::StaticClass());
|
|
||||||
Assets.Substring(Filter).NoDerived().Info();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
|
||||||
for (const FAssetData& Asset : Assets.AllData())
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
||||||
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
|
|
||||||
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
|
|
||||||
Entry->SetStringField(TEXT("type"),
|
|
||||||
Asset.AssetClassPath.GetAssetName() == TEXT("MaterialInstanceConstant")
|
|
||||||
? TEXT("MaterialInstance") : TEXT("Material"));
|
|
||||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
|
||||||
Result->SetArrayField(TEXT("materials"), Entries);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DumpMaterial : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
|
|
||||||
FString Material;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Get detailed info about a material or material instance, including parameters, usage flags, and referenced textures.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString DecodedName = MCPUtils::UrlDecode(Material);
|
|
||||||
|
|
||||||
// Try loading as UMaterial or UMaterialInstanceConstant
|
|
||||||
MCPAssets<UMaterialInterface> Assets;
|
|
||||||
Assets.Scan(UMaterial::StaticClass());
|
|
||||||
Assets.Scan(UMaterialInstanceConstant::StaticClass());
|
|
||||||
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterialInterface* LoadedObj = Assets.Object();
|
|
||||||
|
|
||||||
if (UMaterial* MaterialObj = Cast<UMaterial>(LoadedObj))
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *MaterialObj->GetName());
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("name"), MaterialObj->GetName());
|
|
||||||
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
|
|
||||||
Result->SetStringField(TEXT("type"), TEXT("Material"));
|
|
||||||
|
|
||||||
// Material domain
|
|
||||||
FString DomainStr = TEXT("Unknown");
|
|
||||||
if (const UEnum* DomainEnum = StaticEnum<EMaterialDomain>())
|
|
||||||
{
|
|
||||||
DomainStr = DomainEnum->GetNameStringByValue((int64)MaterialObj->MaterialDomain);
|
|
||||||
}
|
|
||||||
Result->SetStringField(TEXT("domain"), DomainStr);
|
|
||||||
|
|
||||||
// Blend mode
|
|
||||||
FString BlendModeStr = TEXT("Unknown");
|
|
||||||
if (const UEnum* BlendEnum = StaticEnum<EBlendMode>())
|
|
||||||
{
|
|
||||||
BlendModeStr = BlendEnum->GetNameStringByValue((int64)MaterialObj->BlendMode);
|
|
||||||
}
|
|
||||||
Result->SetStringField(TEXT("blendMode"), BlendModeStr);
|
|
||||||
|
|
||||||
// Shading models
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ShadingModels;
|
|
||||||
FMaterialShadingModelField SMField = MaterialObj->GetShadingModels();
|
|
||||||
if (const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>())
|
|
||||||
{
|
|
||||||
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
|
|
||||||
{
|
|
||||||
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
|
|
||||||
if (SMField.HasShadingModel(SM))
|
|
||||||
{
|
|
||||||
ShadingModels.Add(MakeShared<FJsonValueString>(SMEnum->GetNameStringByIndex(i)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("shadingModels"), ShadingModels);
|
|
||||||
|
|
||||||
// Two-sided
|
|
||||||
Result->SetBoolField(TEXT("twoSided"), MaterialObj->IsTwoSided());
|
|
||||||
|
|
||||||
// Expression count
|
|
||||||
auto Expressions = MaterialObj->GetExpressions();
|
|
||||||
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
|
||||||
|
|
||||||
// Parameters — iterate expressions for parameter types
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Parameters;
|
|
||||||
for (UMaterialExpression* Expr : Expressions)
|
|
||||||
{
|
|
||||||
if (!Expr) continue;
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
|
||||||
bool bIsParam = false;
|
|
||||||
|
|
||||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
||||||
{
|
|
||||||
bIsParam = true;
|
|
||||||
ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
|
||||||
ParamObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
|
||||||
ParamObj->SetStringField(TEXT("group"), SP->Group.ToString());
|
|
||||||
ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
|
||||||
}
|
|
||||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
||||||
{
|
|
||||||
bIsParam = true;
|
|
||||||
ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
|
||||||
ParamObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
|
||||||
ParamObj->SetStringField(TEXT("group"), VP->Group.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);
|
|
||||||
ParamObj->SetObjectField(TEXT("defaultValue"), DefVal);
|
|
||||||
}
|
|
||||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
||||||
{
|
|
||||||
bIsParam = true;
|
|
||||||
ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
|
||||||
ParamObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
|
||||||
ParamObj->SetStringField(TEXT("group"), TP->Group.ToString());
|
|
||||||
if (TP->Texture)
|
|
||||||
ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName());
|
|
||||||
}
|
|
||||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
||||||
{
|
|
||||||
bIsParam = true;
|
|
||||||
ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
|
||||||
ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
|
||||||
ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString());
|
|
||||||
ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bIsParam)
|
|
||||||
{
|
|
||||||
Parameters.Add(MakeShared<FJsonValueObject>(ParamObj));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("parameters"), Parameters);
|
|
||||||
|
|
||||||
// Referenced textures
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ReferencedTextures;
|
|
||||||
auto RefTexObjs = MaterialObj->GetReferencedTextures();
|
|
||||||
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
|
|
||||||
{
|
|
||||||
if (TexObj)
|
|
||||||
{
|
|
||||||
ReferencedTextures.Add(MakeShared<FJsonValueString>(TexObj->GetPathName()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures);
|
|
||||||
|
|
||||||
// Graph node count
|
|
||||||
int32 GraphNodeCount = 0;
|
|
||||||
if (MaterialObj->MaterialGraph)
|
|
||||||
{
|
|
||||||
GraphNodeCount = MaterialObj->MaterialGraph->Nodes.Num();
|
|
||||||
}
|
|
||||||
Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount);
|
|
||||||
|
|
||||||
// Usage flags
|
|
||||||
TSharedRef<FJsonObject> UsageFlags = MakeShared<FJsonObject>();
|
|
||||||
UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), MaterialObj->bUsedWithSkeletalMesh != 0);
|
|
||||||
UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), MaterialObj->bUsedWithMorphTargets != 0);
|
|
||||||
UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), MaterialObj->bUsedWithNiagaraSprites != 0);
|
|
||||||
UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), MaterialObj->bUsedWithParticleSprites != 0);
|
|
||||||
UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), MaterialObj->bUsedWithStaticLighting != 0);
|
|
||||||
Result->SetObjectField(TEXT("usageFlags"), UsageFlags);
|
|
||||||
|
|
||||||
// Opacity mask clip value
|
|
||||||
Result->SetNumberField(TEXT("opacityMaskClipValue"), MaterialObj->OpacityMaskClipValue);
|
|
||||||
|
|
||||||
// Additional settings
|
|
||||||
Result->SetBoolField(TEXT("ditheredLODTransition"), MaterialObj->DitheredLODTransition != 0);
|
|
||||||
Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), MaterialObj->bAllowNegativeEmissiveColor != 0);
|
|
||||||
|
|
||||||
// Texture sample count (simple expression scan)
|
|
||||||
int32 TextureSampleCount = 0;
|
|
||||||
for (UMaterialExpression* Expr : Expressions)
|
|
||||||
{
|
|
||||||
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
|
|
||||||
{
|
|
||||||
TextureSampleCount++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(LoadedObj))
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName());
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("name"), MI->GetName());
|
|
||||||
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
|
||||||
Result->SetStringField(TEXT("type"), TEXT("MaterialInstance"));
|
|
||||||
|
|
||||||
if (MI->Parent)
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("parent"), MI->Parent->GetName());
|
|
||||||
Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Overridden parameters
|
|
||||||
TArray<TSharedPtr<FJsonValue>> OverriddenParams;
|
|
||||||
|
|
||||||
// Scalar parameters
|
|
||||||
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
|
||||||
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
|
||||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Vector parameters
|
|
||||||
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
|
||||||
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
|
|
||||||
Val->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
|
||||||
Val->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
|
||||||
Val->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
|
||||||
Val->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
|
||||||
PObj->SetObjectField(TEXT("value"), Val);
|
|
||||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Texture parameters
|
|
||||||
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
|
||||||
if (Param.ParameterValue)
|
|
||||||
PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName());
|
|
||||||
else
|
|
||||||
PObj->SetStringField(TEXT("value"), TEXT("None"));
|
|
||||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Static switch parameters
|
|
||||||
for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
|
||||||
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
|
||||||
PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
|
||||||
PObj->SetBoolField(TEXT("value"), Param.Value);
|
|
||||||
PObj->SetBoolField(TEXT("overridden"), Param.bOverride);
|
|
||||||
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DumpMaterialExpressionGraph : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material name or package path"))
|
|
||||||
FString Material;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Get the serialized expression graph for a material, including all nodes and connections.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString DecodedName = MCPUtils::UrlDecode(Material);
|
|
||||||
|
|
||||||
MCPAssets<UMaterial> Assets;
|
|
||||||
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterial* MaterialObj = Assets.Object();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *MaterialObj->GetName());
|
|
||||||
|
|
||||||
// Ensure the material graph is built
|
|
||||||
if (!MaterialObj->MaterialGraph)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild"));
|
|
||||||
// The material graph is built lazily by the material editor; force-create it
|
|
||||||
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
|
|
||||||
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
|
||||||
MaterialObj->MaterialGraph->Material = MaterialObj;
|
|
||||||
MaterialObj->MaterialGraph->RebuildGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MaterialObj->MaterialGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(MaterialObj->MaterialGraph);
|
|
||||||
if (!GraphJson.IsValid())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
|
||||||
|
|
||||||
// Add material name context
|
|
||||||
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
||||||
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DescribeMaterialInEnglish : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material name or package path"))
|
|
||||||
FString Material;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Generate a human-readable description of a material by tracing its expression graph from the root node inputs.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterial> Assets;
|
|
||||||
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterial* MaterialObj = Assets.Object();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *MaterialObj->GetName());
|
|
||||||
|
|
||||||
// Ensure material graph is built
|
|
||||||
if (!MaterialObj->MaterialGraph)
|
|
||||||
{
|
|
||||||
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
|
|
||||||
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
|
||||||
MaterialObj->MaterialGraph->Material = MaterialObj;
|
|
||||||
MaterialObj->MaterialGraph->RebuildGraph();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MaterialObj->MaterialGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursive helper: trace backwards from a pin and build a description string
|
|
||||||
TFunction<FString(UEdGraphPin*, int32)> TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString
|
|
||||||
{
|
|
||||||
if (!Pin || Depth > 10)
|
|
||||||
return TEXT("(unknown)");
|
|
||||||
|
|
||||||
// If no connections, report as unconnected
|
|
||||||
if (Pin->LinkedTo.Num() == 0)
|
|
||||||
{
|
|
||||||
if (!Pin->DefaultValue.IsEmpty())
|
|
||||||
return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue);
|
|
||||||
return TEXT("(unconnected)");
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<FString> Sources;
|
|
||||||
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
|
||||||
{
|
|
||||||
if (!LinkedPin || !LinkedPin->GetOwningNode()) continue;
|
|
||||||
|
|
||||||
UEdGraphNode* SourceNode = LinkedPin->GetOwningNode();
|
|
||||||
FString NodeDesc;
|
|
||||||
|
|
||||||
// Check if this is a material graph node
|
|
||||||
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode))
|
|
||||||
{
|
|
||||||
UMaterialExpression* Expr = MatNode->MaterialExpression;
|
|
||||||
if (!Expr)
|
|
||||||
{
|
|
||||||
NodeDesc = TEXT("(null expression)");
|
|
||||||
}
|
|
||||||
else if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
||||||
{
|
|
||||||
NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue);
|
|
||||||
}
|
|
||||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
||||||
{
|
|
||||||
NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"),
|
|
||||||
*VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
|
|
||||||
}
|
|
||||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
||||||
{
|
|
||||||
FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None");
|
|
||||||
NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName);
|
|
||||||
}
|
|
||||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
||||||
{
|
|
||||||
NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"),
|
|
||||||
*SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
|
|
||||||
}
|
|
||||||
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
|
|
||||||
{
|
|
||||||
NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R);
|
|
||||||
}
|
|
||||||
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
|
|
||||||
{
|
|
||||||
NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
|
|
||||||
}
|
|
||||||
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
|
|
||||||
{
|
|
||||||
NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
|
|
||||||
}
|
|
||||||
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
|
|
||||||
{
|
|
||||||
FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None");
|
|
||||||
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
|
|
||||||
}
|
|
||||||
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
|
|
||||||
{
|
|
||||||
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None");
|
|
||||||
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
NodeDesc = Expr->GetClass()->GetName();
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the source node has input pins with connections, recurse
|
|
||||||
TArray<FString> InputDescs;
|
|
||||||
for (UEdGraphPin* InputPin : SourceNode->Pins)
|
|
||||||
{
|
|
||||||
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
|
|
||||||
FString InputDesc = TracePin(InputPin, Depth + 1);
|
|
||||||
InputDescs.Add(InputDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (InputDescs.Num() > 0)
|
|
||||||
{
|
|
||||||
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Non-material node (e.g., root, comment), just use title
|
|
||||||
NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
Sources.Add(NodeDesc);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Sources.Num() == 1)
|
|
||||||
return Sources[0];
|
|
||||||
|
|
||||||
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
|
|
||||||
};
|
|
||||||
|
|
||||||
// Find root node and trace each input
|
|
||||||
TArray<TSharedPtr<FJsonValue>> InputDescriptions;
|
|
||||||
|
|
||||||
UMaterialGraphNode_Root* RootNode = nullptr;
|
|
||||||
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
|
||||||
{
|
|
||||||
RootNode = Cast<UMaterialGraphNode_Root>(Node);
|
|
||||||
if (RootNode) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!RootNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (UEdGraphPin* Pin : RootNode->Pins)
|
|
||||||
{
|
|
||||||
if (!Pin || Pin->Direction != EGPD_Input) continue;
|
|
||||||
|
|
||||||
FString PinName = Pin->PinName.ToString();
|
|
||||||
FString Description;
|
|
||||||
|
|
||||||
if (Pin->LinkedTo.Num() == 0)
|
|
||||||
{
|
|
||||||
Description = TEXT("(unconnected)");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Description = TracePin(Pin, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
|
||||||
InputObj->SetStringField(TEXT("input"), PinName);
|
|
||||||
InputObj->SetStringField(TEXT("chain"), Description);
|
|
||||||
InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0);
|
|
||||||
InputDescriptions.Add(MakeShared<FJsonValueObject>(InputObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
||||||
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
|
||||||
Result->SetArrayField(TEXT("inputs"), InputDescriptions);
|
|
||||||
|
|
||||||
// Also include a compact text description
|
|
||||||
FString TextDesc;
|
|
||||||
for (const TSharedPtr<FJsonValue>& Val : InputDescriptions)
|
|
||||||
{
|
|
||||||
TSharedPtr<FJsonObject> Obj = Val->AsObject();
|
|
||||||
if (!Obj.IsValid()) continue;
|
|
||||||
FString InputName = Obj->GetStringField(TEXT("input"));
|
|
||||||
FString Chain = Obj->GetStringField(TEXT("chain"));
|
|
||||||
bool bConnected = Obj->GetBoolField(TEXT("connected"));
|
|
||||||
if (bConnected)
|
|
||||||
{
|
|
||||||
TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!TextDesc.IsEmpty())
|
|
||||||
{
|
|
||||||
Result->SetStringField(TEXT("description"), TextDesc);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SearchWithinMaterials : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Search query string to match against material names, expression classes, and parameter names"))
|
|
||||||
FString Query;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
|
|
||||||
int32 MaxResults = 50;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Search across all materials for matching material names, expression types, and parameter names.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString DecodedQuery = MCPUtils::UrlDecode(Query);
|
|
||||||
|
|
||||||
MaxResults = FMath::Clamp(MaxResults, 1, 200);
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Results;
|
|
||||||
|
|
||||||
MCPAssets<UMaterial> AllMaterials;
|
|
||||||
AllMaterials.Info();
|
|
||||||
|
|
||||||
for (const FAssetData& Asset : AllMaterials.AllData())
|
|
||||||
{
|
|
||||||
if (Results.Num() >= MaxResults) break;
|
|
||||||
|
|
||||||
FString MatName = Asset.AssetName.ToString();
|
|
||||||
|
|
||||||
// Check material name first
|
|
||||||
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
|
|
||||||
|
|
||||||
UMaterial* MaterialObj = Cast<UMaterial>(const_cast<FAssetData&>(Asset).GetAsset());
|
|
||||||
if (!MaterialObj) continue;
|
|
||||||
|
|
||||||
auto Expressions = MaterialObj->GetExpressions();
|
|
||||||
|
|
||||||
if (bNameMatch)
|
|
||||||
{
|
|
||||||
// Add a match for the material itself
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("material"), MatName);
|
|
||||||
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
|
||||||
R->SetStringField(TEXT("matchType"), TEXT("materialName"));
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search expressions
|
|
||||||
for (UMaterialExpression* Expr : Expressions)
|
|
||||||
{
|
|
||||||
if (!Expr || Results.Num() >= MaxResults) continue;
|
|
||||||
|
|
||||||
FString ExprDesc = Expr->GetDescription();
|
|
||||||
FString ExprClass = Expr->GetClass()->GetName();
|
|
||||||
|
|
||||||
// Check parameter name
|
|
||||||
FString ParamName;
|
|
||||||
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
|
||||||
ParamName = SP->ParameterName.ToString();
|
|
||||||
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
|
||||||
ParamName = VP->ParameterName.ToString();
|
|
||||||
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
|
||||||
ParamName = TP->ParameterName.ToString();
|
|
||||||
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
|
||||||
ParamName = SSP->ParameterName.ToString();
|
|
||||||
|
|
||||||
bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
|
||||||
ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
|
||||||
(!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase));
|
|
||||||
|
|
||||||
if (bExprMatch)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("material"), MatName);
|
|
||||||
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
|
||||||
R->SetStringField(TEXT("matchType"), TEXT("expression"));
|
|
||||||
R->SetStringField(TEXT("expressionClass"), ExprClass);
|
|
||||||
if (!ExprDesc.IsEmpty())
|
|
||||||
R->SetStringField(TEXT("description"), ExprDesc);
|
|
||||||
if (!ParamName.IsEmpty())
|
|
||||||
R->SetStringField(TEXT("parameterName"), ParamName);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("query"), DecodedQuery);
|
|
||||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
|
||||||
Result->SetArrayField(TEXT("results"), Results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_FindMaterialReferences : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
|
|
||||||
FString Material;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Find all assets that reference a given material or material instance.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Try to find the material's package path (search both Material and MaterialInstance)
|
|
||||||
MCPAssets<UMaterial> Assets;
|
|
||||||
Assets.Scan<UMaterial>().Scan<UMaterialInstanceConstant>();
|
|
||||||
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Info()) return;
|
|
||||||
FString PackagePath = Assets.OneData().PackageName.ToString();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *Material, *PackagePath);
|
|
||||||
|
|
||||||
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
|
||||||
|
|
||||||
TArray<FName> Referencers;
|
|
||||||
Registry.GetReferencers(FName(*PackagePath), Referencers);
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> RefArray;
|
|
||||||
for (const FName& Ref : Referencers)
|
|
||||||
{
|
|
||||||
FString RefStr = Ref.ToString();
|
|
||||||
// Skip self-reference
|
|
||||||
if (RefStr == PackagePath) continue;
|
|
||||||
RefArray.Add(MakeShared<FJsonValueString>(RefStr));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("packagePath"), PackagePath);
|
|
||||||
Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num());
|
|
||||||
Result->SetArrayField(TEXT("referencers"), RefArray);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListMaterialFunctionAssets : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Optional, Description="Filter string to match against function name or path"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List MaterialFunction assets, optionally filtered by name or path.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UMaterialFunction> Assets;
|
|
||||||
Assets.Substring(Filter).Info();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Entries;
|
|
||||||
for (const FAssetData& Asset : Assets.AllData())
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
|
||||||
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
|
|
||||||
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
|
|
||||||
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("count"), Entries.Num());
|
|
||||||
Result->SetArrayField(TEXT("functions"), Entries);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DumpMaterialFunction : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="MaterialFunction name or package path"))
|
|
||||||
FString MaterialFunction;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Get detailed info about a material function, including its inputs, outputs, and expressions.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString DecodedName = MCPUtils::UrlDecode(MaterialFunction);
|
|
||||||
|
|
||||||
MCPAssets<UMaterialFunction> Assets;
|
|
||||||
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterialFunction* MF = Assets.Object();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("name"), MF->GetName());
|
|
||||||
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
|
||||||
Result->SetStringField(TEXT("description"), MF->GetDescription());
|
|
||||||
|
|
||||||
// Expression count
|
|
||||||
auto Expressions = MF->GetExpressions();
|
|
||||||
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
|
||||||
|
|
||||||
// List function inputs and outputs from expressions
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Inputs;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Outputs;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ExpressionList;
|
|
||||||
|
|
||||||
{
|
|
||||||
for (UMaterialExpression* Expr : Expressions)
|
|
||||||
{
|
|
||||||
if (!Expr) continue;
|
|
||||||
|
|
||||||
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
|
||||||
InputObj->SetStringField(TEXT("name"), FI->InputName.ToString());
|
|
||||||
InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput"));
|
|
||||||
InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX);
|
|
||||||
InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY);
|
|
||||||
Inputs.Add(MakeShared<FJsonValueObject>(InputObj));
|
|
||||||
}
|
|
||||||
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> OutputObj = MakeShared<FJsonObject>();
|
|
||||||
OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString());
|
|
||||||
OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput"));
|
|
||||||
OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX);
|
|
||||||
OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY);
|
|
||||||
Outputs.Add(MakeShared<FJsonValueObject>(OutputObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Serialize every expression
|
|
||||||
TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
|
|
||||||
if (ExprJson.IsValid())
|
|
||||||
{
|
|
||||||
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetArrayField(TEXT("inputs"), Inputs);
|
|
||||||
Result->SetArrayField(TEXT("outputs"), Outputs);
|
|
||||||
Result->SetArrayField(TEXT("expressions"), ExpressionList);
|
|
||||||
|
|
||||||
// If the function has an editor graph, serialize it
|
|
||||||
UEdGraph* FuncGraph = MF->MaterialGraph;
|
|
||||||
if (FuncGraph)
|
|
||||||
{
|
|
||||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
|
|
||||||
if (GraphJson.IsValid())
|
|
||||||
{
|
|
||||||
Result->SetObjectField(TEXT("graph"), GraphJson);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CompileMaterial : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Material name or package path"))
|
|
||||||
FString Material;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Force recompile a material and check for compilation errors.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Load material
|
|
||||||
MCPAssets<UMaterial> Assets;
|
|
||||||
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UMaterial* MaterialObj = Assets.Object();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *MaterialObj->GetName());
|
|
||||||
|
|
||||||
// Force recompile by triggering PreEditChange/PostEditChange
|
|
||||||
MaterialObj->PreEditChange(nullptr);
|
|
||||||
MaterialObj->PostEditChange();
|
|
||||||
|
|
||||||
// Collect compilation errors
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ErrorArray;
|
|
||||||
bool bValid = true;
|
|
||||||
|
|
||||||
// Check for compilation errors via FMaterialResource on current platform
|
|
||||||
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
|
|
||||||
if (Resource)
|
|
||||||
{
|
|
||||||
const TArray<FString>& CompileErrors = Resource->GetCompileErrors();
|
|
||||||
for (const FString& Err : CompileErrors)
|
|
||||||
{
|
|
||||||
bValid = false;
|
|
||||||
ErrorArray.Add(MakeShared<FJsonValueString>(Err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count expressions and connections
|
|
||||||
auto Expressions = MaterialObj->GetExpressions();
|
|
||||||
int32 ExprCount = Expressions.Num();
|
|
||||||
int32 ConnectionCount = 0;
|
|
||||||
if (MaterialObj->MaterialGraph)
|
|
||||||
{
|
|
||||||
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
|
||||||
{
|
|
||||||
if (!Node) continue;
|
|
||||||
for (UEdGraphPin* Pin : Node->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && Pin->Direction == EGPD_Output)
|
|
||||||
{
|
|
||||||
ConnectionCount += Pin->LinkedTo.Num();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("valid"), bValid);
|
|
||||||
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
|
||||||
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
|
||||||
Result->SetNumberField(TEXT("expressionCount"), ExprCount);
|
|
||||||
Result->SetNumberField(TEXT("connectionCount"), ConnectionCount);
|
|
||||||
Result->SetArrayField(TEXT("errors"), ErrorArray);
|
|
||||||
Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num());
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"),
|
|
||||||
*MaterialObj->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,490 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphPin.h"
|
|
||||||
#include "K2Node_FunctionEntry.h"
|
|
||||||
#include "K2Node_CustomEvent.h"
|
|
||||||
#include "K2Node_EditablePinBase.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "MCPHandlers_Params.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ChangeFunctionParameterType : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
|
||||||
FString FunctionName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the parameter to change"))
|
|
||||||
FString ParamName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="New type for the parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
|
||||||
FString NewType;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, analyze impact without making changes"))
|
|
||||||
bool DryRun = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Change the type of an existing parameter on a function or custom event in a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
|
|
||||||
FEdGraphPinType NewPinType;
|
|
||||||
if (!MCPUtils::ResolveTypeFromString(NewType, NewPinType, Result))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Find the entry node: K2Node_FunctionEntry in a function graph,
|
|
||||||
// or K2Node_CustomEvent in any graph
|
|
||||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
|
||||||
FString FoundNodeType;
|
|
||||||
|
|
||||||
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
|
|
||||||
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
||||||
{
|
|
||||||
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
EntryNode = FuncEntry;
|
|
||||||
FoundNodeType = TEXT("FunctionEntry");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
EntryNode = CustomEvent;
|
|
||||||
FoundNodeType = TEXT("CustomEvent");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
// List available functions/events for debugging
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Available;
|
|
||||||
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
||||||
{
|
|
||||||
Available.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
|
|
||||||
}
|
|
||||||
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
Available.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
|
||||||
*FunctionName, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the UserDefinedPin matching paramName
|
|
||||||
bool bPinFound = false;
|
|
||||||
for (TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
EntryNode->PreEditChange(nullptr);
|
|
||||||
PinInfo->PinType = NewPinType;
|
|
||||||
EntryNode->PostEditChange();
|
|
||||||
bPinFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bPinFound)
|
|
||||||
{
|
|
||||||
// List available params for debugging
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
|
||||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (PinInfo.IsValid())
|
|
||||||
{
|
|
||||||
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Parameter '%s' not found in %s '%s'"),
|
|
||||||
*ParamName, *FoundNodeType, *FunctionName));
|
|
||||||
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for dry run
|
|
||||||
if (DryRun)
|
|
||||||
{
|
|
||||||
// Analyze what would change: report connected pins that may disconnect
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AffectedPins;
|
|
||||||
for (UEdGraphPin* Pin : EntryNode->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0)
|
|
||||||
{
|
|
||||||
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
|
||||||
{
|
|
||||||
if (Linked && Linked->GetOwningNode())
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> AffPin = MakeShared<FJsonObject>();
|
|
||||||
AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
|
||||||
AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
|
||||||
AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
|
||||||
AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString());
|
|
||||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
|
||||||
AffectedPins.Add(MakeShared<FJsonValueObject>(AffPin));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("dryRun"), true);
|
|
||||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
|
||||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
|
||||||
Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num());
|
|
||||||
Result->SetArrayField(TEXT("affectedPins"), AffectedPins);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"),
|
|
||||||
*ParamName, *FoundNodeType, *FunctionName, *Blueprint, *NewType);
|
|
||||||
|
|
||||||
// Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat)
|
|
||||||
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
|
||||||
{
|
|
||||||
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
|
||||||
{
|
|
||||||
Schema->ReconstructNode(*EntryNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
|
|
||||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
|
||||||
|
|
||||||
// Serialize the updated entry node state
|
|
||||||
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
|
||||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
if (UpdatedNodeState.IsValid())
|
|
||||||
{
|
|
||||||
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RemoveFunctionParameter : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
|
||||||
FString FunctionName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the parameter to remove"))
|
|
||||||
FString ParamName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Remove a parameter from a function or custom event in a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Find the entry node
|
|
||||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
|
||||||
FString FoundNodeType;
|
|
||||||
|
|
||||||
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
|
|
||||||
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
||||||
{
|
|
||||||
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
EntryNode = FuncEntry;
|
|
||||||
FoundNodeType = TEXT("FunctionEntry");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
EntryNode = CustomEvent;
|
|
||||||
FoundNodeType = TEXT("CustomEvent");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
// List available functions/events for debugging
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Available;
|
|
||||||
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
||||||
{
|
|
||||||
Available.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
|
|
||||||
}
|
|
||||||
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
Available.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
|
||||||
*FunctionName, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find and remove the UserDefinedPin matching paramName
|
|
||||||
int32 RemovedIndex = INDEX_NONE;
|
|
||||||
for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i)
|
|
||||||
{
|
|
||||||
if (EntryNode->UserDefinedPins[i].IsValid() &&
|
|
||||||
EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
RemovedIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (RemovedIndex == INDEX_NONE)
|
|
||||||
{
|
|
||||||
// List available params for debugging
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
|
||||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (PinInfo.IsValid())
|
|
||||||
{
|
|
||||||
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Parameter '%s' not found in %s '%s'"),
|
|
||||||
*ParamName, *FoundNodeType, *FunctionName));
|
|
||||||
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"),
|
|
||||||
*ParamName, *FoundNodeType, *FunctionName, *Blueprint);
|
|
||||||
|
|
||||||
// Remove the pin
|
|
||||||
EntryNode->UserDefinedPins.RemoveAt(RemovedIndex);
|
|
||||||
|
|
||||||
// Reconstruct the node to update output pins (use schema for MinimalAPI compat)
|
|
||||||
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
|
||||||
{
|
|
||||||
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
|
||||||
{
|
|
||||||
Schema->ReconstructNode(*EntryNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
|
|
||||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
|
||||||
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddFunctionParameter : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the function, custom event, or event dispatcher"))
|
|
||||||
FString FunctionName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name for the new parameter"))
|
|
||||||
FString ParamName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Type for the new parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
|
||||||
FString ParamType;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a new parameter to a function, custom event, or event dispatcher in a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Resolve param type
|
|
||||||
FEdGraphPinType PinType;
|
|
||||||
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Find the entry node using 3 strategies
|
|
||||||
UK2Node_EditablePinBase* EntryNode = nullptr;
|
|
||||||
FString NodeType;
|
|
||||||
|
|
||||||
FName FuncFName(*FunctionName);
|
|
||||||
|
|
||||||
// Strategy 1: K2Node_FunctionEntry in function graphs
|
|
||||||
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
||||||
{
|
|
||||||
UEdGraph* FEGraph = FE->GetGraph();
|
|
||||||
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
|
|
||||||
// Skip delegate signature graphs (handled in Strategy 3)
|
|
||||||
if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
|
|
||||||
|
|
||||||
EntryNode = FE;
|
|
||||||
NodeType = TEXT("FunctionEntry");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
EntryNode = CE;
|
|
||||||
NodeType = TEXT("CustomEvent");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
|
||||||
{
|
|
||||||
UEdGraph* FEGraph = FE->GetGraph();
|
|
||||||
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
|
|
||||||
if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
|
|
||||||
|
|
||||||
EntryNode = FE;
|
|
||||||
NodeType = TEXT("EventDispatcher");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!EntryNode)
|
|
||||||
{
|
|
||||||
// Build a helpful error listing available functions, events, and dispatchers
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AvailFuncs;
|
|
||||||
|
|
||||||
for (UEdGraph* Graph : BP->FunctionGraphs)
|
|
||||||
{
|
|
||||||
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Custom events
|
|
||||||
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
|
||||||
{
|
|
||||||
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dispatchers
|
|
||||||
TSet<FName> DelegateNames;
|
|
||||||
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames);
|
|
||||||
for (const FName& DN : DelegateNames)
|
|
||||||
{
|
|
||||||
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString())));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
|
|
||||||
*FunctionName, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for duplicate parameter name
|
|
||||||
for (const TSharedPtr<FUserPinInfo>& Existing : EntryNode->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"),
|
|
||||||
*ParamName, *ParamType, *NodeType, *FunctionName, *Blueprint);
|
|
||||||
|
|
||||||
// Add the parameter pin (EGPD_Output on entry = input to callers)
|
|
||||||
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
|
|
||||||
*ParamName, *FunctionName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("nodeType"), NodeType);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,382 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "Engine/World.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "EdGraph/EdGraphPin.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "MCPHandlers_PinMutation.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FSetPinDefaultEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Node;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString PinName;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Value;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetPinDefaultValues : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Array of {blueprint, node, pinName, value} objects"))
|
|
||||||
FMCPJsonArray Pins;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Set the default value of input pins on nodes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Results;
|
|
||||||
int32 SuccessCount = 0;
|
|
||||||
TSet<UEdGraphNode*> ModifiedNodes;
|
|
||||||
TSet<UBlueprint*> ModifiedBlueprints;
|
|
||||||
|
|
||||||
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
||||||
|
|
||||||
FSetPinDefaultEntry Entry;
|
|
||||||
if (!MCPUtils::PopulateFromJson(FSetPinDefaultEntry::StaticStruct(), &Entry, PinVal, &*EntryResult)) continue;
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Scan<UBlueprint>().Scan<UWorld>().Exact(Entry.Blueprint).Errors(&*EntryResult).ENone().ETwo().Load())
|
|
||||||
continue;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
UEdGraph* Graph = nullptr;
|
|
||||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node, &Graph);
|
|
||||||
if (!Node)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
|
||||||
if (!Pin)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Pin->Direction != EGPD_Input)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' is an output pin"), *Entry.PinName));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UEdGraphSchema* Schema = Graph->GetSchema();
|
|
||||||
if (Schema)
|
|
||||||
{
|
|
||||||
FString ValidationError = Schema->IsPinDefaultValid(Pin, Entry.Value, nullptr, FText::GetEmpty());
|
|
||||||
if (!ValidationError.IsEmpty())
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
||||||
TEXT("Invalid value for pin '%s': %s"), *Entry.PinName, *ValidationError));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FString OldValue = Pin->DefaultValue;
|
|
||||||
Pin->DefaultValue = Entry.Value;
|
|
||||||
|
|
||||||
EntryResult->SetStringField(TEXT("oldValue"), OldValue);
|
|
||||||
SuccessCount++;
|
|
||||||
ModifiedNodes.Add(Node);
|
|
||||||
ModifiedBlueprints.Add(BP);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (UEdGraphNode* Node : ModifiedNodes)
|
|
||||||
{
|
|
||||||
Node->ReconstructNode();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (UBlueprint* BP : ModifiedBlueprints)
|
|
||||||
{
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetPinDefault — %d/%d succeeded"),
|
|
||||||
SuccessCount, Pins.Array.Num());
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
||||||
Result->SetNumberField(TEXT("totalCount"), Pins.Array.Num());
|
|
||||||
Result->SetArrayField(TEXT("results"), Results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FConnectPinsEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString SourceNode;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString SourcePinName;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString TargetNode;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString TargetPinName;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ConnectBlueprintPins : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Array of {sourceNode, sourcePinName, targetNode, targetPinName} objects"))
|
|
||||||
FMCPJsonArray Connections;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Connect pins between nodes in a Blueprint graph.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Results;
|
|
||||||
int32 SuccessCount = 0;
|
|
||||||
|
|
||||||
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
||||||
|
|
||||||
FConnectPinsEntry Entry;
|
|
||||||
if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue;
|
|
||||||
|
|
||||||
UEdGraph* SourceGraph = nullptr;
|
|
||||||
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph);
|
|
||||||
if (!SourceNode)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
|
|
||||||
if (!TargetNode)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName));
|
|
||||||
if (!SourcePin)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
|
||||||
if (!TargetPin)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const UEdGraphSchema* Schema = SourceGraph->GetSchema();
|
|
||||||
if (!Schema)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
|
||||||
if (!bConnected)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
|
||||||
TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"),
|
|
||||||
*Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(),
|
|
||||||
*Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString()));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
SuccessCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (SuccessCount > 0)
|
|
||||||
{
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"),
|
|
||||||
SuccessCount, Connections.Array.Num(), *Blueprint);
|
|
||||||
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
||||||
Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num());
|
|
||||||
Result->SetArrayField(TEXT("results"), Results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FDisconnectPinEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Node;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString PinName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional))
|
|
||||||
FString TargetNode;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional))
|
|
||||||
FString TargetPinName;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DisconnectBlueprintPins : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Array of {node, pinName, targetNode?, targetPinName?} objects. If target is omitted, all connections on the pin are broken."))
|
|
||||||
FMCPJsonArray Disconnections;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Disconnect pins in a Blueprint graph. "
|
|
||||||
"Can disconnect a specific link or all links on a pin.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Results;
|
|
||||||
int32 SuccessCount = 0;
|
|
||||||
int32 TotalDisconnected = 0;
|
|
||||||
|
|
||||||
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
|
||||||
|
|
||||||
FDisconnectPinEntry Entry;
|
|
||||||
if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue;
|
|
||||||
|
|
||||||
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
|
|
||||||
if (!Node)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
|
||||||
if (!Pin)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 DisconnectedCount = 0;
|
|
||||||
|
|
||||||
if (!Entry.TargetNode.IsEmpty() && !Entry.TargetPinName.IsEmpty())
|
|
||||||
{
|
|
||||||
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
|
|
||||||
if (!TargetNode)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
|
||||||
if (!TargetPin)
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Pin->LinkedTo.Contains(TargetPin))
|
|
||||||
{
|
|
||||||
EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other"));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
Pin->BreakLinkTo(TargetPin);
|
|
||||||
DisconnectedCount = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
DisconnectedCount = Pin->LinkedTo.Num();
|
|
||||||
if (DisconnectedCount > 0)
|
|
||||||
{
|
|
||||||
Pin->BreakAllPinLinks(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
EntryResult->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount);
|
|
||||||
SuccessCount++;
|
|
||||||
TotalDisconnected += DisconnectedCount;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TotalDisconnected > 0)
|
|
||||||
{
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DisconnectPin — %d/%d succeeded, %d links broken in '%s'"),
|
|
||||||
SuccessCount, Disconnections.Array.Num(), TotalDisconnected, *Blueprint);
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
|
||||||
Result->SetNumberField(TEXT("totalCount"), Disconnections.Array.Num());
|
|
||||||
Result->SetNumberField(TEXT("totalDisconnected"), TotalDisconnected);
|
|
||||||
Result->SetArrayField(TEXT("results"), Results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,669 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "Engine/World.h"
|
|
||||||
#include "Engine/Level.h"
|
|
||||||
#include "Engine/LevelScriptBlueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "K2Node_CallFunction.h"
|
|
||||||
#include "K2Node_Event.h"
|
|
||||||
#include "K2Node_CustomEvent.h"
|
|
||||||
#include "K2Node_VariableGet.h"
|
|
||||||
#include "K2Node_VariableSet.h"
|
|
||||||
#include "K2Node_BreakStruct.h"
|
|
||||||
#include "K2Node_MakeStruct.h"
|
|
||||||
#include "K2Node_FunctionEntry.h"
|
|
||||||
#include "K2Node_EditablePinBase.h"
|
|
||||||
#include "AssetRegistry/IAssetRegistry.h"
|
|
||||||
#include "MCPHandlers_Read.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Filter by parent class name (substring match)"))
|
|
||||||
FString ParentClass;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
|
|
||||||
bool IncludeRegular = true;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
|
|
||||||
bool IncludeLevel = true;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all Blueprint assets in the project, with optional filtering by name, parent class, or type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UObject> Assets;
|
|
||||||
Assets.NoScans().Substring(Filter).Limit(500);
|
|
||||||
if (IncludeRegular) Assets.Scan<UBlueprint>();
|
|
||||||
if (IncludeLevel) Assets.Scan<UWorld>();
|
|
||||||
Assets.Info();
|
|
||||||
for (const FAssetData& Asset : Assets.AllData())
|
|
||||||
{
|
|
||||||
// Extract parent class name
|
|
||||||
FString ParentClassName;
|
|
||||||
if (Asset.AssetClassPath == UWorld::StaticClass()->GetClassPathName())
|
|
||||||
{
|
|
||||||
ParentClassName = TEXT("LevelScriptActor");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName);
|
|
||||||
int32 DotIndex;
|
|
||||||
if (ParentClassName.FindLastChar('.', DotIndex))
|
|
||||||
{
|
|
||||||
ParentClassName = ParentClassName.Mid(DotIndex + 1);
|
|
||||||
}
|
|
||||||
ParentClassName.RemoveFromEnd(TEXT("'"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply parent class filter
|
|
||||||
if (!ParentClass.IsEmpty())
|
|
||||||
{
|
|
||||||
if (!ParentClassName.Equals(ParentClass, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result.Appendf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DumpBlueprint : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Load and serialize a Blueprint, returning its full structure including graphs, variables, and components.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(Assets.Object());
|
|
||||||
MCPUtils::CopyJsonFields(&*Tmp, Result);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_DumpBlueprintGraph : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Graph name to dump"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Dump the detailed node/pin structure of a specific graph within a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// URL-decode graph name to handle spaces encoded as '+' or '%20'
|
|
||||||
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
TArray<UEdGraph*> AllGraphs = MCPUtils::AllGraphs(BP);
|
|
||||||
|
|
||||||
for (UEdGraph* GraphObj : AllGraphs)
|
|
||||||
{
|
|
||||||
if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(GraphObj);
|
|
||||||
if (GraphJson.IsValid())
|
|
||||||
{
|
|
||||||
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Not found — list available graphs
|
|
||||||
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
|
||||||
for (UEdGraph* GraphObj : AllGraphs)
|
|
||||||
{
|
|
||||||
GraphNames.Add(MakeShared<FJsonValueString>(GraphObj->GetName()));
|
|
||||||
}
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
|
||||||
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SearchWithinBlueprints : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Search query string to match against node titles, function names, event names, and variable names"))
|
|
||||||
FString Query;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Filter results to blueprints whose path contains this substring"))
|
|
||||||
FString Path;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
|
|
||||||
int32 MaxResults = 0;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Search across all Blueprint graphs for nodes matching a query string.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50;
|
|
||||||
|
|
||||||
// Build a combined list of all searchable blueprints (regular + level)
|
|
||||||
auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray<TSharedPtr<FJsonValue>>& OutResults)
|
|
||||||
{
|
|
||||||
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
|
|
||||||
{
|
|
||||||
if (OutResults.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
|
||||||
|
|
||||||
FString FuncName, EventName, VarName;
|
|
||||||
if (auto* CF = Cast<UK2Node_CallFunction>(Node))
|
|
||||||
{
|
|
||||||
FuncName = CF->FunctionReference.GetMemberName().ToString();
|
|
||||||
}
|
|
||||||
else if (auto* Ev = Cast<UK2Node_Event>(Node))
|
|
||||||
{
|
|
||||||
EventName = Ev->EventReference.GetMemberName().ToString();
|
|
||||||
}
|
|
||||||
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
|
|
||||||
{
|
|
||||||
EventName = CE->CustomFunctionName.ToString();
|
|
||||||
}
|
|
||||||
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
|
||||||
{
|
|
||||||
VarName = VG->GetVarName().ToString();
|
|
||||||
}
|
|
||||||
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
|
||||||
{
|
|
||||||
VarName = VS->GetVarName().ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) ||
|
|
||||||
(!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
|
||||||
(!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
|
||||||
(!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase));
|
|
||||||
|
|
||||||
if (bMatch)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), AssetName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), AssetPath);
|
|
||||||
R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
|
|
||||||
R->SetStringField(TEXT("nodeTitle"), Title);
|
|
||||||
R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
|
|
||||||
if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName);
|
|
||||||
if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName);
|
|
||||||
if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName);
|
|
||||||
OutResults.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> AllBlueprints;
|
|
||||||
AllBlueprints.Info();
|
|
||||||
MCPAssets<UWorld> AllWorlds;
|
|
||||||
AllWorlds.Info();
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Results;
|
|
||||||
for (const FAssetData& Asset : AllBlueprints.AllData())
|
|
||||||
{
|
|
||||||
if (Results.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
FString AssetPath = Asset.PackageName.ToString();
|
|
||||||
if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
|
||||||
if (!BP) continue;
|
|
||||||
|
|
||||||
SearchBlueprint(Asset.AssetName.ToString(), AssetPath, BP, Results);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also search level blueprints
|
|
||||||
for (const FAssetData& MapAsset : AllWorlds.AllData())
|
|
||||||
{
|
|
||||||
if (Results.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
FString AssetPath = MapAsset.PackageName.ToString();
|
|
||||||
if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
|
||||||
if (!World || !World->PersistentLevel) continue;
|
|
||||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
|
||||||
if (!LevelBP) continue;
|
|
||||||
|
|
||||||
int32 BeforeCount = Results.Num();
|
|
||||||
SearchBlueprint(MapAsset.AssetName.ToString(), AssetPath, LevelBP, Results);
|
|
||||||
// Tag newly-added entries as level blueprint results
|
|
||||||
for (int32 i = BeforeCount; i < Results.Num(); ++i)
|
|
||||||
{
|
|
||||||
Results[i]->AsObject()->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
|
||||||
Result->SetArrayField(TEXT("results"), Results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleTestSave — load a Blueprint and save it unmodified (diagnostic)
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_TestSaveBlueprintPackage : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Load a Blueprint and save it unmodified as a diagnostic test.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save requested for '%s'"), *Blueprint);
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: test-save — loaded '%s', GeneratedClass=%s"),
|
|
||||||
*BP->GetName(),
|
|
||||||
BP->GeneratedClass ? *BP->GeneratedClass->GetName() : TEXT("null"));
|
|
||||||
|
|
||||||
// Attempt save with NO modifications
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("packagePath"), BP->GetPackage()->GetName());
|
|
||||||
Result->SetBoolField(TEXT("hasGeneratedClass"), BP->GeneratedClass != nullptr);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleFindReferences — find all Blueprints referencing an asset
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_FindAssetReferences : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Asset package path to find references for"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Find all assets that reference a given asset, categorized into Blueprint and non-Blueprint referencers.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
|
||||||
|
|
||||||
TArray<FName> Referencers;
|
|
||||||
Registry.GetReferencers(FName(*AssetPath), Referencers);
|
|
||||||
|
|
||||||
// Build set of known Blueprint package names for filtering
|
|
||||||
MCPAssets<UBlueprint> AllBlueprints;
|
|
||||||
AllBlueprints.Info();
|
|
||||||
TSet<FString> BlueprintPackages;
|
|
||||||
for (const FAssetData& Asset : AllBlueprints.AllData())
|
|
||||||
{
|
|
||||||
BlueprintPackages.Add(Asset.PackageName.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> BPRefs;
|
|
||||||
TArray<TSharedPtr<FJsonValue>> OtherRefs;
|
|
||||||
for (const FName& Ref : Referencers)
|
|
||||||
{
|
|
||||||
FString RefStr = Ref.ToString();
|
|
||||||
if (BlueprintPackages.Contains(RefStr))
|
|
||||||
{
|
|
||||||
BPRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
OtherRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("totalReferencers"), Referencers.Num());
|
|
||||||
Result->SetNumberField(TEXT("blueprintReferencerCount"), BPRefs.Num());
|
|
||||||
Result->SetArrayField(TEXT("blueprintReferencers"), BPRefs);
|
|
||||||
Result->SetNumberField(TEXT("otherReferencerCount"), OtherRefs.Num());
|
|
||||||
Result->SetArrayField(TEXT("otherReferencers"), OtherRefs);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// HandleSearchByType — find all usages of a type across blueprints
|
|
||||||
// ============================================================
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SearchTypeUsageInBlueprints : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Type name to search for (e.g. 'FVector', 'MyStruct'). F/E/U prefix is stripped for matching."))
|
|
||||||
FString TypeName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring"))
|
|
||||||
FString Filter;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)"))
|
|
||||||
int32 MaxResults = 0;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Search all Blueprints for usages of a specific type in variables, function parameters, struct nodes, and pin connections.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
|
|
||||||
FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter);
|
|
||||||
|
|
||||||
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200;
|
|
||||||
|
|
||||||
// Strip F/E/U prefix for comparison
|
|
||||||
FString TypeNameNoPrefix = DecodedTypeName;
|
|
||||||
if (TypeNameNoPrefix.StartsWith(TEXT("F")) || TypeNameNoPrefix.StartsWith(TEXT("E")) || TypeNameNoPrefix.StartsWith(TEXT("U")))
|
|
||||||
{
|
|
||||||
TypeNameNoPrefix = TypeNameNoPrefix.Mid(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
auto MatchesType = [&DecodedTypeName, &TypeNameNoPrefix](const FString& TestType) -> bool
|
|
||||||
{
|
|
||||||
return TestType.Equals(DecodedTypeName, ESearchCase::IgnoreCase) ||
|
|
||||||
TestType.Equals(TypeNameNoPrefix, ESearchCase::IgnoreCase);
|
|
||||||
};
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Results;
|
|
||||||
|
|
||||||
// Lambda that searches a single Blueprint for type usages
|
|
||||||
auto SearchOneBlueprint = [&](const FString& BPName, const FString& BPPath, UBlueprint* BP, bool bIsLevel)
|
|
||||||
{
|
|
||||||
// Check variables
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Results.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
FString VarSubtype;
|
|
||||||
if (Var.VarType.PinSubCategoryObject.IsValid())
|
|
||||||
{
|
|
||||||
VarSubtype = Var.VarType.PinSubCategoryObject->GetName();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (MatchesType(VarSubtype) || MatchesType(Var.VarType.PinCategory.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), BPName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
|
||||||
R->SetStringField(TEXT("usage"), TEXT("variable"));
|
|
||||||
R->SetStringField(TEXT("location"), Var.VarName.ToString());
|
|
||||||
R->SetStringField(TEXT("currentType"), Var.VarType.PinCategory.ToString());
|
|
||||||
if (!VarSubtype.IsEmpty())
|
|
||||||
R->SetStringField(TEXT("currentSubtype"), VarSubtype);
|
|
||||||
if (bIsLevel)
|
|
||||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check graphs for function/event params, struct nodes, and pin connections
|
|
||||||
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
|
|
||||||
{
|
|
||||||
if (Results.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
// Check FunctionEntry/CustomEvent parameters
|
|
||||||
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
|
||||||
{
|
|
||||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (!PinInfo.IsValid()) continue;
|
|
||||||
FString ParamSubtype;
|
|
||||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
|
||||||
|
|
||||||
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), BPName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
|
||||||
R->SetStringField(TEXT("usage"), TEXT("functionParameter"));
|
|
||||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
|
||||||
*Node->GetGraph()->GetName(), *PinInfo->PinName.ToString()));
|
|
||||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
|
||||||
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
|
||||||
if (!ParamSubtype.IsEmpty())
|
|
||||||
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
|
||||||
if (bIsLevel)
|
|
||||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
|
||||||
{
|
|
||||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (!PinInfo.IsValid()) continue;
|
|
||||||
FString ParamSubtype;
|
|
||||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
|
||||||
|
|
||||||
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), BPName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
|
||||||
R->SetStringField(TEXT("usage"), TEXT("eventParameter"));
|
|
||||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
|
||||||
*CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString()));
|
|
||||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
|
||||||
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
|
||||||
if (!ParamSubtype.IsEmpty())
|
|
||||||
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
|
||||||
if (bIsLevel)
|
|
||||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check Break/Make struct nodes
|
|
||||||
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
|
|
||||||
{
|
|
||||||
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), BPName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
|
||||||
R->SetStringField(TEXT("usage"), TEXT("breakStruct"));
|
|
||||||
R->SetStringField(TEXT("location"), Node->GetGraph()->GetName());
|
|
||||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
|
||||||
R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName());
|
|
||||||
if (bIsLevel)
|
|
||||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
|
|
||||||
{
|
|
||||||
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), BPName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
|
||||||
R->SetStringField(TEXT("usage"), TEXT("makeStruct"));
|
|
||||||
R->SetStringField(TEXT("location"), Node->GetGraph()->GetName());
|
|
||||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
|
||||||
R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName());
|
|
||||||
if (bIsLevel)
|
|
||||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check pin connections carrying the type
|
|
||||||
for (UEdGraphPin* Pin : Node->Pins)
|
|
||||||
{
|
|
||||||
if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue;
|
|
||||||
|
|
||||||
FString PinSubtype;
|
|
||||||
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
PinSubtype = Pin->PinType.PinSubCategoryObject->GetName();
|
|
||||||
|
|
||||||
if ((Pin->LinkedTo.Num() > 0) &&
|
|
||||||
(MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString())))
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
|
||||||
R->SetStringField(TEXT("blueprint"), BPName);
|
|
||||||
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
|
||||||
R->SetStringField(TEXT("usage"), TEXT("pinConnection"));
|
|
||||||
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
|
||||||
*Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(),
|
|
||||||
*Pin->PinName.ToString()));
|
|
||||||
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
|
||||||
R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
|
|
||||||
R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString());
|
|
||||||
if (!PinSubtype.IsEmpty())
|
|
||||||
R->SetStringField(TEXT("pinSubtype"), PinSubtype);
|
|
||||||
R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num());
|
|
||||||
if (bIsLevel)
|
|
||||||
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
|
||||||
Results.Add(MakeShared<FJsonValueObject>(R));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
MCPAssets<UBlueprint> AllBlueprints;
|
|
||||||
AllBlueprints.Info();
|
|
||||||
MCPAssets<UWorld> AllWorlds;
|
|
||||||
AllWorlds.Info();
|
|
||||||
|
|
||||||
// Search regular blueprints
|
|
||||||
for (const FAssetData& Asset : AllBlueprints.AllData())
|
|
||||||
{
|
|
||||||
if (Results.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
FString AssetPath = Asset.PackageName.ToString();
|
|
||||||
FString BPName = Asset.AssetName.ToString();
|
|
||||||
|
|
||||||
if (!FilterStr.IsEmpty() && !BPName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
|
||||||
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
|
||||||
if (!BP) continue;
|
|
||||||
|
|
||||||
SearchOneBlueprint(BPName, AssetPath, BP, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search level blueprints from maps
|
|
||||||
for (const FAssetData& MapAsset : AllWorlds.AllData())
|
|
||||||
{
|
|
||||||
if (Results.Num() >= EffectiveMaxResults) break;
|
|
||||||
|
|
||||||
FString AssetPath = MapAsset.PackageName.ToString();
|
|
||||||
FString MapName = MapAsset.AssetName.ToString();
|
|
||||||
|
|
||||||
if (!FilterStr.IsEmpty() && !MapName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
|
||||||
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
|
||||||
if (!World || !World->PersistentLevel) continue;
|
|
||||||
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
|
||||||
if (!LevelBP) continue;
|
|
||||||
|
|
||||||
SearchOneBlueprint(MapName, AssetPath, LevelBP, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
|
||||||
Result->SetArrayField(TEXT("results"), Results);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,665 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphNode.h"
|
|
||||||
#include "EdGraph/EdGraphPin.h"
|
|
||||||
#include "Kismet2/KismetEditorUtilities.h"
|
|
||||||
#include "Animation/AnimBlueprint.h"
|
|
||||||
#include "Animation/AnimSequence.h"
|
|
||||||
#include "Animation/BlendSpace.h"
|
|
||||||
#include "AnimGraphNode_SequencePlayer.h"
|
|
||||||
#include "AnimGraphNode_BlendSpacePlayer.h"
|
|
||||||
#include "AnimStateNode.h"
|
|
||||||
#include "AnimStateTransitionNode.h"
|
|
||||||
#include "AnimationStateMachineGraph.h"
|
|
||||||
#include "K2Node_VariableGet.h"
|
|
||||||
#include "MCPHandlers_StateMachine.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddAnimStateToMachine : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="State machine graph name"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name for the new state"))
|
|
||||||
FString StateName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="X position of the new state node"))
|
|
||||||
int32 PosX = 200;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Y position of the new state node"))
|
|
||||||
int32 PosY = 0;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Animation asset name to assign to the state"))
|
|
||||||
FString AnimationAsset;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a new state to an animation state machine graph. "
|
|
||||||
"Optionally assign an animation asset to the state.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
|
||||||
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
// Check for duplicate state name
|
|
||||||
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *Graph));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the state node
|
|
||||||
UAnimStateNode* NewState = NewObject<UAnimStateNode>(SMGraph);
|
|
||||||
NewState->CreateNewGuid();
|
|
||||||
NewState->NodePosX = PosX;
|
|
||||||
NewState->NodePosY = PosY;
|
|
||||||
|
|
||||||
// Set the state name via the bound graph
|
|
||||||
NewState->PostPlacedNewNode();
|
|
||||||
NewState->AllocateDefaultPins();
|
|
||||||
|
|
||||||
// Rename the bound graph to set the state name
|
|
||||||
if (NewState->GetBoundGraph())
|
|
||||||
{
|
|
||||||
NewState->GetBoundGraph()->Rename(*StateName, nullptr);
|
|
||||||
}
|
|
||||||
|
|
||||||
SMGraph->AddNode(NewState, false, false);
|
|
||||||
NewState->SetFlags(RF_Transactional);
|
|
||||||
|
|
||||||
// Optionally set animation asset
|
|
||||||
if (!AnimationAsset.IsEmpty() && NewState->GetBoundGraph())
|
|
||||||
{
|
|
||||||
// Try to find the animation asset and create a sequence player in the state's inner graph
|
|
||||||
MCPAssets<UAnimSequence> AnimAssets;
|
|
||||||
UAnimSequence* AnimSeq = AnimAssets.Exact(AnimationAsset).ENone().ETwo().Load() ? AnimAssets.Object() : nullptr;
|
|
||||||
|
|
||||||
if (AnimSeq)
|
|
||||||
{
|
|
||||||
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(NewState->GetBoundGraph());
|
|
||||||
SeqNode->CreateNewGuid();
|
|
||||||
SeqNode->PostPlacedNewNode();
|
|
||||||
SeqNode->AllocateDefaultPins();
|
|
||||||
SeqNode->SetAnimationAsset(AnimSeq);
|
|
||||||
SeqNode->NodePosX = 0;
|
|
||||||
SeqNode->NodePosY = 0;
|
|
||||||
NewState->GetBoundGraph()->AddNode(SeqNode, false, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile and save
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RemoveAnimStateFromMachine : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="State machine graph name"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the state to remove"))
|
|
||||||
FString StateName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Remove a state and its connected transitions from an animation state machine graph.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
|
||||||
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
|
||||||
if (!StateNode) return;
|
|
||||||
|
|
||||||
// Collect and remove transitions connected to this state
|
|
||||||
TArray<UAnimStateTransitionNode*> TransitionsToRemove;
|
|
||||||
for (UEdGraphNode* Node : SMGraph->Nodes)
|
|
||||||
{
|
|
||||||
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
|
|
||||||
{
|
|
||||||
if ((TransNode->GetPreviousState() == StateNode) || (TransNode->GetNextState() == StateNode))
|
|
||||||
{
|
|
||||||
TransitionsToRemove.Add(TransNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int32 RemovedTransitions = TransitionsToRemove.Num();
|
|
||||||
for (UAnimStateTransitionNode* Trans : TransitionsToRemove)
|
|
||||||
{
|
|
||||||
Trans->BreakAllNodeLinks();
|
|
||||||
SMGraph->RemoveNode(Trans);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove the state
|
|
||||||
StateNode->BreakAllNodeLinks();
|
|
||||||
SMGraph->RemoveNode(StateNode);
|
|
||||||
|
|
||||||
// Compile and save
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddAnimStateTransition : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="State machine graph name"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the source state"))
|
|
||||||
FString FromState;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the target state"))
|
|
||||||
FString ToState;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds"))
|
|
||||||
float CrossfadeDuration = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Transition priority order"))
|
|
||||||
int32 Priority = 0;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional"))
|
|
||||||
bool BBidirectional = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a transition between two states in an animation state machine graph.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
|
||||||
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
UAnimStateNode* FromStateNode = MCPUtils::FindStateByName(SMGraph, FromState, Result);
|
|
||||||
if (!FromStateNode) return;
|
|
||||||
|
|
||||||
UAnimStateNode* ToStateNode = MCPUtils::FindStateByName(SMGraph, ToState, Result);
|
|
||||||
if (!ToStateNode) return;
|
|
||||||
|
|
||||||
// Create transition node
|
|
||||||
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
|
|
||||||
TransNode->CreateNewGuid();
|
|
||||||
TransNode->PostPlacedNewNode();
|
|
||||||
TransNode->AllocateDefaultPins();
|
|
||||||
|
|
||||||
// Position between the two states
|
|
||||||
TransNode->NodePosX = (FromStateNode->NodePosX + ToStateNode->NodePosX) / 2;
|
|
||||||
TransNode->NodePosY = (FromStateNode->NodePosY + ToStateNode->NodePosY) / 2;
|
|
||||||
|
|
||||||
SMGraph->AddNode(TransNode, false, false);
|
|
||||||
TransNode->SetFlags(RF_Transactional);
|
|
||||||
|
|
||||||
// Connect: FromState output -> Transition input, Transition output -> ToState input
|
|
||||||
TransNode->CreateConnections(FromStateNode, ToStateNode);
|
|
||||||
|
|
||||||
// Set optional properties
|
|
||||||
if (Json->HasField(TEXT("crossfadeDuration")))
|
|
||||||
{
|
|
||||||
TransNode->CrossfadeDuration = CrossfadeDuration;
|
|
||||||
}
|
|
||||||
if (Json->HasField(TEXT("priority")))
|
|
||||||
{
|
|
||||||
TransNode->PriorityOrder = Priority;
|
|
||||||
}
|
|
||||||
if (Json->HasField(TEXT("bBidirectional")))
|
|
||||||
{
|
|
||||||
TransNode->Bidirectional = BBidirectional;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compile and save
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetAnimTransitionRule : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="State machine graph name"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the source state"))
|
|
||||||
FString FromState;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the target state"))
|
|
||||||
FString ToState;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds"))
|
|
||||||
float CrossfadeDuration = 0.0f;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Blend mode (as integer enum value)"))
|
|
||||||
int32 BlendMode = 0;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Transition priority order"))
|
|
||||||
int32 PriorityOrder = 0;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Logic type (as integer enum value)"))
|
|
||||||
int32 LogicType = 0;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional"))
|
|
||||||
bool BBidirectional = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Update properties on an existing transition between two states in an animation state machine.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
|
||||||
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromState, ToState);
|
|
||||||
if (!TransNode)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Transition from '%s' to '%s' not found in graph '%s'"),
|
|
||||||
*FromState, *ToState, *Graph));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update properties
|
|
||||||
int32 ChangedCount = 0;
|
|
||||||
TransNode->PreEditChange(nullptr);
|
|
||||||
|
|
||||||
if (Json->HasField(TEXT("crossfadeDuration")))
|
|
||||||
{
|
|
||||||
TransNode->CrossfadeDuration = CrossfadeDuration;
|
|
||||||
ChangedCount++;
|
|
||||||
}
|
|
||||||
if (Json->HasField(TEXT("blendMode")))
|
|
||||||
{
|
|
||||||
TransNode->BlendMode = (EAlphaBlendOption)BlendMode;
|
|
||||||
ChangedCount++;
|
|
||||||
}
|
|
||||||
if (Json->HasField(TEXT("priorityOrder")))
|
|
||||||
{
|
|
||||||
TransNode->PriorityOrder = PriorityOrder;
|
|
||||||
ChangedCount++;
|
|
||||||
}
|
|
||||||
if (Json->HasField(TEXT("logicType")))
|
|
||||||
{
|
|
||||||
TransNode->LogicType = (ETransitionLogicType::Type)LogicType;
|
|
||||||
ChangedCount++;
|
|
||||||
}
|
|
||||||
if (Json->HasField(TEXT("bBidirectional")))
|
|
||||||
{
|
|
||||||
TransNode->Bidirectional = BBidirectional;
|
|
||||||
ChangedCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ChangedCount == 0)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional"));
|
|
||||||
}
|
|
||||||
TransNode->PostEditChange();
|
|
||||||
|
|
||||||
// Compile and save
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetAnimStateAnimation : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="State machine graph name"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the state to modify"))
|
|
||||||
FString StateName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Animation asset name to assign"))
|
|
||||||
FString AnimationAsset;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Set or replace the animation sequence played by a state in an animation state machine.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
|
||||||
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
|
||||||
if (!StateNode) return;
|
|
||||||
|
|
||||||
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
|
||||||
if (!InnerGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the animation asset
|
|
||||||
MCPAssets<UAnimSequence> AnimAssets;
|
|
||||||
if (!AnimAssets.Exact(AnimationAsset).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimSequence* AnimSeq = AnimAssets.Object();
|
|
||||||
|
|
||||||
// Find existing SequencePlayer or create one
|
|
||||||
UAnimGraphNode_SequencePlayer* SeqNode = nullptr;
|
|
||||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
|
||||||
{
|
|
||||||
SeqNode = Cast<UAnimGraphNode_SequencePlayer>(Node);
|
|
||||||
if (SeqNode) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bCreatedNew = false;
|
|
||||||
if (!SeqNode)
|
|
||||||
{
|
|
||||||
SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(InnerGraph);
|
|
||||||
SeqNode->CreateNewGuid();
|
|
||||||
SeqNode->PostPlacedNewNode();
|
|
||||||
SeqNode->AllocateDefaultPins();
|
|
||||||
SeqNode->NodePosX = 0;
|
|
||||||
SeqNode->NodePosY = 0;
|
|
||||||
InnerGraph->AddNode(SeqNode, false, false);
|
|
||||||
bCreatedNew = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
SeqNode->SetAnimationAsset(AnimSeq);
|
|
||||||
|
|
||||||
// Compile and save
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetAnimStateBlendSpace : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="State machine graph name"))
|
|
||||||
FString Graph;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the state to modify"))
|
|
||||||
FString StateName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Blend Space asset name or path"))
|
|
||||||
FString BlendSpace;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the X axis input"))
|
|
||||||
FString XVariable;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the Y axis input"))
|
|
||||||
FString YVariable;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Place a BlendSpacePlayer in a state's inner graph, connect it to the output pose, "
|
|
||||||
"and optionally wire blueprint variables to the X and Y axis inputs.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UAnimBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
|
||||||
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
|
||||||
UAnimBlueprint* AnimBP = Assets.Object();
|
|
||||||
|
|
||||||
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
|
||||||
if (!StateNode) return;
|
|
||||||
|
|
||||||
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
|
||||||
if (!InnerGraph)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the blend space asset
|
|
||||||
MCPAssets<UBlendSpace> BlendSpaceAssets;
|
|
||||||
if (!BlendSpaceAssets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlendSpace* BlendSpaceAsset = BlendSpaceAssets.Object();
|
|
||||||
|
|
||||||
// Find existing BlendSpacePlayer or create one
|
|
||||||
UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr;
|
|
||||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
|
||||||
{
|
|
||||||
BSNode = Cast<UAnimGraphNode_BlendSpacePlayer>(Node);
|
|
||||||
if (BSNode) break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!BSNode)
|
|
||||||
{
|
|
||||||
BSNode = NewObject<UAnimGraphNode_BlendSpacePlayer>(InnerGraph);
|
|
||||||
BSNode->CreateNewGuid();
|
|
||||||
BSNode->PostPlacedNewNode();
|
|
||||||
BSNode->AllocateDefaultPins();
|
|
||||||
BSNode->NodePosX = 0;
|
|
||||||
BSNode->NodePosY = 0;
|
|
||||||
InnerGraph->AddNode(BSNode, false, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
BSNode->SetAnimationAsset(BlendSpaceAsset);
|
|
||||||
|
|
||||||
// Connect BlendSpacePlayer output to the Output Animation Pose node
|
|
||||||
{
|
|
||||||
// Find the AnimGraphNode_Root (Output Pose) in the inner graph
|
|
||||||
UEdGraphNode* ResultNode = nullptr;
|
|
||||||
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
|
||||||
{
|
|
||||||
if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) ||
|
|
||||||
Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult")))
|
|
||||||
{
|
|
||||||
ResultNode = Node;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ResultNode)
|
|
||||||
{
|
|
||||||
// Find output pose pin on BlendSpacePlayer and input pose pin on result node
|
|
||||||
UEdGraphPin* BSOutputPin = nullptr;
|
|
||||||
for (UEdGraphPin* Pin : BSNode->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct))
|
|
||||||
{
|
|
||||||
BSOutputPin = Pin;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
UEdGraphPin* ResultInputPin = nullptr;
|
|
||||||
for (UEdGraphPin* Pin : ResultNode->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && (Pin->Direction == EGPD_Input) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct))
|
|
||||||
{
|
|
||||||
ResultInputPin = Pin;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (BSOutputPin && ResultInputPin)
|
|
||||||
{
|
|
||||||
// Break existing connections on the result input
|
|
||||||
ResultInputPin->BreakAllPinLinks();
|
|
||||||
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
|
||||||
if (Schema)
|
|
||||||
{
|
|
||||||
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wire X and Y variables if provided
|
|
||||||
auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool
|
|
||||||
{
|
|
||||||
if (VarName.IsEmpty()) return false;
|
|
||||||
|
|
||||||
// Verify the variable exists in the blueprint
|
|
||||||
FName VarFName(*VarName);
|
|
||||||
bool bVarFound = false;
|
|
||||||
for (FBPVariableDescription& Var : AnimBP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName == VarFName)
|
|
||||||
{
|
|
||||||
bVarFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!bVarFound)
|
|
||||||
{
|
|
||||||
// Also check parent class properties
|
|
||||||
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
|
|
||||||
{
|
|
||||||
if (FProperty* Prop = GenClass->FindPropertyByName(VarFName))
|
|
||||||
{
|
|
||||||
bVarFound = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!bVarFound)
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"),
|
|
||||||
*VarName, *Blueprint);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a VariableGet node
|
|
||||||
UK2Node_VariableGet* GetNode = NewObject<UK2Node_VariableGet>(InnerGraph);
|
|
||||||
GetNode->VariableReference.SetSelfMember(VarFName);
|
|
||||||
GetNode->NodePosX = BSNode->NodePosX - 250;
|
|
||||||
GetNode->NodePosY = BSNode->NodePosY;
|
|
||||||
InnerGraph->AddNode(GetNode, false, false);
|
|
||||||
GetNode->AllocateDefaultPins();
|
|
||||||
|
|
||||||
// Find the variable output pin
|
|
||||||
UEdGraphPin* VarOutPin = nullptr;
|
|
||||||
for (UEdGraphPin* Pin : GetNode->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinName == VarFName))
|
|
||||||
{
|
|
||||||
VarOutPin = Pin;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the target pin on the BlendSpacePlayer
|
|
||||||
UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName));
|
|
||||||
|
|
||||||
if (VarOutPin && TargetPin)
|
|
||||||
{
|
|
||||||
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
|
||||||
if (Schema)
|
|
||||||
{
|
|
||||||
Schema->TryCreateConnection(VarOutPin, TargetPin);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
WireVariable(XVariable, TEXT("X"));
|
|
||||||
WireVariable(YVariable, TEXT("Y"));
|
|
||||||
|
|
||||||
// Compile and save
|
|
||||||
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,350 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "StructUtils/UserDefinedStruct.h"
|
|
||||||
#include "Engine/UserDefinedEnum.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
|
|
||||||
#include "Kismet2/EnumEditorUtils.h"
|
|
||||||
#include "AssetRegistry/AssetRegistryModule.h"
|
|
||||||
#include "AssetRegistry/IAssetRegistry.h"
|
|
||||||
#include "AssetToolsModule.h"
|
|
||||||
#include "IAssetTools.h"
|
|
||||||
#include "Factories/StructureFactory.h"
|
|
||||||
#include "Factories/EnumFactory.h"
|
|
||||||
#include "MCPHandlers_UserTypes.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
USTRUCT()
|
|
||||||
struct FStructPropertyEntry
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
UPROPERTY()
|
|
||||||
FString Type;
|
|
||||||
};
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateStructAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Full package path for the new struct (e.g. '/Game/DataTypes/S_MyStruct')"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Array of initial properties, each with 'name' and 'type' fields"))
|
|
||||||
FMCPJsonArray Properties;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new UserDefinedStruct asset. "
|
|
||||||
"Optionally add initial properties via the 'properties' array (each element needs 'name' and 'type').");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString PackagePath, AssetName;
|
|
||||||
if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if asset already exists
|
|
||||||
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
|
||||||
FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName));
|
|
||||||
if (ExistingAsset.IsValid())
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the struct using the AssetTools factory
|
|
||||||
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
||||||
IAssetTools& AssetTools = AssetToolsModule.Get();
|
|
||||||
|
|
||||||
UStructureFactory* Factory = NewObject<UStructureFactory>();
|
|
||||||
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedStruct::StaticClass(), Factory);
|
|
||||||
|
|
||||||
if (!NewAsset)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
|
|
||||||
}
|
|
||||||
|
|
||||||
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
|
|
||||||
if (!NewStruct)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add properties if specified
|
|
||||||
int32 PropsAdded = 0;
|
|
||||||
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
|
|
||||||
{
|
|
||||||
FStructPropertyEntry Entry;
|
|
||||||
if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return;
|
|
||||||
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
|
|
||||||
|
|
||||||
FEdGraphPinType PinType;
|
|
||||||
FString TypeError;
|
|
||||||
if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, TypeError))
|
|
||||||
{
|
|
||||||
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *Entry.Type, *Entry.Name, *TypeError);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snapshot existing GUIDs so we can find the newly added one
|
|
||||||
TSet<FGuid> ExistingGuids;
|
|
||||||
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
|
|
||||||
{
|
|
||||||
ExistingGuids.Add(Var.VarGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bAdded = FStructureEditorUtils::AddVariable(NewStruct, PinType);
|
|
||||||
if (bAdded)
|
|
||||||
{
|
|
||||||
// Find the new variable by diffing GUID sets
|
|
||||||
FGuid NewPropGuid;
|
|
||||||
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
|
|
||||||
{
|
|
||||||
if (!ExistingGuids.Contains(Var.VarGuid))
|
|
||||||
{
|
|
||||||
NewPropGuid = Var.VarGuid;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (NewPropGuid.IsValid())
|
|
||||||
{
|
|
||||||
FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, Entry.Name);
|
|
||||||
}
|
|
||||||
PropsAdded++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(NewStruct);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("assetName"), AssetName);
|
|
||||||
Result->SetNumberField(TEXT("propertiesAdded"), PropsAdded);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_CreateEnumAsset : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Full package path for the new enum (e.g. '/Game/DataTypes/E_MyEnum')"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Array of enum value names"))
|
|
||||||
FMCPJsonArray Values;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Create a new UserDefinedEnum asset with the specified values.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
FString PackagePath, AssetName;
|
|
||||||
if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName))
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')"));
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<FString> EnumValues;
|
|
||||||
for (const TSharedPtr<FJsonValue>& Val : Values.Array)
|
|
||||||
{
|
|
||||||
FString Str = Val->AsString();
|
|
||||||
if (!Str.IsEmpty()) EnumValues.Add(Str);
|
|
||||||
}
|
|
||||||
if (EnumValues.Num() == 0)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the enum using AssetTools
|
|
||||||
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
|
||||||
IAssetTools& AssetTools = AssetToolsModule.Get();
|
|
||||||
|
|
||||||
UEnumFactory* Factory = NewObject<UEnumFactory>();
|
|
||||||
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory);
|
|
||||||
|
|
||||||
if (!NewAsset)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
|
|
||||||
}
|
|
||||||
|
|
||||||
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset);
|
|
||||||
if (!NewEnum)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add enum values — UUserDefinedEnum starts with a MAX value.
|
|
||||||
// We need to add entries before MAX.
|
|
||||||
for (int32 i = 0; i < EnumValues.Num(); ++i)
|
|
||||||
{
|
|
||||||
// AddNewEnumeratorForUserDefinedEnum adds before the _MAX entry (returns void)
|
|
||||||
FEnumEditorUtils::AddNewEnumeratorForUserDefinedEnum(NewEnum);
|
|
||||||
// The new entry is at index (NumEnums - 2) because _MAX is last
|
|
||||||
int32 NewIndex = NewEnum->NumEnums() - 2;
|
|
||||||
FEnumEditorUtils::SetEnumeratorDisplayName(NewEnum, NewIndex, FText::FromString(EnumValues[i]));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(NewEnum);
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("assetName"), AssetName);
|
|
||||||
Result->SetNumberField(TEXT("valueCount"), EnumValues.Num());
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddStructField : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Name or package path of the struct asset"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name for the new field"))
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Type for the new field (e.g. 'int32', 'FString', 'FVector')"))
|
|
||||||
FString Type;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a new field to a UserDefinedStruct asset.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Find the struct
|
|
||||||
MCPAssets<UUserDefinedStruct> Assets;
|
|
||||||
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UUserDefinedStruct* Struct = Assets.Object();
|
|
||||||
|
|
||||||
// Resolve type
|
|
||||||
FEdGraphPinType PinType;
|
|
||||||
if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Snapshot existing GUIDs so we can find the newly added one
|
|
||||||
TSet<FGuid> ExistingGuids;
|
|
||||||
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
|
||||||
{
|
|
||||||
ExistingGuids.Add(Var.VarGuid);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType);
|
|
||||||
if (!bAdded)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to add property to struct"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the new variable by diffing GUID sets and rename it
|
|
||||||
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
|
||||||
{
|
|
||||||
if (!ExistingGuids.Contains(Var.VarGuid))
|
|
||||||
{
|
|
||||||
FStructureEditorUtils::RenameVariable(Struct, Var.VarGuid, Name);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(Struct);
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RemoveStructField : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Name or package path of the struct asset"))
|
|
||||||
FString AssetPath;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the field to remove"))
|
|
||||||
FString Name;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Remove a field from a UserDefinedStruct asset.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
// Find the struct
|
|
||||||
MCPAssets<UUserDefinedStruct> Assets;
|
|
||||||
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UUserDefinedStruct* Struct = Assets.Object();
|
|
||||||
|
|
||||||
// Find the property GUID by name
|
|
||||||
FGuid TargetGuid;
|
|
||||||
bool bFound = false;
|
|
||||||
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
|
||||||
{
|
|
||||||
if (Var.FriendlyName == Name || Var.VarName.ToString() == Name)
|
|
||||||
{
|
|
||||||
TargetGuid = Var.VarGuid;
|
|
||||||
bFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bFound)
|
|
||||||
{
|
|
||||||
// List available properties
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AvailProps;
|
|
||||||
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
|
||||||
{
|
|
||||||
AvailProps.Add(MakeShared<FJsonValueString>(Var.FriendlyName));
|
|
||||||
}
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found in struct '%s'"), *Name, *AssetPath));
|
|
||||||
Result->SetArrayField(TEXT("availableProperties"), AvailProps);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid);
|
|
||||||
if (!bRemoved)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *Name));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveGenericPackage(Struct);
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,576 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "CoreMinimal.h"
|
|
||||||
#include "MCPHandler.h"
|
|
||||||
#include "MCPAssetFinder.h"
|
|
||||||
#include "MCPUtils.h"
|
|
||||||
#include "Engine/Blueprint.h"
|
|
||||||
#include "EdGraph/EdGraph.h"
|
|
||||||
#include "EdGraph/EdGraphPin.h"
|
|
||||||
#include "K2Node_VariableGet.h"
|
|
||||||
#include "K2Node_VariableSet.h"
|
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
|
||||||
#include "MCPHandlers_Variables.generated.h"
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ChangeBlueprintVariableType : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the variable to change"))
|
|
||||||
FString Variable;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="New type name for the variable"))
|
|
||||||
FString NewType;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Type category: object, softobject, class, softclass, interface, struct, enum"))
|
|
||||||
FString TypeCategory;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, analyze the change without applying it"))
|
|
||||||
bool DryRun = false;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Change the type of a Blueprint member variable. "
|
|
||||||
"Supports dry-run mode to preview affected nodes before committing.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Verify variable exists
|
|
||||||
bool bVarFound = false;
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName.ToString() == Variable)
|
|
||||||
{
|
|
||||||
bVarFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!bVarFound)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the new pin type using shared resolver
|
|
||||||
FEdGraphPinType NewPinType;
|
|
||||||
FString ResolveInput = NewType;
|
|
||||||
|
|
||||||
// If typeCategory is an object reference variant, use colon syntax for the resolver
|
|
||||||
if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") ||
|
|
||||||
TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") ||
|
|
||||||
TypeCategory == TEXT("interface"))
|
|
||||||
{
|
|
||||||
ResolveInput = TypeCategory + TEXT(":") + NewType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Derive typeCategory from the resolved pin type for the response
|
|
||||||
FString ResolvedTypeCategory = TypeCategory;
|
|
||||||
if (ResolvedTypeCategory.IsEmpty())
|
|
||||||
{
|
|
||||||
if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
|
|
||||||
ResolvedTypeCategory = TEXT("struct");
|
|
||||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte)
|
|
||||||
ResolvedTypeCategory = TEXT("enum");
|
|
||||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object)
|
|
||||||
ResolvedTypeCategory = TEXT("object");
|
|
||||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject)
|
|
||||||
ResolvedTypeCategory = TEXT("softobject");
|
|
||||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class)
|
|
||||||
ResolvedTypeCategory = TEXT("class");
|
|
||||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)
|
|
||||||
ResolvedTypeCategory = TEXT("softclass");
|
|
||||||
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface)
|
|
||||||
ResolvedTypeCategory = TEXT("interface");
|
|
||||||
else
|
|
||||||
ResolvedTypeCategory = NewPinType.PinCategory.ToString();
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"),
|
|
||||||
DryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"),
|
|
||||||
*Variable, *Blueprint, *NewType, *ResolvedTypeCategory);
|
|
||||||
|
|
||||||
// Analyze affected nodes (get/set nodes for this variable)
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AffectedNodes;
|
|
||||||
for (UK2Node_VariableGet* VG : MCPUtils::AllNodes<UK2Node_VariableGet>(BP))
|
|
||||||
{
|
|
||||||
if (VG->GetVarName().ToString() != Variable) continue;
|
|
||||||
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
|
||||||
AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString());
|
|
||||||
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
|
|
||||||
AffNode->SetStringField(TEXT("graph"), VG->GetGraph()->GetName());
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AffPins;
|
|
||||||
for (UEdGraphPin* Pin : VG->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && (Pin->LinkedTo.Num() > 0) && (Pin->Direction == EGPD_Output))
|
|
||||||
{
|
|
||||||
AffPins.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
|
||||||
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
|
||||||
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
|
||||||
}
|
|
||||||
for (UK2Node_VariableSet* VS : MCPUtils::AllNodes<UK2Node_VariableSet>(BP))
|
|
||||||
{
|
|
||||||
if (VS->GetVarName().ToString() != Variable) continue;
|
|
||||||
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
|
||||||
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
|
|
||||||
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
|
|
||||||
AffNode->SetStringField(TEXT("graph"), VS->GetGraph()->GetName());
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AffPins;
|
|
||||||
for (UEdGraphPin* Pin : VS->Pins)
|
|
||||||
{
|
|
||||||
if (Pin && Pin->LinkedTo.Num() > 0)
|
|
||||||
{
|
|
||||||
AffPins.Add(MakeShared<FJsonValueString>(
|
|
||||||
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
|
||||||
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
|
||||||
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DryRun)
|
|
||||||
{
|
|
||||||
Result->SetBoolField(TEXT("dryRun"), true);
|
|
||||||
Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory);
|
|
||||||
Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num());
|
|
||||||
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Directly modify the variable type in the description array.
|
|
||||||
BP->PreEditChange(nullptr);
|
|
||||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName == FName(*Variable))
|
|
||||||
{
|
|
||||||
Var.VarType = NewPinType;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
BP->PostEditChange();
|
|
||||||
|
|
||||||
// Save
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"),
|
|
||||||
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
|
||||||
|
|
||||||
// Return updated variable state
|
|
||||||
TSharedRef<FJsonObject> UpdatedVar = MakeShared<FJsonObject>();
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName == FName(*Variable))
|
|
||||||
{
|
|
||||||
UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString());
|
|
||||||
UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString());
|
|
||||||
if (Var.VarType.PinSubCategoryObject.IsValid())
|
|
||||||
UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName());
|
|
||||||
UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray());
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar);
|
|
||||||
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_AddBlueprintVariable : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the new variable"))
|
|
||||||
FString VariableName;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Type of the new variable"))
|
|
||||||
FString VariableType;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
|
||||||
FString Category;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, make the variable an array"))
|
|
||||||
bool IsArray = false;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Default value for the variable"))
|
|
||||||
FString DefaultValue;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Add a new member variable to a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Check for duplicate variable name
|
|
||||||
FName VarFName(*VariableName);
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName == VarFName)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *Blueprint));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve the type using the shared helper
|
|
||||||
FEdGraphPinType PinType;
|
|
||||||
if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Set container type for arrays
|
|
||||||
if (IsArray)
|
|
||||||
{
|
|
||||||
PinType.ContainerType = EPinContainerType::Array;
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"),
|
|
||||||
*VariableName, *VariableType, IsArray ? TEXT("true") : TEXT("false"), *Blueprint);
|
|
||||||
|
|
||||||
// Add the variable using the editor utility function
|
|
||||||
bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue);
|
|
||||||
if (!bSuccess)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set category if provided
|
|
||||||
if (!Category.IsEmpty())
|
|
||||||
{
|
|
||||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
|
||||||
}
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"),
|
|
||||||
*VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_RemoveBlueprintVariable : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the variable to remove"))
|
|
||||||
FString VariableName;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Remove a member variable from a Blueprint.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Find variable by name (case-insensitive)
|
|
||||||
FName VarFName(*VariableName);
|
|
||||||
bool bVarFound = false;
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
|
|
||||||
{
|
|
||||||
VarFName = Var.VarName; // Use the exact name found
|
|
||||||
bVarFound = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bVarFound)
|
|
||||||
{
|
|
||||||
// Build available variables list for helpful error message
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AvailVars;
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("availableVariables"), AvailVars);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"),
|
|
||||||
*VariableName, *Blueprint);
|
|
||||||
|
|
||||||
// Use the editor utility to remove the variable (also cleans up Get/Set nodes)
|
|
||||||
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
|
|
||||||
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"),
|
|
||||||
*VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_SetBlueprintVariableMetadata : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Description="Name of the variable to modify"))
|
|
||||||
FString Variable;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
|
||||||
FString Category;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Tooltip text for the variable"))
|
|
||||||
FString Tooltip;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Replication mode: none, replicated, or repNotify"))
|
|
||||||
FString Replication;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, expose this variable on spawn"))
|
|
||||||
bool ExposeOnSpawn = false;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="If true, mark the variable as private"))
|
|
||||||
bool IsPrivate = false;
|
|
||||||
|
|
||||||
UPROPERTY(meta=(Optional, Description="Editability mode: editAnywhere, editDefaultsOnly, editInstanceOnly, or none"))
|
|
||||||
FString Editability;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("Set variable metadata properties such as category, tooltip, "
|
|
||||||
"replication, editability, and visibility flags.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
// Find the variable
|
|
||||||
FName VarFName(*Variable);
|
|
||||||
FBPVariableDescription* VarDesc = nullptr;
|
|
||||||
for (FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
if (Var.VarName == VarFName)
|
|
||||||
{
|
|
||||||
VarDesc = &Var;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!VarDesc)
|
|
||||||
{
|
|
||||||
TArray<TSharedPtr<FJsonValue>> AvailableVars;
|
|
||||||
for (const FBPVariableDescription& Var : BP->NewVariables)
|
|
||||||
{
|
|
||||||
AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
|
||||||
}
|
|
||||||
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint));
|
|
||||||
Result->SetArrayField(TEXT("availableVariables"), AvailableVars);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> Changes;
|
|
||||||
|
|
||||||
// Category
|
|
||||||
if (Json->HasField(TEXT("category")))
|
|
||||||
{
|
|
||||||
FString OldCategory = VarDesc->Category.ToString();
|
|
||||||
VarDesc->Category = FText::FromString(Category);
|
|
||||||
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
|
||||||
Change->SetStringField(TEXT("field"), TEXT("category"));
|
|
||||||
Change->SetStringField(TEXT("oldValue"), OldCategory);
|
|
||||||
Change->SetStringField(TEXT("newValue"), Category);
|
|
||||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip
|
|
||||||
if (Json->HasField(TEXT("tooltip")))
|
|
||||||
{
|
|
||||||
FString OldTooltip;
|
|
||||||
FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip);
|
|
||||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip);
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
|
||||||
Change->SetStringField(TEXT("field"), TEXT("tooltip"));
|
|
||||||
Change->SetStringField(TEXT("oldValue"), OldTooltip);
|
|
||||||
Change->SetStringField(TEXT("newValue"), Tooltip);
|
|
||||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replication
|
|
||||||
if (Json->HasField(TEXT("replication")))
|
|
||||||
{
|
|
||||||
uint64 OldFlags = VarDesc->PropertyFlags;
|
|
||||||
|
|
||||||
if (Replication == TEXT("none"))
|
|
||||||
{
|
|
||||||
VarDesc->PropertyFlags &= ~CPF_Net;
|
|
||||||
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
|
||||||
VarDesc->RepNotifyFunc = NAME_None;
|
|
||||||
}
|
|
||||||
else if (Replication == TEXT("replicated"))
|
|
||||||
{
|
|
||||||
VarDesc->PropertyFlags |= CPF_Net;
|
|
||||||
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
|
||||||
VarDesc->RepNotifyFunc = NAME_None;
|
|
||||||
}
|
|
||||||
else if (Replication == TEXT("repNotify"))
|
|
||||||
{
|
|
||||||
VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify;
|
|
||||||
// Auto-generate RepNotify function name
|
|
||||||
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *Replication));
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
|
||||||
Change->SetStringField(TEXT("field"), TEXT("replication"));
|
|
||||||
Change->SetStringField(TEXT("newValue"), Replication);
|
|
||||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExposeOnSpawn
|
|
||||||
if (Json->HasField(TEXT("exposeOnSpawn")))
|
|
||||||
{
|
|
||||||
bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0;
|
|
||||||
if (ExposeOnSpawn)
|
|
||||||
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
|
|
||||||
else
|
|
||||||
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
|
||||||
Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn"));
|
|
||||||
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
|
||||||
Change->SetStringField(TEXT("newValue"), ExposeOnSpawn ? TEXT("true") : TEXT("false"));
|
|
||||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
|
||||||
}
|
|
||||||
|
|
||||||
// isPrivate
|
|
||||||
if (Json->HasField(TEXT("isPrivate")))
|
|
||||||
{
|
|
||||||
bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0;
|
|
||||||
// In UE5, "private" for Blueprint variables is represented via metadata
|
|
||||||
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
|
|
||||||
TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false"));
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
|
||||||
Change->SetStringField(TEXT("field"), TEXT("isPrivate"));
|
|
||||||
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
|
||||||
Change->SetStringField(TEXT("newValue"), IsPrivate ? TEXT("true") : TEXT("false"));
|
|
||||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly)
|
|
||||||
if (Json->HasField(TEXT("editability")))
|
|
||||||
{
|
|
||||||
// Clear all edit flags first
|
|
||||||
VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate);
|
|
||||||
|
|
||||||
if (Editability == TEXT("editAnywhere"))
|
|
||||||
{
|
|
||||||
VarDesc->PropertyFlags |= CPF_Edit;
|
|
||||||
}
|
|
||||||
else if (Editability == TEXT("editDefaultsOnly"))
|
|
||||||
{
|
|
||||||
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance;
|
|
||||||
}
|
|
||||||
else if (Editability == TEXT("editInstanceOnly"))
|
|
||||||
{
|
|
||||||
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate;
|
|
||||||
}
|
|
||||||
else if (Editability == TEXT("none"))
|
|
||||||
{
|
|
||||||
// All edit flags already cleared
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
|
||||||
TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"),
|
|
||||||
*Editability));
|
|
||||||
}
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
|
||||||
Change->SetStringField(TEXT("field"), TEXT("editability"));
|
|
||||||
Change->SetStringField(TEXT("newValue"), Editability);
|
|
||||||
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Changes.Num() == 0)
|
|
||||||
{
|
|
||||||
return MCPUtils::MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability"));
|
|
||||||
}
|
|
||||||
|
|
||||||
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"),
|
|
||||||
*Blueprint, *Variable, Changes.Num());
|
|
||||||
|
|
||||||
BP->PreEditChange(nullptr);
|
|
||||||
BP->PostEditChange();
|
|
||||||
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
|
||||||
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
|
||||||
|
|
||||||
Result->SetArrayField(TEXT("changes"), Changes);
|
|
||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
|
#include "AnimStateNode.h"
|
||||||
|
#include "AnimStateTransitionNode.h"
|
||||||
|
#include "AnimationStateMachineGraph.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "UMCPHandler_AddAnimStateToMachine.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddAnimStateToMachine : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="State machine graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name for the new state"))
|
||||||
|
FString StateName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="X position of the new state node"))
|
||||||
|
int32 PosX = 200;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Y position of the new state node"))
|
||||||
|
int32 PosY = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Animation asset name to assign to the state"))
|
||||||
|
FString AnimationAsset;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a new state to an animation state machine graph. "
|
||||||
|
"Optionally assign an animation asset to the state.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
||||||
|
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
// Check for duplicate state name
|
||||||
|
if (MCPUtils::FindStateByName(SMGraph, StateName, nullptr))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' already exists in graph '%s'"), *StateName, *Graph));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the state node
|
||||||
|
UAnimStateNode* NewState = NewObject<UAnimStateNode>(SMGraph);
|
||||||
|
NewState->CreateNewGuid();
|
||||||
|
NewState->NodePosX = PosX;
|
||||||
|
NewState->NodePosY = PosY;
|
||||||
|
|
||||||
|
// Set the state name via the bound graph
|
||||||
|
NewState->PostPlacedNewNode();
|
||||||
|
NewState->AllocateDefaultPins();
|
||||||
|
|
||||||
|
// Rename the bound graph to set the state name
|
||||||
|
if (NewState->GetBoundGraph())
|
||||||
|
{
|
||||||
|
NewState->GetBoundGraph()->Rename(*StateName, nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
SMGraph->AddNode(NewState, false, false);
|
||||||
|
NewState->SetFlags(RF_Transactional);
|
||||||
|
|
||||||
|
// Optionally set animation asset
|
||||||
|
if (!AnimationAsset.IsEmpty() && NewState->GetBoundGraph())
|
||||||
|
{
|
||||||
|
// Try to find the animation asset and create a sequence player in the state's inner graph
|
||||||
|
MCPAssets<UAnimSequence> AnimAssets;
|
||||||
|
UAnimSequence* AnimSeq = AnimAssets.Exact(AnimationAsset).ENone().ETwo().Load() ? AnimAssets.Object() : nullptr;
|
||||||
|
|
||||||
|
if (AnimSeq)
|
||||||
|
{
|
||||||
|
UAnimGraphNode_SequencePlayer* SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(NewState->GetBoundGraph());
|
||||||
|
SeqNode->CreateNewGuid();
|
||||||
|
SeqNode->PostPlacedNewNode();
|
||||||
|
SeqNode->AllocateDefaultPins();
|
||||||
|
SeqNode->SetAnimationAsset(AnimSeq);
|
||||||
|
SeqNode->NodePosX = 0;
|
||||||
|
SeqNode->NodePosY = 0;
|
||||||
|
NewState->GetBoundGraph()->AddNode(SeqNode, false, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile and save
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeId"), NewState->NodeGuid.ToString());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
|
#include "AnimStateNode.h"
|
||||||
|
#include "AnimStateTransitionNode.h"
|
||||||
|
#include "AnimationStateMachineGraph.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "UMCPHandler_AddAnimStateTransition.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddAnimStateTransition : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="State machine graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the source state"))
|
||||||
|
FString FromState;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the target state"))
|
||||||
|
FString ToState;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds"))
|
||||||
|
float CrossfadeDuration = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Transition priority order"))
|
||||||
|
int32 Priority = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional"))
|
||||||
|
bool BBidirectional = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a transition between two states in an animation state machine graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
||||||
|
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
UAnimStateNode* FromStateNode = MCPUtils::FindStateByName(SMGraph, FromState, Result);
|
||||||
|
if (!FromStateNode) return;
|
||||||
|
|
||||||
|
UAnimStateNode* ToStateNode = MCPUtils::FindStateByName(SMGraph, ToState, Result);
|
||||||
|
if (!ToStateNode) return;
|
||||||
|
|
||||||
|
// Create transition node
|
||||||
|
UAnimStateTransitionNode* TransNode = NewObject<UAnimStateTransitionNode>(SMGraph);
|
||||||
|
TransNode->CreateNewGuid();
|
||||||
|
TransNode->PostPlacedNewNode();
|
||||||
|
TransNode->AllocateDefaultPins();
|
||||||
|
|
||||||
|
// Position between the two states
|
||||||
|
TransNode->NodePosX = (FromStateNode->NodePosX + ToStateNode->NodePosX) / 2;
|
||||||
|
TransNode->NodePosY = (FromStateNode->NodePosY + ToStateNode->NodePosY) / 2;
|
||||||
|
|
||||||
|
SMGraph->AddNode(TransNode, false, false);
|
||||||
|
TransNode->SetFlags(RF_Transactional);
|
||||||
|
|
||||||
|
// Connect: FromState output -> Transition input, Transition output -> ToState input
|
||||||
|
TransNode->CreateConnections(FromStateNode, ToStateNode);
|
||||||
|
|
||||||
|
// Set optional properties
|
||||||
|
if (Json->HasField(TEXT("crossfadeDuration")))
|
||||||
|
{
|
||||||
|
TransNode->CrossfadeDuration = CrossfadeDuration;
|
||||||
|
}
|
||||||
|
if (Json->HasField(TEXT("priority")))
|
||||||
|
{
|
||||||
|
TransNode->PriorityOrder = Priority;
|
||||||
|
}
|
||||||
|
if (Json->HasField(TEXT("bBidirectional")))
|
||||||
|
{
|
||||||
|
TransNode->Bidirectional = BBidirectional;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile and save
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeId"), TransNode->NodeGuid.ToString());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/SimpleConstructionScript.h"
|
||||||
|
#include "Engine/SCS_Node.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_AddBlueprintComponent.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddBlueprintComponent : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Component class name (e.g. StaticMeshComponent, SceneComponent)"))
|
||||||
|
FString ComponentClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Component name for the new component"))
|
||||||
|
FString Component;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Name of the parent component to attach to"))
|
||||||
|
FString ParentComponent;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a component to a Blueprint's SimpleConstructionScript. "
|
||||||
|
"Optionally attach it to an existing parent component.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||||
|
if (!SCS)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||||
|
*Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate component names
|
||||||
|
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
|
||||||
|
for (USCS_Node* Existing : ExistingNodes)
|
||||||
|
{
|
||||||
|
if (Existing && Existing->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("A component named '%s' already exists in Blueprint '%s'"),
|
||||||
|
*Component, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the component class by name
|
||||||
|
// Try multiple name variants: exact name, with U prefix, without U prefix
|
||||||
|
UClass* ComponentClassObj = nullptr;
|
||||||
|
|
||||||
|
TArray<FString> NamesToTry;
|
||||||
|
NamesToTry.Add(ComponentClass);
|
||||||
|
if (!ComponentClass.StartsWith(TEXT("U")))
|
||||||
|
{
|
||||||
|
NamesToTry.Add(FString::Printf(TEXT("U%s"), *ComponentClass));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Also try without U prefix
|
||||||
|
NamesToTry.Add(ComponentClass.Mid(1));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (!It->IsChildOf(UActorComponent::StaticClass()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ClassName = It->GetName();
|
||||||
|
for (const FString& NameToTry : NamesToTry)
|
||||||
|
{
|
||||||
|
if (ClassName.Equals(NameToTry, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
ComponentClassObj = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ComponentClassObj)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ComponentClassObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Component class '%s' not found or is not a subclass of UActorComponent. "
|
||||||
|
"Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, "
|
||||||
|
"SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, "
|
||||||
|
"ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, "
|
||||||
|
"WidgetComponent, BillboardComponent"),
|
||||||
|
*ComponentClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
// If parent component specified, find its SCS node
|
||||||
|
USCS_Node* ParentSCSNode = nullptr;
|
||||||
|
if (!ParentComponent.IsEmpty())
|
||||||
|
{
|
||||||
|
for (USCS_Node* Node : ExistingNodes)
|
||||||
|
{
|
||||||
|
if (Node && Node->GetVariableName().ToString().Equals(ParentComponent, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
ParentSCSNode = Node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ParentSCSNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Parent component '%s' not found in Blueprint '%s'"),
|
||||||
|
*ParentComponent, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding component '%s' (%s) to Blueprint '%s'"),
|
||||||
|
*Component, *ComponentClassObj->GetName(), *Blueprint);
|
||||||
|
|
||||||
|
// Create the SCS node
|
||||||
|
USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component));
|
||||||
|
if (!NewNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Failed to create SCS node for component '%s' with class '%s'"),
|
||||||
|
*Component, *ComponentClassObj->GetName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add to the hierarchy
|
||||||
|
if (ParentSCSNode)
|
||||||
|
{
|
||||||
|
ParentSCSNode->AddChildNode(NewNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SCS->AddNode(NewNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added component '%s' (%s) to '%s' (parent: %s, saved: %s)"),
|
||||||
|
*Component, *ComponentClassObj->GetName(), *Blueprint,
|
||||||
|
ParentSCSNode ? *ParentComponent : TEXT("(root)"),
|
||||||
|
bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("component"), NewNode->GetVariableName().ToString());
|
||||||
|
Result->SetStringField(TEXT("componentClass"), ComponentClassObj->GetName());
|
||||||
|
if (ParentSCSNode)
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("parentComponent"), ParentSCSNode->GetVariableName().ToString());
|
||||||
|
}
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_AddBlueprintInterface.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddBlueprintInterface : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Interface name (e.g. 'BPI_MyInterface') or native UInterface class name"))
|
||||||
|
FString InterfaceName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a Blueprint Interface implementation to a Blueprint. "
|
||||||
|
"Creates stub function graphs for each interface function.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Resolve the interface class
|
||||||
|
UClass* InterfaceClass = nullptr;
|
||||||
|
|
||||||
|
// Strategy 1: Search loaded UInterface classes by name
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (!It->IsChildOf(UInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ClassName = It->GetName();
|
||||||
|
// Match by class name (e.g. "BPI_Foo_C") or by trimmed name (e.g. "BPI_Foo")
|
||||||
|
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
InterfaceClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Strip the generated "_C" suffix for comparison
|
||||||
|
FString TrimmedName = ClassName;
|
||||||
|
if (TrimmedName.EndsWith(TEXT("_C")))
|
||||||
|
{
|
||||||
|
TrimmedName = TrimmedName.LeftChop(2);
|
||||||
|
}
|
||||||
|
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
InterfaceClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Try loading as a Blueprint Interface asset
|
||||||
|
if (!InterfaceClass)
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> IfaceAssets;
|
||||||
|
if (!IfaceAssets.Exact(InterfaceName).AllContent().Errors(Result).ETwo().Load()) return;
|
||||||
|
if (!IfaceAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
UClass* GenClass = IfaceAssets.Object()->GeneratedClass;
|
||||||
|
if (GenClass && GenClass->IsChildOf(UInterface::StaticClass()))
|
||||||
|
InterfaceClass = GenClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!InterfaceClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Interface '%s' not found. Provide a Blueprint Interface asset name (e.g. 'BPI_MyInterface') or a native UInterface class name."),
|
||||||
|
*InterfaceName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicates
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (IfaceDesc.Interface == InterfaceClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Interface '%s' is already implemented by Blueprint '%s'"),
|
||||||
|
*InterfaceName, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding interface '%s' to Blueprint '%s'"),
|
||||||
|
*InterfaceClass->GetName(), *Blueprint);
|
||||||
|
|
||||||
|
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
|
||||||
|
if (!bAdded)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("FBlueprintEditorUtils::ImplementNewInterface failed for interface '%s' on Blueprint '%s'"),
|
||||||
|
*InterfaceName, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect stub function graph names from the newly added interface entry
|
||||||
|
TArray<FString> AddedFunctions;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (IfaceDesc.Interface == InterfaceClass)
|
||||||
|
{
|
||||||
|
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
||||||
|
{
|
||||||
|
if (Graph)
|
||||||
|
{
|
||||||
|
AddedFunctions.Add(Graph->GetName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added interface '%s' to '%s' (%d function stubs)"),
|
||||||
|
*InterfaceClass->GetName(), *Blueprint, AddedFunctions.Num());
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("interfaceName"), InterfaceClass->GetName());
|
||||||
|
Result->SetStringField(TEXT("interfacePath"), InterfaceClass->GetPathName());
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
||||||
|
for (const FString& FuncName : AddedFunctions)
|
||||||
|
{
|
||||||
|
FuncArr.Add(MakeShared<FJsonValueString>(FuncName));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("functionGraphsAdded"), FuncArr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_AddBlueprintVariable.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddBlueprintVariable : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the new variable"))
|
||||||
|
FString VariableName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Type of the new variable"))
|
||||||
|
FString VariableType;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
||||||
|
FString Category;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, make the variable an array"))
|
||||||
|
bool IsArray = false;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Default value for the variable"))
|
||||||
|
FString DefaultValue;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a new member variable to a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Check for duplicate variable name
|
||||||
|
FName VarFName(*VariableName);
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName == VarFName)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Variable '%s' already exists in Blueprint '%s'"), *VariableName, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resolve the type using the shared helper
|
||||||
|
FEdGraphPinType PinType;
|
||||||
|
if (!MCPUtils::ResolveTypeFromString(VariableType, PinType, Result))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Set container type for arrays
|
||||||
|
if (IsArray)
|
||||||
|
{
|
||||||
|
PinType.ContainerType = EPinContainerType::Array;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding variable '%s' (type=%s, array=%s) to Blueprint '%s'"),
|
||||||
|
*VariableName, *VariableType, IsArray ? TEXT("true") : TEXT("false"), *Blueprint);
|
||||||
|
|
||||||
|
// Add the variable using the editor utility function
|
||||||
|
bool bSuccess = FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, PinType, DefaultValue);
|
||||||
|
if (!bSuccess)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("FBlueprintEditorUtils::AddMemberVariable failed for '%s'"), *VariableName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set category if provided
|
||||||
|
if (!Category.IsEmpty())
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added variable '%s' to '%s' (saved: %s)"),
|
||||||
|
*VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -10,7 +10,8 @@
|
|||||||
#include "K2Node_FunctionEntry.h"
|
#include "K2Node_FunctionEntry.h"
|
||||||
#include "K2Node_EditablePinBase.h"
|
#include "K2Node_EditablePinBase.h"
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
#include "MCPHandlers_Dispatchers.generated.h"
|
#include "UMCPHandler_AddEventDispatcher.generated.h"
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -156,75 +157,3 @@ public:
|
|||||||
Result->SetBoolField(TEXT("saved"), bSaved);
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
|
|
||||||
UCLASS()
|
|
||||||
class UMCPHandler_ListEventDispatchers : public UObject, public IMCPHandler
|
|
||||||
{
|
|
||||||
GENERATED_BODY()
|
|
||||||
|
|
||||||
public:
|
|
||||||
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
|
||||||
FString Blueprint;
|
|
||||||
|
|
||||||
virtual FString GetDescription() const override
|
|
||||||
{
|
|
||||||
return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures.");
|
|
||||||
}
|
|
||||||
|
|
||||||
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
|
||||||
{
|
|
||||||
MCPAssets<UBlueprint> Assets;
|
|
||||||
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
|
||||||
UBlueprint* BP = Assets.Object();
|
|
||||||
|
|
||||||
TSet<FName> DelegateNameSet;
|
|
||||||
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet);
|
|
||||||
|
|
||||||
TArray<TSharedPtr<FJsonValue>> DispatchersArr;
|
|
||||||
|
|
||||||
for (const FName& DelegateName : DelegateNameSet)
|
|
||||||
{
|
|
||||||
TSharedRef<FJsonObject> DispObj = MakeShared<FJsonObject>();
|
|
||||||
DispObj->SetStringField(TEXT("name"), DelegateName.ToString());
|
|
||||||
|
|
||||||
// Get parameter info from the signature graph
|
|
||||||
TArray<TSharedPtr<FJsonValue>> ParamsArr;
|
|
||||||
|
|
||||||
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
|
|
||||||
if (SigGraph)
|
|
||||||
{
|
|
||||||
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(SigGraph))
|
|
||||||
{
|
|
||||||
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
|
|
||||||
{
|
|
||||||
if (!PinInfo.IsValid()) continue;
|
|
||||||
|
|
||||||
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
|
||||||
ParamObj->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
|
|
||||||
|
|
||||||
// Build a human-readable type name from the pin type
|
|
||||||
FString TypeStr = PinInfo->PinType.PinCategory.ToString();
|
|
||||||
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
|
||||||
{
|
|
||||||
TypeStr = PinInfo->PinType.PinSubCategoryObject->GetName();
|
|
||||||
}
|
|
||||||
ParamObj->SetStringField(TEXT("type"), TypeStr);
|
|
||||||
|
|
||||||
ParamsArr.Add(MakeShared<FJsonValueObject>(ParamObj));
|
|
||||||
}
|
|
||||||
break; // only need the first entry node
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DispObj->SetArrayField(TEXT("parameters"), ParamsArr);
|
|
||||||
DispatchersArr.Add(MakeShared<FJsonValueObject>(DispObj));
|
|
||||||
}
|
|
||||||
|
|
||||||
Result->SetNumberField(TEXT("count"), DispatchersArr.Num());
|
|
||||||
Result->SetArrayField(TEXT("dispatchers"), DispatchersArr);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_AddFunctionParameter.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddFunctionParameter : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the function, custom event, or event dispatcher"))
|
||||||
|
FString FunctionName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name for the new parameter"))
|
||||||
|
FString ParamName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Type for the new parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
||||||
|
FString ParamType;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a new parameter to a function, custom event, or event dispatcher in a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Resolve param type
|
||||||
|
FEdGraphPinType PinType;
|
||||||
|
if (!MCPUtils::ResolveTypeFromString(ParamType, PinType, Result))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find the entry node using 3 strategies
|
||||||
|
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||||
|
FString NodeType;
|
||||||
|
|
||||||
|
FName FuncFName(*FunctionName);
|
||||||
|
|
||||||
|
// Strategy 1: K2Node_FunctionEntry in function graphs
|
||||||
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
||||||
|
{
|
||||||
|
UEdGraph* FEGraph = FE->GetGraph();
|
||||||
|
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
|
||||||
|
// Skip delegate signature graphs (handled in Strategy 3)
|
||||||
|
if (BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
|
||||||
|
|
||||||
|
EntryNode = FE;
|
||||||
|
NodeType = TEXT("FunctionEntry");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: K2Node_CustomEvent with matching CustomFunctionName
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
if (CE->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
EntryNode = CE;
|
||||||
|
NodeType = TEXT("CustomEvent");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: K2Node_FunctionEntry in DelegateSignatureGraphs
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
||||||
|
{
|
||||||
|
UEdGraph* FEGraph = FE->GetGraph();
|
||||||
|
if (!FEGraph->GetName().Equals(FunctionName, ESearchCase::IgnoreCase)) continue;
|
||||||
|
if (!BP->DelegateSignatureGraphs.Contains(FEGraph)) continue;
|
||||||
|
|
||||||
|
EntryNode = FE;
|
||||||
|
NodeType = TEXT("EventDispatcher");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
// Build a helpful error listing available functions, events, and dispatchers
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AvailFuncs;
|
||||||
|
|
||||||
|
for (UEdGraph* Graph : BP->FunctionGraphs)
|
||||||
|
{
|
||||||
|
if (Graph) AvailFuncs.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Custom events
|
||||||
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (custom event)"), *CE->CustomFunctionName.ToString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dispatchers
|
||||||
|
TSet<FName> DelegateNames;
|
||||||
|
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNames);
|
||||||
|
for (const FName& DN : DelegateNames)
|
||||||
|
{
|
||||||
|
AvailFuncs.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (event dispatcher)"), *DN.ToString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Function, custom event, or event dispatcher '%s' not found in Blueprint '%s'"),
|
||||||
|
*FunctionName, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("availableFunctions"), AvailFuncs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for duplicate parameter name
|
||||||
|
for (const TSharedPtr<FUserPinInfo>& Existing : EntryNode->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (Existing.IsValid() && Existing->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Parameter '%s' already exists on '%s'"), *ParamName, *FunctionName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Adding parameter '%s' (type=%s) to %s '%s' in Blueprint '%s'"),
|
||||||
|
*ParamName, *ParamType, *NodeType, *FunctionName, *Blueprint);
|
||||||
|
|
||||||
|
// Add the parameter pin (EGPD_Output on entry = input to callers)
|
||||||
|
EntryNode->CreateUserDefinedPin(FName(*ParamName), PinType, EGPD_Output);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added parameter '%s' to '%s' in '%s' (saved: %s)"),
|
||||||
|
*ParamName, *FunctionName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeType"), NodeType);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_AddMaterialExpression.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddMaterialExpression : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction, not both)"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material, not both)"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Expression class name without 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')"))
|
||||||
|
FString ExpressionClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="X position in the material graph editor"))
|
||||||
|
int32 PosX = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Y position in the material graph editor"))
|
||||||
|
int32 PosY = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a new expression node to a material or material function graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map string class name to UClass via dynamic lookup
|
||||||
|
UClass* ExprClass = nullptr;
|
||||||
|
|
||||||
|
// Convenience aliases for backward compatibility
|
||||||
|
static TMap<FString, FString> Aliases = {
|
||||||
|
{TEXT("Lerp"), TEXT("LinearInterpolate")},
|
||||||
|
};
|
||||||
|
|
||||||
|
FString LookupName = ExpressionClass;
|
||||||
|
if (const FString* Alias = Aliases.Find(ExpressionClass))
|
||||||
|
{
|
||||||
|
LookupName = *Alias;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dynamic lookup: find UMaterialExpression<Name> via UClass iteration
|
||||||
|
FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName);
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass()))
|
||||||
|
{
|
||||||
|
ExprClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ExprClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix "
|
||||||
|
"(e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp', 'Subtract', 'Fresnel', 'Comment', etc.)"),
|
||||||
|
*ExpressionClass));
|
||||||
|
}
|
||||||
|
if (ExprClass->HasAnyClassFlags(CLASS_Abstract))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Expression class '%s' is abstract and cannot be instantiated."), *ExpressionClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material or material function
|
||||||
|
UMaterial* MaterialObj = nullptr;
|
||||||
|
UMaterialFunction* MatFunc = nullptr;
|
||||||
|
UObject* Owner = nullptr;
|
||||||
|
FString AssetDisplayName;
|
||||||
|
|
||||||
|
if (!MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
if (!Material.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Specify either 'material' or 'materialFunction', not both"));
|
||||||
|
}
|
||||||
|
MCPAssets<UMaterialFunction> MFAssets;
|
||||||
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MatFunc = MFAssets.Object();
|
||||||
|
Owner = MatFunc;
|
||||||
|
AssetDisplayName = MatFunc->GetName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MaterialObj = MatAssets.Object();
|
||||||
|
Owner = MaterialObj;
|
||||||
|
AssetDisplayName = MaterialObj->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would add expression '%s' to '%s' at (%d, %d)"),
|
||||||
|
*ExpressionClass, *AssetDisplayName, PosX, PosY);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it)
|
||||||
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
||||||
|
|
||||||
|
// Create, register, and PostEditChange the expression — all inside an SEH wrapper because
|
||||||
|
// some classes (e.g. UMaterialExpressionParameter) lack CLASS_Abstract but crash during
|
||||||
|
// PostEditChange. The SEH wrapper cleans up the bad expression on crash.
|
||||||
|
UMaterialExpression* NewExpr = nullptr;
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
int32 CreateResult = TryAddMaterialExpressionSEH(Owner, ExprClass, MaterialObj, MatFunc, PosX, PosY, &NewExpr);
|
||||||
|
if (CreateResult != 0 || !NewExpr)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Expression class '%s' cannot be instantiated (may be abstract or have internal errors)."),
|
||||||
|
*ExpressionClass));
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
|
||||||
|
if (!NewExpr)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create material expression object"));
|
||||||
|
}
|
||||||
|
NewExpr->MaterialExpressionEditorX = PosX;
|
||||||
|
NewExpr->MaterialExpressionEditorY = PosY;
|
||||||
|
if (MaterialObj)
|
||||||
|
{
|
||||||
|
MaterialObj->GetExpressionCollection().AddExpression(NewExpr);
|
||||||
|
if (MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
MaterialObj->MaterialGraph->RebuildGraph();
|
||||||
|
}
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
MaterialObj->MarkPackageDirty();
|
||||||
|
}
|
||||||
|
else if (MatFunc)
|
||||||
|
{
|
||||||
|
MatFunc->GetExpressionCollection().AddExpression(NewExpr);
|
||||||
|
MatFunc->PreEditChange(nullptr);
|
||||||
|
MatFunc->PostEditChange();
|
||||||
|
MatFunc->MarkPackageDirty();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
||||||
|
|
||||||
|
// Find the node GUID from the material graph (only for materials)
|
||||||
|
FString NodeGuid;
|
||||||
|
if (MaterialObj && MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
||||||
|
{
|
||||||
|
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node);
|
||||||
|
if (MatNode && MatNode->MaterialExpression == NewExpr)
|
||||||
|
{
|
||||||
|
NodeGuid = Node->NodeGuid.ToString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Added expression '%s' to '%s' (nodeId: %s, saved: %s)"),
|
||||||
|
*ExpressionClass, *AssetDisplayName, *NodeGuid, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
// Serialize the expression details
|
||||||
|
TSharedPtr<FJsonObject> ExprDetails = MCPUtils::SerializeMaterialExpression(NewExpr);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetStringField(TEXT("nodeId"), NodeGuid);
|
||||||
|
if (ExprDetails.IsValid())
|
||||||
|
{
|
||||||
|
Result->SetObjectField(TEXT("expression"), ExprDetails);
|
||||||
|
}
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "StructUtils/UserDefinedStruct.h"
|
||||||
|
#include "Engine/UserDefinedEnum.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
|
||||||
|
#include "Kismet2/EnumEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Factories/StructureFactory.h"
|
||||||
|
#include "Factories/EnumFactory.h"
|
||||||
|
#include "UMCPHandler_AddStructField.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_AddStructField : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name or package path of the struct asset"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name for the new field"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Type for the new field (e.g. 'int32', 'FString', 'FVector')"))
|
||||||
|
FString Type;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Add a new field to a UserDefinedStruct asset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Find the struct
|
||||||
|
MCPAssets<UUserDefinedStruct> Assets;
|
||||||
|
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UUserDefinedStruct* Struct = Assets.Object();
|
||||||
|
|
||||||
|
// Resolve type
|
||||||
|
FEdGraphPinType PinType;
|
||||||
|
if (!MCPUtils::ResolveTypeFromString(Type, PinType, Result))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Snapshot existing GUIDs so we can find the newly added one
|
||||||
|
TSet<FGuid> ExistingGuids;
|
||||||
|
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
||||||
|
{
|
||||||
|
ExistingGuids.Add(Var.VarGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bAdded = FStructureEditorUtils::AddVariable(Struct, PinType);
|
||||||
|
if (!bAdded)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to add property to struct"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the new variable by diffing GUID sets and rename it
|
||||||
|
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
||||||
|
{
|
||||||
|
if (!ExistingGuids.Contains(Var.VarGuid))
|
||||||
|
{
|
||||||
|
FStructureEditorUtils::RenameVariable(Struct, Var.VarGuid, Name);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(Struct);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "FileHelpers.h"
|
||||||
|
#include "UMCPHandler_BackupAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_BackupAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Copy an asset's .uasset file to a .uasset.bak backup.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString Filename = FPaths::ConvertRelativePathToFull(
|
||||||
|
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
|
||||||
|
|
||||||
|
if (!IFileManager::Get().FileExists(*Filename))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found: %s"), *Filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString BackupFilename = Filename + TEXT(".bak");
|
||||||
|
uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true);
|
||||||
|
if (CopyResult != COPY_OK)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to back up %s"), *Filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("backupFile"), BackupFilename);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_ChangeBlueprintVariableType.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ChangeBlueprintVariableType : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the variable to change"))
|
||||||
|
FString Variable;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New type name for the variable"))
|
||||||
|
FString NewType;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Type category: object, softobject, class, softclass, interface, struct, enum"))
|
||||||
|
FString TypeCategory;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, analyze the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Change the type of a Blueprint member variable. "
|
||||||
|
"Supports dry-run mode to preview affected nodes before committing.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Verify variable exists
|
||||||
|
bool bVarFound = false;
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName.ToString() == Variable)
|
||||||
|
{
|
||||||
|
bVarFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bVarFound)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the new pin type using shared resolver
|
||||||
|
FEdGraphPinType NewPinType;
|
||||||
|
FString ResolveInput = NewType;
|
||||||
|
|
||||||
|
// If typeCategory is an object reference variant, use colon syntax for the resolver
|
||||||
|
if (TypeCategory == TEXT("object") || TypeCategory == TEXT("softobject") ||
|
||||||
|
TypeCategory == TEXT("class") || TypeCategory == TEXT("softclass") ||
|
||||||
|
TypeCategory == TEXT("interface"))
|
||||||
|
{
|
||||||
|
ResolveInput = TypeCategory + TEXT(":") + NewType;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MCPUtils::ResolveTypeFromString(ResolveInput, NewPinType, Result))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Derive typeCategory from the resolved pin type for the response
|
||||||
|
FString ResolvedTypeCategory = TypeCategory;
|
||||||
|
if (ResolvedTypeCategory.IsEmpty())
|
||||||
|
{
|
||||||
|
if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Struct)
|
||||||
|
ResolvedTypeCategory = TEXT("struct");
|
||||||
|
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Enum || NewPinType.PinCategory == UEdGraphSchema_K2::PC_Byte)
|
||||||
|
ResolvedTypeCategory = TEXT("enum");
|
||||||
|
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Object)
|
||||||
|
ResolvedTypeCategory = TEXT("object");
|
||||||
|
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftObject)
|
||||||
|
ResolvedTypeCategory = TEXT("softobject");
|
||||||
|
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Class)
|
||||||
|
ResolvedTypeCategory = TEXT("class");
|
||||||
|
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_SoftClass)
|
||||||
|
ResolvedTypeCategory = TEXT("softclass");
|
||||||
|
else if (NewPinType.PinCategory == UEdGraphSchema_K2::PC_Interface)
|
||||||
|
ResolvedTypeCategory = TEXT("interface");
|
||||||
|
else
|
||||||
|
ResolvedTypeCategory = NewPinType.PinCategory.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s variable '%s' in '%s' to %s (%s)"),
|
||||||
|
DryRun ? TEXT("[DRY RUN] Analyzing change of") : TEXT("Changing"),
|
||||||
|
*Variable, *Blueprint, *NewType, *ResolvedTypeCategory);
|
||||||
|
|
||||||
|
// Analyze affected nodes (get/set nodes for this variable)
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AffectedNodes;
|
||||||
|
for (UK2Node_VariableGet* VG : MCPUtils::AllNodes<UK2Node_VariableGet>(BP))
|
||||||
|
{
|
||||||
|
if (VG->GetVarName().ToString() != Variable) continue;
|
||||||
|
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
||||||
|
AffNode->SetStringField(TEXT("nodeId"), VG->NodeGuid.ToString());
|
||||||
|
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
|
||||||
|
AffNode->SetStringField(TEXT("graph"), VG->GetGraph()->GetName());
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AffPins;
|
||||||
|
for (UEdGraphPin* Pin : VG->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && (Pin->LinkedTo.Num() > 0) && (Pin->Direction == EGPD_Output))
|
||||||
|
{
|
||||||
|
AffPins.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
||||||
|
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
||||||
|
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
||||||
|
}
|
||||||
|
for (UK2Node_VariableSet* VS : MCPUtils::AllNodes<UK2Node_VariableSet>(BP))
|
||||||
|
{
|
||||||
|
if (VS->GetVarName().ToString() != Variable) continue;
|
||||||
|
TSharedRef<FJsonObject> AffNode = MakeShared<FJsonObject>();
|
||||||
|
AffNode->SetStringField(TEXT("nodeId"), VS->NodeGuid.ToString());
|
||||||
|
AffNode->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
|
||||||
|
AffNode->SetStringField(TEXT("graph"), VS->GetGraph()->GetName());
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AffPins;
|
||||||
|
for (UEdGraphPin* Pin : VS->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && Pin->LinkedTo.Num() > 0)
|
||||||
|
{
|
||||||
|
AffPins.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (connected to %d pin(s))"),
|
||||||
|
*Pin->PinName.ToString(), Pin->LinkedTo.Num())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AffNode->SetArrayField(TEXT("affectedPins"), AffPins);
|
||||||
|
AffectedNodes.Add(MakeShared<FJsonValueObject>(AffNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory);
|
||||||
|
Result->SetNumberField(TEXT("affectedNodeCount"), AffectedNodes.Num());
|
||||||
|
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directly modify the variable type in the description array.
|
||||||
|
BP->PreEditChange(nullptr);
|
||||||
|
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName == FName(*Variable))
|
||||||
|
{
|
||||||
|
Var.VarType = NewPinType;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BP->PostEditChange();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Variable type changed, save %s"),
|
||||||
|
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||||
|
|
||||||
|
// Return updated variable state
|
||||||
|
TSharedRef<FJsonObject> UpdatedVar = MakeShared<FJsonObject>();
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName == FName(*Variable))
|
||||||
|
{
|
||||||
|
UpdatedVar->SetStringField(TEXT("name"), Var.VarName.ToString());
|
||||||
|
UpdatedVar->SetStringField(TEXT("type"), Var.VarType.PinCategory.ToString());
|
||||||
|
if (Var.VarType.PinSubCategoryObject.IsValid())
|
||||||
|
UpdatedVar->SetStringField(TEXT("subtype"), Var.VarType.PinSubCategoryObject->GetName());
|
||||||
|
UpdatedVar->SetBoolField(TEXT("isArray"), Var.VarType.IsArray());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("typeCategory"), ResolvedTypeCategory);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
Result->SetObjectField(TEXT("updatedVariable"), UpdatedVar);
|
||||||
|
Result->SetArrayField(TEXT("affectedNodes"), AffectedNodes);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_ChangeFunctionParameterType.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ChangeFunctionParameterType : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
||||||
|
FString FunctionName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the parameter to change"))
|
||||||
|
FString ParamName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New type for the parameter (e.g. 'Float', 'Vector', 'MyStruct')"))
|
||||||
|
FString NewType;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, analyze impact without making changes"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Change the type of an existing parameter on a function or custom event in a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Resolve the new type using the shared resolver (supports primitives, structs, enums, and object references)
|
||||||
|
FEdGraphPinType NewPinType;
|
||||||
|
if (!MCPUtils::ResolveTypeFromString(NewType, NewPinType, Result))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find the entry node: K2Node_FunctionEntry in a function graph,
|
||||||
|
// or K2Node_CustomEvent in any graph
|
||||||
|
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||||
|
FString FoundNodeType;
|
||||||
|
|
||||||
|
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
|
||||||
|
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
||||||
|
{
|
||||||
|
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
EntryNode = FuncEntry;
|
||||||
|
FoundNodeType = TEXT("FunctionEntry");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
EntryNode = CustomEvent;
|
||||||
|
FoundNodeType = TEXT("CustomEvent");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
// List available functions/events for debugging
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Available;
|
||||||
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
||||||
|
{
|
||||||
|
Available.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
|
||||||
|
}
|
||||||
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
Available.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
||||||
|
*FunctionName, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the UserDefinedPin matching paramName
|
||||||
|
bool bPinFound = false;
|
||||||
|
for (TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (PinInfo.IsValid() && PinInfo->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
EntryNode->PreEditChange(nullptr);
|
||||||
|
PinInfo->PinType = NewPinType;
|
||||||
|
EntryNode->PostEditChange();
|
||||||
|
bPinFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bPinFound)
|
||||||
|
{
|
||||||
|
// List available params for debugging
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
||||||
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (PinInfo.IsValid())
|
||||||
|
{
|
||||||
|
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Parameter '%s' not found in %s '%s'"),
|
||||||
|
*ParamName, *FoundNodeType, *FunctionName));
|
||||||
|
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for dry run
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
// Analyze what would change: report connected pins that may disconnect
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AffectedPins;
|
||||||
|
for (UEdGraphPin* Pin : EntryNode->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && Pin->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase) && Pin->LinkedTo.Num() > 0)
|
||||||
|
{
|
||||||
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (Linked && Linked->GetOwningNode())
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> AffPin = MakeShared<FJsonObject>();
|
||||||
|
AffPin->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
||||||
|
AffPin->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
||||||
|
AffPin->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
||||||
|
AffPin->SetStringField(TEXT("currentType"), Pin->PinType.PinCategory.ToString());
|
||||||
|
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
AffPin->SetStringField(TEXT("currentSubtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
||||||
|
AffectedPins.Add(MakeShared<FJsonValueObject>(AffPin));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||||
|
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||||
|
Result->SetNumberField(TEXT("connectionsAtRisk"), AffectedPins.Num());
|
||||||
|
Result->SetArrayField(TEXT("affectedPins"), AffectedPins);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing param '%s' in %s '%s' of '%s' to %s"),
|
||||||
|
*ParamName, *FoundNodeType, *FunctionName, *Blueprint, *NewType);
|
||||||
|
|
||||||
|
// Reconstruct the node to update output pins with the new type (use schema for MinimalAPI compat)
|
||||||
|
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
||||||
|
{
|
||||||
|
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
||||||
|
{
|
||||||
|
Schema->ReconstructNode(*EntryNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter type changed, save %s"),
|
||||||
|
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||||
|
|
||||||
|
// Serialize the updated entry node state
|
||||||
|
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(EntryNode);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||||
|
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
if (UpdatedNodeState.IsValid())
|
||||||
|
{
|
||||||
|
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,279 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_ChangeStructNodeType.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ChangeStructNodeType : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the BreakStruct or MakeStruct node"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New struct type name (e.g. 'FVector', 'Vector')"))
|
||||||
|
FString NewType;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Change the struct type on a BreakStruct or MakeStruct node. "
|
||||||
|
"Attempts to reconnect matching pins after the type change.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
// Load Blueprint
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find node
|
||||||
|
UEdGraph* Graph = nullptr;
|
||||||
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
||||||
|
if (!FoundNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine what kind of struct node this is
|
||||||
|
UK2Node_BreakStruct* BreakNode = Cast<UK2Node_BreakStruct>(FoundNode);
|
||||||
|
UK2Node_MakeStruct* MakeNode = Cast<UK2Node_MakeStruct>(FoundNode);
|
||||||
|
|
||||||
|
if (!BreakNode && !MakeNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' is not a BreakStruct or MakeStruct node (class: %s)"),
|
||||||
|
*Node, *FoundNode->GetClass()->GetName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the new struct type
|
||||||
|
FString SearchName = NewType;
|
||||||
|
if (SearchName.StartsWith(TEXT("F")))
|
||||||
|
{
|
||||||
|
SearchName = SearchName.Mid(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
UScriptStruct* NewStruct = FindFirstObject<UScriptStruct>(*SearchName);
|
||||||
|
if (!NewStruct)
|
||||||
|
{
|
||||||
|
// Try with full name including F prefix
|
||||||
|
NewStruct = FindFirstObject<UScriptStruct>(*NewType);
|
||||||
|
}
|
||||||
|
if (!NewStruct)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Struct type '%s' not found"), *NewType));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Changing struct node '%s' to type '%s'"),
|
||||||
|
*Node, *NewStruct->GetName());
|
||||||
|
|
||||||
|
// Helper: extract property base name from a BreakStruct pin name
|
||||||
|
auto ExtractPropertyBaseName = [](const FString& PinName) -> FString
|
||||||
|
{
|
||||||
|
// Find the last underscore before 32 hex chars (GUID)
|
||||||
|
int32 LastUnderscore;
|
||||||
|
if (PinName.FindLastChar(TEXT('_'), LastUnderscore) && (LastUnderscore > 0))
|
||||||
|
{
|
||||||
|
FString Suffix = PinName.Mid(LastUnderscore + 1);
|
||||||
|
if (Suffix.Len() == 32)
|
||||||
|
{
|
||||||
|
FString WithoutGuid = PinName.Left(LastUnderscore);
|
||||||
|
// Strip _Index
|
||||||
|
int32 SecondUnderscore;
|
||||||
|
if (WithoutGuid.FindLastChar(TEXT('_'), SecondUnderscore) && (SecondUnderscore > 0))
|
||||||
|
{
|
||||||
|
FString IndexStr = WithoutGuid.Mid(SecondUnderscore + 1);
|
||||||
|
if (IndexStr.IsNumeric())
|
||||||
|
{
|
||||||
|
return WithoutGuid.Left(SecondUnderscore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return PinName;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Remember existing connections keyed by property base name
|
||||||
|
struct FPinConnection
|
||||||
|
{
|
||||||
|
EEdGraphPinDirection Direction;
|
||||||
|
TArray<UEdGraphPin*> LinkedPins;
|
||||||
|
};
|
||||||
|
TMap<FString, FPinConnection> ConnectionsByBaseName;
|
||||||
|
|
||||||
|
for (UEdGraphPin* Pin : FoundNode->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
|
||||||
|
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
|
||||||
|
|
||||||
|
FString BaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
|
||||||
|
FPinConnection& Conn = ConnectionsByBaseName.FindOrAdd(BaseName);
|
||||||
|
Conn.Direction = Pin->Direction;
|
||||||
|
Conn.LinkedPins = Pin->LinkedTo;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Saved %d pin connections to reconnect"), ConnectionsByBaseName.Num());
|
||||||
|
|
||||||
|
// Change the struct type and reconstruct
|
||||||
|
if (BreakNode)
|
||||||
|
{
|
||||||
|
BreakNode->StructType = NewStruct;
|
||||||
|
}
|
||||||
|
else if (MakeNode)
|
||||||
|
{
|
||||||
|
MakeNode->StructType = NewStruct;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Break all existing links before reconstruction
|
||||||
|
FoundNode->BreakAllNodeLinks();
|
||||||
|
|
||||||
|
// Reconnect pins by matching property base names
|
||||||
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
||||||
|
if (!Schema)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct to rebuild pins for the new struct type (use schema version for MinimalAPI compat)
|
||||||
|
Schema->ReconstructNode(*FoundNode);
|
||||||
|
|
||||||
|
int32 Reconnected = 0;
|
||||||
|
int32 Failed = 0;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ReconnectDetails;
|
||||||
|
|
||||||
|
for (auto& Pair : ConnectionsByBaseName)
|
||||||
|
{
|
||||||
|
const FString& BaseName = Pair.Key;
|
||||||
|
const FPinConnection& OldConn = Pair.Value;
|
||||||
|
|
||||||
|
// Find matching new pin
|
||||||
|
UEdGraphPin* NewPin = nullptr;
|
||||||
|
for (UEdGraphPin* Pin : FoundNode->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || Pin->Direction != OldConn.Direction) continue;
|
||||||
|
FString NewBaseName = ExtractPropertyBaseName(Pin->PinName.ToString());
|
||||||
|
if (NewBaseName.Equals(BaseName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
NewPin = Pin;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also try matching the struct input/output pin (single struct pin)
|
||||||
|
if (!NewPin)
|
||||||
|
{
|
||||||
|
for (UEdGraphPin* Pin : FoundNode->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || Pin->Direction != OldConn.Direction) continue;
|
||||||
|
if ((Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct) &&
|
||||||
|
(Pin->PinType.PinSubCategoryObject == NewStruct))
|
||||||
|
{
|
||||||
|
NewPin = Pin;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NewPin)
|
||||||
|
{
|
||||||
|
for (UEdGraphPin* Target : OldConn.LinkedPins)
|
||||||
|
{
|
||||||
|
bool bOK = Schema->TryCreateConnection(NewPin, Target);
|
||||||
|
if (bOK)
|
||||||
|
{
|
||||||
|
Reconnected++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedPtr<FJsonObject> Detail = MakeShared<FJsonObject>();
|
||||||
|
Detail->SetStringField(TEXT("property"), BaseName);
|
||||||
|
Detail->SetBoolField(TEXT("connected"), bOK);
|
||||||
|
ReconnectDetails.Add(MakeShared<FJsonValueObject>(Detail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Failed += OldConn.LinkedPins.Num();
|
||||||
|
TSharedPtr<FJsonObject> Detail = MakeShared<FJsonObject>();
|
||||||
|
Detail->SetStringField(TEXT("property"), BaseName);
|
||||||
|
Detail->SetBoolField(TEXT("connected"), false);
|
||||||
|
Detail->SetStringField(TEXT("reason"), TEXT("No matching pin found on new struct"));
|
||||||
|
ReconnectDetails.Add(MakeShared<FJsonValueObject>(Detail));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
// Return updated node state
|
||||||
|
TSharedPtr<FJsonObject> UpdatedNodeState = MCPUtils::SerializeNode(FoundNode);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("newStructType"), NewStruct->GetName());
|
||||||
|
Result->SetStringField(TEXT("nodeClass"), FoundNode->GetClass()->GetName());
|
||||||
|
Result->SetNumberField(TEXT("reconnected"), Reconnected);
|
||||||
|
Result->SetNumberField(TEXT("failed"), Failed);
|
||||||
|
Result->SetArrayField(TEXT("reconnectDetails"), ReconnectDetails);
|
||||||
|
if (UpdatedNodeState.IsValid())
|
||||||
|
{
|
||||||
|
Result->SetObjectField(TEXT("updatedNode"), UpdatedNodeState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_CheckPinConnectionCompatibility.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleCheckPinCompatibility — pre-flight check for connect_pins
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CheckPinConnectionCompatibility : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Source node (GUID)"))
|
||||||
|
FString SourceNode;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Source pin name"))
|
||||||
|
FString SourcePinName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Target node (GUID)"))
|
||||||
|
FString TargetNode;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Target pin name"))
|
||||||
|
FString TargetPinName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Check whether two pins can be connected, and what kind of connection would result. "
|
||||||
|
"Use as a pre-flight check before connect_blueprint_pins.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
UEdGraph* SourceGraph = nullptr;
|
||||||
|
UEdGraphNode* FoundSourceNode = MCPUtils::FindNodeByGuid(BP, SourceNode, &SourceGraph);
|
||||||
|
if (!FoundSourceNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found"), *SourceNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphNode* FoundTargetNode = MCPUtils::FindNodeByGuid(BP, TargetNode);
|
||||||
|
if (!FoundTargetNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found"), *TargetNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* SourcePin = FoundSourceNode->FindPin(FName(*SourcePinName));
|
||||||
|
if (!SourcePin)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *SourcePinName, *SourceNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* TargetPin = FoundTargetNode->FindPin(FName(*TargetPinName));
|
||||||
|
if (!TargetPin)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *TargetPinName, *TargetNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
const UEdGraphSchema* Schema = SourceGraph ? SourceGraph->GetSchema() : nullptr;
|
||||||
|
if (!Schema)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Graph schema not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check compatibility using the schema
|
||||||
|
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
|
||||||
|
|
||||||
|
bool bCompatible = (Response.Response != ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW);
|
||||||
|
Result->SetBoolField(TEXT("compatible"), bCompatible);
|
||||||
|
|
||||||
|
// Decode the response type
|
||||||
|
FString ResponseType;
|
||||||
|
switch (Response.Response)
|
||||||
|
{
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE:
|
||||||
|
ResponseType = TEXT("direct");
|
||||||
|
break;
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_A:
|
||||||
|
ResponseType = TEXT("breakSourceConnections");
|
||||||
|
break;
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_B:
|
||||||
|
ResponseType = TEXT("breakTargetConnections");
|
||||||
|
break;
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_BREAK_OTHERS_AB:
|
||||||
|
ResponseType = TEXT("breakBothConnections");
|
||||||
|
break;
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_CONVERSION_NODE:
|
||||||
|
ResponseType = TEXT("requiresConversion");
|
||||||
|
break;
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_MAKE_WITH_PROMOTION:
|
||||||
|
ResponseType = TEXT("requiresPromotion");
|
||||||
|
break;
|
||||||
|
case ECanCreateConnectionResponse::CONNECT_RESPONSE_DISALLOW:
|
||||||
|
default:
|
||||||
|
ResponseType = TEXT("disallowed");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Result->SetStringField(TEXT("connectionType"), ResponseType);
|
||||||
|
|
||||||
|
if (!Response.Message.IsEmpty())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("message"), Response.Message.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Include pin type info for context
|
||||||
|
Result->SetStringField(TEXT("sourcePinType"), SourcePin->PinType.PinCategory.ToString());
|
||||||
|
if (SourcePin->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
Result->SetStringField(TEXT("sourcePinSubtype"), SourcePin->PinType.PinSubCategoryObject->GetName());
|
||||||
|
Result->SetStringField(TEXT("targetPinType"), TargetPin->PinType.PinCategory.ToString());
|
||||||
|
if (TargetPin->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
Result->SetStringField(TEXT("targetPinSubtype"), TargetPin->PinType.PinSubCategoryObject->GetName());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
#include "EdGraph/EdGraphNode.h"
|
#include "EdGraph/EdGraphNode.h"
|
||||||
#include "Kismet2/BlueprintEditorUtils.h"
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
#include "Kismet2/KismetEditorUtilities.h"
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
#include "MCPHandlers_Validation.generated.h"
|
#include "UMCPHandler_CompileBlueprint.generated.h"
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_CompileMaterial.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CompileMaterial : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material name or package path"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Force recompile a material and check for compilation errors.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Load material
|
||||||
|
MCPAssets<UMaterial> Assets;
|
||||||
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterial* MaterialObj = Assets.Object();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Validating material '%s'"), *MaterialObj->GetName());
|
||||||
|
|
||||||
|
// Force recompile by triggering PreEditChange/PostEditChange
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
|
||||||
|
// Collect compilation errors
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ErrorArray;
|
||||||
|
bool bValid = true;
|
||||||
|
|
||||||
|
// Check for compilation errors via FMaterialResource on current platform
|
||||||
|
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
|
||||||
|
if (Resource)
|
||||||
|
{
|
||||||
|
const TArray<FString>& CompileErrors = Resource->GetCompileErrors();
|
||||||
|
for (const FString& Err : CompileErrors)
|
||||||
|
{
|
||||||
|
bValid = false;
|
||||||
|
ErrorArray.Add(MakeShared<FJsonValueString>(Err));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count expressions and connections
|
||||||
|
auto Expressions = MaterialObj->GetExpressions();
|
||||||
|
int32 ExprCount = Expressions.Num();
|
||||||
|
int32 ConnectionCount = 0;
|
||||||
|
if (MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
||||||
|
{
|
||||||
|
if (!Node) continue;
|
||||||
|
for (UEdGraphPin* Pin : Node->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && Pin->Direction == EGPD_Output)
|
||||||
|
{
|
||||||
|
ConnectionCount += Pin->LinkedTo.Num();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("valid"), bValid);
|
||||||
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
||||||
|
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
||||||
|
Result->SetNumberField(TEXT("expressionCount"), ExprCount);
|
||||||
|
Result->SetNumberField(TEXT("connectionCount"), ConnectionCount);
|
||||||
|
Result->SetArrayField(TEXT("errors"), ErrorArray);
|
||||||
|
Result->SetNumberField(TEXT("errorCount"), ErrorArray.Num());
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Material '%s' validation %s (%d errors)"),
|
||||||
|
*MaterialObj->GetName(), bValid ? TEXT("passed") : TEXT("failed"), ErrorArray.Num());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_ConnectBlueprintPins.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FConnectPinsEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString SourceNode;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString SourcePinName;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString TargetNode;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString TargetPinName;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ConnectBlueprintPins : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Array of {sourceNode, sourcePinName, targetNode, targetPinName} objects"))
|
||||||
|
FMCPJsonArray Connections;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Connect pins between nodes in a Blueprint graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
int32 SuccessCount = 0;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
||||||
|
|
||||||
|
FConnectPinsEntry Entry;
|
||||||
|
if (!MCPUtils::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal, &*EntryResult)) continue;
|
||||||
|
|
||||||
|
UEdGraph* SourceGraph = nullptr;
|
||||||
|
UEdGraphNode* SourceNode = MCPUtils::FindNodeByGuid(BP, Entry.SourceNode, &SourceGraph);
|
||||||
|
if (!SourceNode)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source node '%s' not found"), *Entry.SourceNode));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
|
||||||
|
if (!TargetNode)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* SourcePin = SourceNode->FindPin(FName(*Entry.SourcePinName));
|
||||||
|
if (!SourcePin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Source pin '%s' not found on node '%s'"), *Entry.SourcePinName, *Entry.SourceNode));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
||||||
|
if (!TargetPin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UEdGraphSchema* Schema = SourceGraph->GetSchema();
|
||||||
|
if (!Schema)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), TEXT("Graph schema not found"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
||||||
|
if (!bConnected)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(
|
||||||
|
TEXT("Cannot connect %s (%s) to %s (%s) — types are incompatible"),
|
||||||
|
*Entry.SourcePinName, *SourcePin->PinType.PinCategory.ToString(),
|
||||||
|
*Entry.TargetPinName, *TargetPin->PinType.PinCategory.ToString()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
SuccessCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SuccessCount > 0)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: ConnectPins — %d/%d succeeded in '%s'"),
|
||||||
|
SuccessCount, Connections.Array.Num(), *Blueprint);
|
||||||
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
||||||
|
Result->SetNumberField(TEXT("totalCount"), Connections.Array.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_ConnectMaterialExpressionPins.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ConnectMaterialExpressionPins : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the source (output) node"))
|
||||||
|
FString SourceNode;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Pin name on the source node"))
|
||||||
|
FString SourcePinName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the target (input) node"))
|
||||||
|
FString TargetNode;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Pin name on the target node"))
|
||||||
|
FString TargetPinName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Connect two pins in a material or material function graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material or material function
|
||||||
|
UMaterial* MaterialObj = nullptr;
|
||||||
|
UMaterialFunction* MatFunc = nullptr;
|
||||||
|
FString AssetDisplayName;
|
||||||
|
|
||||||
|
if (!MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialFunction> MFAssets;
|
||||||
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MatFunc = MFAssets.Object();
|
||||||
|
AssetDisplayName = MatFunc->GetName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MaterialObj = MatAssets.Object();
|
||||||
|
AssetDisplayName = MaterialObj->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
||||||
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find source and target nodes by GUID
|
||||||
|
UEdGraphNode* SourceGraphNode = nullptr;
|
||||||
|
UEdGraphNode* TargetGraphNode = nullptr;
|
||||||
|
|
||||||
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
||||||
|
{
|
||||||
|
if (!GraphNode) continue;
|
||||||
|
if (GraphNode->NodeGuid.ToString() == SourceNode)
|
||||||
|
SourceGraphNode = GraphNode;
|
||||||
|
if (GraphNode->NodeGuid.ToString() == TargetNode)
|
||||||
|
TargetGraphNode = GraphNode;
|
||||||
|
if (SourceGraphNode && TargetGraphNode)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!SourceGraphNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source node '%s' not found in material graph"), *SourceNode));
|
||||||
|
}
|
||||||
|
if (!TargetGraphNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target node '%s' not found in material graph"), *TargetNode));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find pins
|
||||||
|
UEdGraphPin* SourcePin = SourceGraphNode->FindPin(FName(*SourcePinName));
|
||||||
|
if (!SourcePin)
|
||||||
|
{
|
||||||
|
// List available pins for debugging
|
||||||
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
||||||
|
for (UEdGraphPin* P : SourceGraphNode->Pins)
|
||||||
|
{
|
||||||
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
||||||
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Source pin '%s' not found on node '%s'"),
|
||||||
|
*SourcePinName, *SourceNode));
|
||||||
|
Result->SetArrayField(TEXT("availablePins"), PinNames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* TargetPin = TargetGraphNode->FindPin(FName(*TargetPinName));
|
||||||
|
if (!TargetPin)
|
||||||
|
{
|
||||||
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
||||||
|
for (UEdGraphPin* P : TargetGraphNode->Pins)
|
||||||
|
{
|
||||||
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
||||||
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Target pin '%s' not found on node '%s'"),
|
||||||
|
*TargetPinName, *TargetNode));
|
||||||
|
Result->SetArrayField(TEXT("availablePins"), PinNames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would connect %s.%s -> %s.%s in '%s'"),
|
||||||
|
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Connecting %s.%s -> %s.%s in '%s'"),
|
||||||
|
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName, *AssetDisplayName);
|
||||||
|
|
||||||
|
// Try to connect via the schema
|
||||||
|
const UEdGraphSchema* Schema = Graph->GetSchema();
|
||||||
|
if (!Schema)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Material graph schema not found"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bConnected = Schema->TryCreateConnection(SourcePin, TargetPin);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
|
||||||
|
if (!bConnected)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot connect %s.%s to %s.%s — types may be incompatible"),
|
||||||
|
*SourceNode, *SourcePinName, *TargetNode, *TargetPinName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
||||||
|
Asset->PreEditChange(nullptr);
|
||||||
|
Asset->PostEditChange();
|
||||||
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Dom/JsonValue.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimBlueprintGeneratedClass.h"
|
||||||
|
#include "Animation/Skeleton.h"
|
||||||
|
#include "AnimGraphNode_Base.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "UMCPHandler_CreateAnimBlueprintAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateAnimBlueprintAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name for the new Animation Blueprint asset"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||||
|
FString PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
|
||||||
|
FString Skeleton;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Parent class name (default: AnimInstance)"))
|
||||||
|
FString ParentClass;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new Animation Blueprint asset with a specified skeleton.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
FString FullAssetPath = PackagePath / Name;
|
||||||
|
MCPAssets<UBlueprint> ExistCheck;
|
||||||
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
||||||
|
|
||||||
|
// Resolve skeleton
|
||||||
|
MCPAssets<USkeleton> SkeletonAssets;
|
||||||
|
if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
USkeleton* SkeletonObj = SkeletonAssets.Object();
|
||||||
|
|
||||||
|
// Resolve parent class (default: UAnimInstance)
|
||||||
|
UClass* ParentClassObj = UAnimInstance::StaticClass();
|
||||||
|
if (!ParentClass.IsEmpty() && ParentClass != TEXT("AnimInstance"))
|
||||||
|
{
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == ParentClass && It->IsChildOf(UAnimInstance::StaticClass()))
|
||||||
|
{
|
||||||
|
ParentClassObj = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating AnimBlueprint '%s' in '%s' with skeleton '%s'"),
|
||||||
|
*Name, *PackagePath, *SkeletonObj->GetName());
|
||||||
|
|
||||||
|
// Create the package
|
||||||
|
FString FullPackagePath = PackagePath / Name;
|
||||||
|
UPackage* Package = CreatePackage(*FullPackagePath);
|
||||||
|
if (!Package)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Animation Blueprint
|
||||||
|
UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>(
|
||||||
|
FKismetEditorUtilities::CreateBlueprint(
|
||||||
|
ParentClassObj,
|
||||||
|
Package,
|
||||||
|
FName(*Name),
|
||||||
|
BPTYPE_Normal,
|
||||||
|
UAnimBlueprint::StaticClass(),
|
||||||
|
UAnimBlueprintGeneratedClass::StaticClass()
|
||||||
|
));
|
||||||
|
|
||||||
|
if (!NewAnimBP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null for AnimBlueprint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set target skeleton
|
||||||
|
NewAnimBP->TargetSkeleton = SkeletonObj;
|
||||||
|
|
||||||
|
// Compile
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
|
||||||
|
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> GraphNames = MCPUtils::AllGraphNamesJson(NewAnimBP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created AnimBlueprint '%s' with %d graphs (saved: %s)"),
|
||||||
|
*Name, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
||||||
|
Result->SetStringField(TEXT("targetSkeleton"), SkeletonObj->GetName());
|
||||||
|
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Dom/JsonValue.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimBlueprintGeneratedClass.h"
|
||||||
|
#include "Animation/Skeleton.h"
|
||||||
|
#include "AnimGraphNode_Base.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "UMCPHandler_CreateBlendSpaceAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateBlendSpaceAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name for the new Blend Space asset"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||||
|
FString PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
|
||||||
|
FString Skeleton;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new 2D Blend Space asset with a specified skeleton.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required fields: name, packagePath, skeleton"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
FString FullAssetPath = PackagePath / Name;
|
||||||
|
MCPAssets<UBlendSpace> ExistCheck;
|
||||||
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
||||||
|
|
||||||
|
// Resolve skeleton
|
||||||
|
MCPAssets<USkeleton> SkeletonAssets;
|
||||||
|
if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
USkeleton* SkeletonObj = SkeletonAssets.Object();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blend Space '%s' in '%s' with skeleton '%s'"),
|
||||||
|
*Name, *PackagePath, *SkeletonObj->GetName());
|
||||||
|
|
||||||
|
// Create the package
|
||||||
|
FString FullPackagePath = PackagePath / Name;
|
||||||
|
UPackage* Package = CreatePackage(*FullPackagePath);
|
||||||
|
if (!Package)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Blend Space
|
||||||
|
UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone);
|
||||||
|
if (!NewBS)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create Blend Space object"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set skeleton
|
||||||
|
NewBS->SetSkeleton(SkeletonObj);
|
||||||
|
|
||||||
|
// Mark dirty and save
|
||||||
|
NewBS->MarkPackageDirty();
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(NewBS);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blend Space '%s' (saved: %s)"),
|
||||||
|
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
||||||
|
Result->SetStringField(TEXT("skeleton"), SkeletonObj->GetName());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,146 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_CreateBlueprintAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateBlueprintAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="New Blueprint asset name"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||||
|
FString PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)"))
|
||||||
|
FString ParentClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Blueprint type: Normal, Interface, FunctionLibrary, or MacroLibrary (default: Normal)"))
|
||||||
|
FString BlueprintType;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new Blueprint asset with a specified parent class and type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Validate packagePath starts with /Game
|
||||||
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
FString FullAssetPath = PackagePath / Blueprint;
|
||||||
|
MCPAssets<UBlueprint> ExistCheck;
|
||||||
|
if (!ExistCheck.Exact(Blueprint).Errors(Result).EAny().Info()) return;
|
||||||
|
|
||||||
|
// Resolve parent class — try C++ class first, then Blueprint
|
||||||
|
UClass* ParentClassObj = nullptr;
|
||||||
|
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == ParentClass)
|
||||||
|
{
|
||||||
|
ParentClassObj = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ParentClassObj)
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> ParentAssets;
|
||||||
|
if (!ParentAssets.Exact(ParentClass).AllContent().Errors(Result).ETwo().Load()) return;
|
||||||
|
if (!ParentAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
if (ParentAssets.Object()->GeneratedClass)
|
||||||
|
ParentClassObj = ParentAssets.Object()->GeneratedClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ParentClassObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Could not find parent class '%s'. Provide a C++ class name (e.g. 'Actor', 'Pawn') or Blueprint name."),
|
||||||
|
*ParentClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map blueprintType string to EBlueprintType
|
||||||
|
EBlueprintType BlueprintTypeEnum = BPTYPE_Normal;
|
||||||
|
if (!BlueprintType.IsEmpty())
|
||||||
|
{
|
||||||
|
if (!MCPUtils::StringToEnum(BlueprintType, BlueprintTypeEnum, Result, TEXT("BPTYPE_"))) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For Interface type, parent must be UInterface
|
||||||
|
if ((BlueprintTypeEnum == BPTYPE_Interface) && !ParentClassObj->IsChildOf(UInterface::StaticClass()))
|
||||||
|
{
|
||||||
|
// Use the engine's standard BlueprintInterface parent
|
||||||
|
ParentClassObj = UInterface::StaticClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Blueprint '%s' in '%s' with parent '%s' (type=%s)"),
|
||||||
|
*Blueprint, *PackagePath, *ParentClassObj->GetName(), *BlueprintType);
|
||||||
|
|
||||||
|
// Create the package
|
||||||
|
FString FullPackagePath = PackagePath / Blueprint;
|
||||||
|
UPackage* Package = CreatePackage(*FullPackagePath);
|
||||||
|
if (!Package)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the Blueprint
|
||||||
|
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
|
||||||
|
ParentClassObj,
|
||||||
|
Package,
|
||||||
|
FName(*Blueprint),
|
||||||
|
BlueprintTypeEnum,
|
||||||
|
UBlueprint::StaticClass(),
|
||||||
|
UBlueprintGeneratedClass::StaticClass()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!NewBP)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("FKismetEditorUtilities::CreateBlueprint returned null"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(NewBP);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(NewBP);
|
||||||
|
|
||||||
|
|
||||||
|
// Collect graph names
|
||||||
|
TArray<TSharedPtr<FJsonValue>> GraphNames = MCPUtils::AllGraphNamesJson(NewBP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Blueprint '%s' with %d graphs (saved: %s)"),
|
||||||
|
*Blueprint, GraphNames.Num(), bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("assetPath"), FullAssetPath);
|
||||||
|
Result->SetStringField(TEXT("parentClass"), ParentClassObj->GetName());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
Result->SetArrayField(TEXT("graphs"), GraphNames);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,136 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_CreateBlueprintGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateBlueprintGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name for the new graph"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent"))
|
||||||
|
FString GraphType;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new function, macro, or custom event graph in a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Invalid graphType '%s'. Valid values: function, macro, customEvent"), *GraphType));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load Blueprint
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Check graph name uniqueness
|
||||||
|
if (!MCPUtils::AllGraphsNamed(BP, Graph).IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also check for existing custom events with the same name
|
||||||
|
if (GraphType == TEXT("customEvent"))
|
||||||
|
{
|
||||||
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
if (CE->CustomFunctionName == FName(*Graph))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("A custom event named '%s' already exists in Blueprint '%s'"), *Graph, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating %s graph '%s' in Blueprint '%s'"),
|
||||||
|
*GraphType, *Graph, *Blueprint);
|
||||||
|
|
||||||
|
FString CreatedNodeId;
|
||||||
|
|
||||||
|
if (GraphType == TEXT("function"))
|
||||||
|
{
|
||||||
|
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
|
||||||
|
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||||
|
if (!NewGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create function graph"));
|
||||||
|
}
|
||||||
|
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
|
||||||
|
}
|
||||||
|
else if (GraphType == TEXT("macro"))
|
||||||
|
{
|
||||||
|
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
|
||||||
|
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
|
||||||
|
if (!NewGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create macro graph"));
|
||||||
|
}
|
||||||
|
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
|
||||||
|
}
|
||||||
|
else // customEvent
|
||||||
|
{
|
||||||
|
// Find the EventGraph (first UbergraphPage)
|
||||||
|
UEdGraph* EventGraph = nullptr;
|
||||||
|
if (BP->UbergraphPages.Num() > 0)
|
||||||
|
{
|
||||||
|
EventGraph = BP->UbergraphPages[0];
|
||||||
|
}
|
||||||
|
if (!EventGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no EventGraph to add a custom event to"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a custom event node in the EventGraph
|
||||||
|
UK2Node_CustomEvent* NewEvent = NewObject<UK2Node_CustomEvent>(EventGraph);
|
||||||
|
NewEvent->CustomFunctionName = FName(*Graph);
|
||||||
|
NewEvent->bIsEditable = true;
|
||||||
|
EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false);
|
||||||
|
NewEvent->CreateNewGuid();
|
||||||
|
NewEvent->PostPlacedNewNode();
|
||||||
|
NewEvent->AllocateDefaultPins();
|
||||||
|
CreatedNodeId = NewEvent->NodeGuid.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created %s graph '%s' in '%s' (saved: %s)"),
|
||||||
|
*GraphType, *Graph, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
if (!CreatedNodeId.IsEmpty())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("nodeId"), CreatedNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "StructUtils/UserDefinedStruct.h"
|
||||||
|
#include "Engine/UserDefinedEnum.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
|
||||||
|
#include "Kismet2/EnumEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Factories/StructureFactory.h"
|
||||||
|
#include "Factories/EnumFactory.h"
|
||||||
|
#include "UMCPHandler_CreateEnumAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateEnumAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Full package path for the new enum (e.g. '/Game/DataTypes/E_MyEnum')"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Array of enum value names"))
|
||||||
|
FMCPJsonArray Values;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new UserDefinedEnum asset with the specified values.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString PackagePath, AssetName;
|
||||||
|
if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> EnumValues;
|
||||||
|
for (const TSharedPtr<FJsonValue>& Val : Values.Array)
|
||||||
|
{
|
||||||
|
FString Str = Val->AsString();
|
||||||
|
if (!Str.IsEmpty()) EnumValues.Add(Str);
|
||||||
|
}
|
||||||
|
if (EnumValues.Num() == 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing or empty required field: values (array of strings)"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the enum using AssetTools
|
||||||
|
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
||||||
|
IAssetTools& AssetTools = AssetToolsModule.Get();
|
||||||
|
|
||||||
|
UEnumFactory* Factory = NewObject<UEnumFactory>();
|
||||||
|
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory);
|
||||||
|
|
||||||
|
if (!NewAsset)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedEnum asset"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset);
|
||||||
|
if (!NewEnum)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedEnum"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add enum values — UUserDefinedEnum starts with a MAX value.
|
||||||
|
// We need to add entries before MAX.
|
||||||
|
for (int32 i = 0; i < EnumValues.Num(); ++i)
|
||||||
|
{
|
||||||
|
// AddNewEnumeratorForUserDefinedEnum adds before the _MAX entry (returns void)
|
||||||
|
FEnumEditorUtils::AddNewEnumeratorForUserDefinedEnum(NewEnum);
|
||||||
|
// The new entry is at index (NumEnums - 2) because _MAX is last
|
||||||
|
int32 NewIndex = NewEnum->NumEnums() - 2;
|
||||||
|
FEnumEditorUtils::SetEnumeratorDisplayName(NewEnum, NewIndex, FText::FromString(EnumValues[i]));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(NewEnum);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("assetName"), AssetName);
|
||||||
|
Result->SetNumberField(TEXT("valueCount"), EnumValues.Num());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,175 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_CreateMaterialAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// SEH wrapper defined in BlueprintMCPServer.cpp — catches crashes from abstract/invalid expression classes.
|
||||||
|
// Wraps the entire creation + registration + PostEditChange flow so that if the expression crashes
|
||||||
|
// (e.g. UMaterialExpressionParameter), it cleans up and returns -1 instead of terminating the process.
|
||||||
|
#if PLATFORM_WINDOWS
|
||||||
|
extern int32 TryAddMaterialExpressionSEH(
|
||||||
|
UObject* Owner, UClass* ExprClass, UMaterial* Material, UMaterialFunction* MatFunc,
|
||||||
|
int32 PosX, int32 PosY, UMaterialExpression** OutExpr);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateMaterialAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name for the new material asset"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||||
|
FString PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"))
|
||||||
|
FString Domain;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Blend mode: Opaque, Masked, Translucent, Additive, Modulate"))
|
||||||
|
FString BlendMode;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Whether the material is two-sided"))
|
||||||
|
bool TwoSided = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new UMaterial asset with optional domain, blend mode, and two-sided settings.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
MCPAssets<UMaterial> ExistCheck;
|
||||||
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material '%s' in '%s'"), *Name, *PackagePath);
|
||||||
|
|
||||||
|
// Create via IAssetTools + factory
|
||||||
|
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||||
|
UMaterialFactoryNew* Factory = NewObject<UMaterialFactoryNew>();
|
||||||
|
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory);
|
||||||
|
|
||||||
|
if (!NewAsset)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material '%s' in '%s'"), *Name, *PackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
UMaterial* MaterialObj = Cast<UMaterial>(NewAsset);
|
||||||
|
if (!MaterialObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterial"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply optional properties
|
||||||
|
bool bHasTwoSided = Json->HasField(TEXT("twoSided"));
|
||||||
|
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
|
||||||
|
// Parse domain
|
||||||
|
if (!Domain.IsEmpty())
|
||||||
|
{
|
||||||
|
if (Domain == TEXT("Surface"))
|
||||||
|
MaterialObj->MaterialDomain = MD_Surface;
|
||||||
|
else if (Domain == TEXT("DeferredDecal"))
|
||||||
|
MaterialObj->MaterialDomain = MD_DeferredDecal;
|
||||||
|
else if (Domain == TEXT("LightFunction"))
|
||||||
|
MaterialObj->MaterialDomain = MD_LightFunction;
|
||||||
|
else if (Domain == TEXT("Volume"))
|
||||||
|
MaterialObj->MaterialDomain = MD_Volume;
|
||||||
|
else if (Domain == TEXT("PostProcess"))
|
||||||
|
MaterialObj->MaterialDomain = MD_PostProcess;
|
||||||
|
else if (Domain == TEXT("UI"))
|
||||||
|
MaterialObj->MaterialDomain = MD_UI;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse blend mode
|
||||||
|
if (!BlendMode.IsEmpty())
|
||||||
|
{
|
||||||
|
if (BlendMode == TEXT("Opaque"))
|
||||||
|
MaterialObj->BlendMode = BLEND_Opaque;
|
||||||
|
else if (BlendMode == TEXT("Masked"))
|
||||||
|
MaterialObj->BlendMode = BLEND_Masked;
|
||||||
|
else if (BlendMode == TEXT("Translucent"))
|
||||||
|
MaterialObj->BlendMode = BLEND_Translucent;
|
||||||
|
else if (BlendMode == TEXT("Additive"))
|
||||||
|
MaterialObj->BlendMode = BLEND_Additive;
|
||||||
|
else if (BlendMode == TEXT("Modulate"))
|
||||||
|
MaterialObj->BlendMode = BLEND_Modulate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bHasTwoSided)
|
||||||
|
{
|
||||||
|
MaterialObj->TwoSided = TwoSided;
|
||||||
|
}
|
||||||
|
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material '%s' (saved: %s)"),
|
||||||
|
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
|
||||||
|
Result->SetStringField(TEXT("domain"), MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_")));
|
||||||
|
Result->SetStringField(TEXT("blendMode"), MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_")));
|
||||||
|
Result->SetBoolField(TEXT("twoSided"), MaterialObj->TwoSided != 0);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_CreateMaterialFunctionAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateMaterialFunctionAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name for the new material function asset"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||||
|
FString PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Description for the material function"))
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new UMaterialFunction asset with an optional description.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
MCPAssets<UMaterialFunction> ExistCheck;
|
||||||
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Function '%s' in '%s'"), *Name, *PackagePath);
|
||||||
|
|
||||||
|
// Create via IAssetTools + factory
|
||||||
|
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||||
|
UMaterialFunctionFactoryNew* Factory = NewObject<UMaterialFunctionFactoryNew>();
|
||||||
|
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory);
|
||||||
|
|
||||||
|
if (!NewAsset)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Function '%s' in '%s'"), *Name, *PackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
|
||||||
|
if (!MF)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialFunction"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set optional description
|
||||||
|
if (!Description.IsEmpty())
|
||||||
|
{
|
||||||
|
MF->Description = Description;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(MF);
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Function '%s' (saved: %s)"),
|
||||||
|
*Name, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||||
|
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||||
|
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Engine/Texture.h"
|
||||||
|
#include "UMCPHandler_CreateMaterialInstanceAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateMaterialInstanceAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name for the new Material Instance asset"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
|
||||||
|
FString PackagePath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)"))
|
||||||
|
FString ParentMaterial;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new Material Instance Constant asset with a specified parent material.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Validate packagePath starts with /Game
|
||||||
|
if (!PackagePath.StartsWith(TEXT("/Game")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("packagePath must start with '/Game'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialInstanceConstant> ExistCheck;
|
||||||
|
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load parent material — try as Material first, then as Material Instance
|
||||||
|
UMaterialInterface* ParentMaterialObj = nullptr;
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (MatAssets.Exact(ParentMaterial).ETwo().Load() && !MatAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
ParentMaterialObj = MatAssets.Object();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialInstanceConstant> MIAssets;
|
||||||
|
if (MIAssets.Exact(ParentMaterial).ETwo().Load() && !MIAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
ParentMaterialObj = MIAssets.Object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ParentMaterialObj)
|
||||||
|
{
|
||||||
|
// Also try LoadObject as a fallback with the raw path
|
||||||
|
ParentMaterialObj = LoadObject<UMaterialInterface>(nullptr, *ParentMaterial);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ParentMaterialObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
||||||
|
*ParentMaterial));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Creating Material Instance '%s' in '%s' with parent '%s'"),
|
||||||
|
*Name, *PackagePath, *ParentMaterialObj->GetName());
|
||||||
|
|
||||||
|
// Create via factory + AssetTools
|
||||||
|
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
|
||||||
|
UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>();
|
||||||
|
|
||||||
|
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
|
||||||
|
if (!NewAsset)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to create Material Instance asset '%s' in '%s'"), *Name, *PackagePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
|
||||||
|
if (!MI)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UMaterialInstanceConstant"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set parent
|
||||||
|
MI->PreEditChange(nullptr);
|
||||||
|
MI->Parent = ParentMaterialObj;
|
||||||
|
MI->PostEditChange();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Created Material Instance '%s' with parent '%s' (saved: %s)"),
|
||||||
|
*Name, *ParentMaterialObj->GetName(), bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||||
|
Result->SetStringField(TEXT("parent"), ParentMaterialObj->GetPathName());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "StructUtils/UserDefinedStruct.h"
|
||||||
|
#include "Engine/UserDefinedEnum.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
|
||||||
|
#include "Kismet2/EnumEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Factories/StructureFactory.h"
|
||||||
|
#include "Factories/EnumFactory.h"
|
||||||
|
#include "UMCPHandler_CreateStructAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FStructPropertyEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString Type;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_CreateStructAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Full package path for the new struct (e.g. '/Game/DataTypes/S_MyStruct')"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Array of initial properties, each with 'name' and 'type' fields"))
|
||||||
|
FMCPJsonArray Properties;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Create a new UserDefinedStruct asset. "
|
||||||
|
"Optionally add initial properties via the 'properties' array (each element needs 'name' and 'type').");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString PackagePath, AssetName;
|
||||||
|
if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("assetPath must be a full path (e.g. '/Game/DataTypes/S_MyStruct')"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if asset already exists
|
||||||
|
FAssetRegistryModule& ARM = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
|
||||||
|
FAssetData ExistingAsset = ARM.Get().GetAssetByObjectPath(FSoftObjectPath(AssetPath + TEXT(".") + AssetName));
|
||||||
|
if (ExistingAsset.IsValid())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset already exists at '%s'"), *AssetPath));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the struct using the AssetTools factory
|
||||||
|
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
||||||
|
IAssetTools& AssetTools = AssetToolsModule.Get();
|
||||||
|
|
||||||
|
UStructureFactory* Factory = NewObject<UStructureFactory>();
|
||||||
|
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedStruct::StaticClass(), Factory);
|
||||||
|
|
||||||
|
if (!NewAsset)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to create UserDefinedStruct asset"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
|
||||||
|
if (!NewStruct)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Created asset is not a UserDefinedStruct"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add properties if specified
|
||||||
|
int32 PropsAdded = 0;
|
||||||
|
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
|
||||||
|
{
|
||||||
|
FStructPropertyEntry Entry;
|
||||||
|
if (!MCPUtils::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal, Result)) return;
|
||||||
|
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
|
||||||
|
|
||||||
|
FEdGraphPinType PinType;
|
||||||
|
FString TypeError;
|
||||||
|
if (!MCPUtils::ResolveTypeFromString(Entry.Type, PinType, TypeError))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Could not resolve type '%s' for property '%s': %s"), *Entry.Type, *Entry.Name, *TypeError);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Snapshot existing GUIDs so we can find the newly added one
|
||||||
|
TSet<FGuid> ExistingGuids;
|
||||||
|
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
|
||||||
|
{
|
||||||
|
ExistingGuids.Add(Var.VarGuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bAdded = FStructureEditorUtils::AddVariable(NewStruct, PinType);
|
||||||
|
if (bAdded)
|
||||||
|
{
|
||||||
|
// Find the new variable by diffing GUID sets
|
||||||
|
FGuid NewPropGuid;
|
||||||
|
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
|
||||||
|
{
|
||||||
|
if (!ExistingGuids.Contains(Var.VarGuid))
|
||||||
|
{
|
||||||
|
NewPropGuid = Var.VarGuid;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (NewPropGuid.IsValid())
|
||||||
|
{
|
||||||
|
FStructureEditorUtils::RenameVariable(NewStruct, NewPropGuid, Entry.Name);
|
||||||
|
}
|
||||||
|
PropsAdded++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(NewStruct);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("assetName"), AssetName);
|
||||||
|
Result->SetNumberField(TEXT("propertiesAdded"), PropsAdded);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "FileHelpers.h"
|
||||||
|
#include "UMCPHandler_DeleteAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DeleteAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Package path of the asset to delete"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete"))
|
||||||
|
bool Force = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Delete a .uasset after verifying no references. "
|
||||||
|
"Use force=true to skip the reference check.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
// Check if asset file exists on disk
|
||||||
|
FString PackageFilename = FPackageName::LongPackageNameToFilename(
|
||||||
|
AssetPath, FPackageName::GetAssetPackageExtension());
|
||||||
|
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
|
||||||
|
|
||||||
|
if (!IFileManager::Get().FileExists(*PackageFilename))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Asset file not found on disk: %s"), *PackageFilename));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check references
|
||||||
|
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
||||||
|
TArray<FName> Referencers;
|
||||||
|
Registry.GetReferencers(FName(*AssetPath), Referencers);
|
||||||
|
|
||||||
|
// Filter out self-references
|
||||||
|
Referencers.RemoveAll([this](const FName& Ref) {
|
||||||
|
return Ref.ToString() == AssetPath;
|
||||||
|
});
|
||||||
|
|
||||||
|
if ((Referencers.Num() > 0) && !Force)
|
||||||
|
{
|
||||||
|
// Classify references as "live" (loaded in memory) vs "stale" (only on disk)
|
||||||
|
TArray<TSharedPtr<FJsonValue>> LiveRefs;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> StaleRefs;
|
||||||
|
for (const FName& Ref : Referencers)
|
||||||
|
{
|
||||||
|
FString RefStr = Ref.ToString();
|
||||||
|
UPackage* RefPackage = FindPackage(nullptr, *RefStr);
|
||||||
|
if (RefPackage)
|
||||||
|
{
|
||||||
|
LiveRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StaleRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, TEXT("Asset is still referenced. Remove all references first."));
|
||||||
|
Result->SetStringField(TEXT("assetPath"), AssetPath);
|
||||||
|
Result->SetNumberField(TEXT("referencerCount"), Referencers.Num());
|
||||||
|
Result->SetNumberField(TEXT("liveReferencerCount"), LiveRefs.Num());
|
||||||
|
Result->SetArrayField(TEXT("liveReferencers"), LiveRefs);
|
||||||
|
Result->SetNumberField(TEXT("staleReferencerCount"), StaleRefs.Num());
|
||||||
|
Result->SetArrayField(TEXT("staleReferencers"), StaleRefs);
|
||||||
|
Result->SetStringField(TEXT("suggestion"),
|
||||||
|
StaleRefs.Num() > 0
|
||||||
|
? TEXT("Some references may be stale. Consider force=true to skip the reference check, or use change_variable_type to migrate references first.")
|
||||||
|
: TEXT("All references are live. Migrate with change_variable_type or replace_function_calls before deleting."));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force delete: unload the package from memory first
|
||||||
|
TArray<TSharedPtr<FJsonValue>> RefWarnings;
|
||||||
|
if (Force)
|
||||||
|
{
|
||||||
|
// Collect reference warnings when force-deleting with existing references
|
||||||
|
for (const FName& Ref : Referencers)
|
||||||
|
{
|
||||||
|
RefWarnings.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("Warning: '%s' still references this asset"), *Ref.ToString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
UPackage* Package = FindPackage(nullptr, *AssetPath);
|
||||||
|
if (Package)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Force-unloading package '%s' from memory"), *AssetPath);
|
||||||
|
|
||||||
|
// Collect all objects in this package
|
||||||
|
TArray<UObject*> ObjectsInPackage;
|
||||||
|
GetObjectsWithPackage(Package, ObjectsInPackage);
|
||||||
|
|
||||||
|
// Clear flags and remove from root to allow GC
|
||||||
|
for (UObject* Obj : ObjectsInPackage)
|
||||||
|
{
|
||||||
|
if (Obj)
|
||||||
|
{
|
||||||
|
Obj->ClearFlags(RF_Standalone | RF_Public);
|
||||||
|
Obj->RemoveFromRoot();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Package->ClearFlags(RF_Standalone | RF_Public);
|
||||||
|
Package->RemoveFromRoot();
|
||||||
|
|
||||||
|
// Reset loaders to release file handles
|
||||||
|
ResetLoaders(Package);
|
||||||
|
// Force garbage collection to free the objects
|
||||||
|
CollectGarbage(GARBAGE_COLLECTION_KEEPFLAGS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting asset '%s' (%s)%s"),
|
||||||
|
*AssetPath, *PackageFilename, Force ? TEXT(" [FORCE]") : TEXT(""));
|
||||||
|
|
||||||
|
// Delete the file on disk
|
||||||
|
bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true);
|
||||||
|
|
||||||
|
if (bDeleted)
|
||||||
|
{
|
||||||
|
// Trigger an asset registry rescan so it notices the deletion
|
||||||
|
TArray<FString> PathsToScan;
|
||||||
|
int32 LastSlash;
|
||||||
|
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
|
||||||
|
{
|
||||||
|
PathsToScan.Add(AssetPath.Left(LastSlash));
|
||||||
|
}
|
||||||
|
if (PathsToScan.Num() > 0)
|
||||||
|
{
|
||||||
|
Registry.ScanPathsSynchronous(PathsToScan, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("filename"), PackageFilename);
|
||||||
|
if (!bDeleted)
|
||||||
|
{
|
||||||
|
MCPUtils::MakeErrorJson(Result, TEXT("Failed to delete file from disk"));
|
||||||
|
}
|
||||||
|
if (RefWarnings.Num() > 0)
|
||||||
|
{
|
||||||
|
Result->SetArrayField(TEXT("warnings"), RefWarnings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_DeleteBlueprintGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DeleteBlueprintGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the graph to delete"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find the graph
|
||||||
|
UEdGraph* TargetGraph = nullptr;
|
||||||
|
FString GraphType;
|
||||||
|
|
||||||
|
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
|
||||||
|
{
|
||||||
|
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
TargetGraph = CandidateGraph;
|
||||||
|
GraphType = TEXT("function");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!TargetGraph)
|
||||||
|
{
|
||||||
|
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
|
||||||
|
{
|
||||||
|
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
TargetGraph = CandidateGraph;
|
||||||
|
GraphType = TEXT("macro");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
|
||||||
|
if (!TargetGraph)
|
||||||
|
{
|
||||||
|
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
|
||||||
|
{
|
||||||
|
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot delete UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be deleted."),
|
||||||
|
*Graph));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting %s graph '%s' from Blueprint '%s'"),
|
||||||
|
*GraphType, *Graph, *Blueprint);
|
||||||
|
|
||||||
|
// Count nodes for reporting
|
||||||
|
int32 NodeCount = TargetGraph->Nodes.Num();
|
||||||
|
|
||||||
|
// Remove the graph
|
||||||
|
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted graph '%s' (%d nodes), save %s"),
|
||||||
|
*Graph, NodeCount, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||||
|
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_DeleteMaterialExpression.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DeleteMaterialExpression : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the expression to delete"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove an expression node from a material or material function graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material or material function
|
||||||
|
UMaterial* MaterialObj = nullptr;
|
||||||
|
UMaterialFunction* MatFunc = nullptr;
|
||||||
|
FString AssetDisplayName;
|
||||||
|
|
||||||
|
if (!MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialFunction> MFAssets;
|
||||||
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MatFunc = MFAssets.Object();
|
||||||
|
AssetDisplayName = MatFunc->GetName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MaterialObj = MatAssets.Object();
|
||||||
|
AssetDisplayName = MaterialObj->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// For materials, we need the graph to find nodes by GUID
|
||||||
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
||||||
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the node by GUID
|
||||||
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
||||||
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
||||||
|
{
|
||||||
|
if (!GraphNode) continue;
|
||||||
|
if (GraphNode->NodeGuid.ToString() == Node)
|
||||||
|
{
|
||||||
|
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetMatNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetMatNode->MaterialExpression)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture info before deletion
|
||||||
|
FString DeletedNodeTitle = TargetMatNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
FString DeletedExprClass = TargetMatNode->MaterialExpression->GetClass()->GetName();
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would delete expression '%s' (nodeId: %s) from '%s'"),
|
||||||
|
*DeletedExprClass, *Node, *AssetDisplayName);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
|
||||||
|
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the expression
|
||||||
|
UMaterialExpression* ExprToRemove = TargetMatNode->MaterialExpression;
|
||||||
|
if (MaterialObj)
|
||||||
|
{
|
||||||
|
MaterialObj->GetExpressionCollection().RemoveExpression(ExprToRemove);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MatFunc->GetExpressionCollection().RemoveExpression(ExprToRemove);
|
||||||
|
}
|
||||||
|
ExprToRemove->MarkAsGarbage();
|
||||||
|
|
||||||
|
// Rebuild graph
|
||||||
|
Graph->NotifyGraphChanged();
|
||||||
|
|
||||||
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
||||||
|
Asset->PreEditChange(nullptr);
|
||||||
|
Asset->PostEditChange();
|
||||||
|
Asset->MarkPackageDirty();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleted expression '%s' (nodeId: %s) from '%s' (saved: %s)"),
|
||||||
|
*DeletedExprClass, *Node, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetStringField(TEXT("deletedNodeTitle"), DeletedNodeTitle);
|
||||||
|
Result->SetStringField(TEXT("deletedExpressionClass"), DeletedExprClass);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_DeleteNodeFromGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DeleteNodeFromGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Delete a node from a Blueprint graph. "
|
||||||
|
"Cannot delete entry nodes (FunctionEntry, Event, CustomEvent).");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
UEdGraph* Graph = nullptr;
|
||||||
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
||||||
|
if (!FoundNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||||
|
}
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph not found for node '%s'"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString NodeClass = FoundNode->GetClass()->GetName();
|
||||||
|
FString NodeTitle = FoundNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
FString GraphName = Graph->GetName();
|
||||||
|
|
||||||
|
// Protect root/entry nodes — deleting these leaves the graph in an invalid
|
||||||
|
// state with no root node, causing compiler errors that can't be fixed
|
||||||
|
// without recreating the entire function/event.
|
||||||
|
if (Cast<UK2Node_FunctionEntry>(FoundNode))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot delete FunctionEntry node '%s' in graph '%s'. ")
|
||||||
|
TEXT("This is the root node of the function — removing it would leave an empty, uncompilable graph. ")
|
||||||
|
TEXT("To remove the entire function, delete it from the Blueprint editor."),
|
||||||
|
*NodeTitle, *GraphName));
|
||||||
|
}
|
||||||
|
if (Cast<UK2Node_Event>(FoundNode))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot delete event entry node '%s' in graph '%s'. ")
|
||||||
|
TEXT("This is the root node of the event handler — removing it would leave an empty, uncompilable graph."),
|
||||||
|
*NodeTitle, *GraphName));
|
||||||
|
}
|
||||||
|
if (Cast<UK2Node_CustomEvent>(FoundNode))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot delete CustomEvent entry node '%s' in graph '%s'. ")
|
||||||
|
TEXT("This is the root node of the custom event — removing it would leave an empty, uncompilable graph."),
|
||||||
|
*NodeTitle, *GraphName));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Deleting node '%s' (%s) from graph '%s' in '%s'"),
|
||||||
|
*Node, *NodeTitle, *GraphName, *Blueprint);
|
||||||
|
|
||||||
|
FoundNode->BreakAllNodeLinks();
|
||||||
|
Graph->RemoveNode(FoundNode);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Node deleted"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeClass"), NodeClass);
|
||||||
|
Result->SetStringField(TEXT("nodeTitle"), NodeTitle);
|
||||||
|
Result->SetStringField(TEXT("graph"), GraphName);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_DescribeMaterialInEnglish.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DescribeMaterialInEnglish : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material name or package path"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Generate a human-readable description of a material by tracing its expression graph from the root node inputs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> Assets;
|
||||||
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterial* MaterialObj = Assets.Object();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DescribeMaterial — '%s'"), *MaterialObj->GetName());
|
||||||
|
|
||||||
|
// Ensure material graph is built
|
||||||
|
if (!MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
|
||||||
|
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
||||||
|
MaterialObj->MaterialGraph->Material = MaterialObj;
|
||||||
|
MaterialObj->MaterialGraph->RebuildGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursive helper: trace backwards from a pin and build a description string
|
||||||
|
TFunction<FString(UEdGraphPin*, int32)> TracePin = [&TracePin](UEdGraphPin* Pin, int32 Depth) -> FString
|
||||||
|
{
|
||||||
|
if (!Pin || Depth > 10)
|
||||||
|
return TEXT("(unknown)");
|
||||||
|
|
||||||
|
// If no connections, report as unconnected
|
||||||
|
if (Pin->LinkedTo.Num() == 0)
|
||||||
|
{
|
||||||
|
if (!Pin->DefaultValue.IsEmpty())
|
||||||
|
return FString::Printf(TEXT("(default: %s)"), *Pin->DefaultValue);
|
||||||
|
return TEXT("(unconnected)");
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<FString> Sources;
|
||||||
|
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (!LinkedPin || !LinkedPin->GetOwningNode()) continue;
|
||||||
|
|
||||||
|
UEdGraphNode* SourceNode = LinkedPin->GetOwningNode();
|
||||||
|
FString NodeDesc;
|
||||||
|
|
||||||
|
// Check if this is a material graph node
|
||||||
|
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode))
|
||||||
|
{
|
||||||
|
UMaterialExpression* Expr = MatNode->MaterialExpression;
|
||||||
|
if (!Expr)
|
||||||
|
{
|
||||||
|
NodeDesc = TEXT("(null expression)");
|
||||||
|
}
|
||||||
|
else if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||||
|
{
|
||||||
|
NodeDesc = FString::Printf(TEXT("ScalarParam \"%s\" (default: %.4f)"), *SP->ParameterName.ToString(), SP->DefaultValue);
|
||||||
|
}
|
||||||
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||||
|
{
|
||||||
|
NodeDesc = FString::Printf(TEXT("VectorParam \"%s\" (default: R=%.2f G=%.2f B=%.2f A=%.2f)"),
|
||||||
|
*VP->ParameterName.ToString(), VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
|
||||||
|
}
|
||||||
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||||
|
{
|
||||||
|
FString TexName = TP->Texture ? TP->Texture->GetName() : TEXT("None");
|
||||||
|
NodeDesc = FString::Printf(TEXT("TextureParam \"%s\" (%s)"), *TP->ParameterName.ToString(), *TexName);
|
||||||
|
}
|
||||||
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||||
|
{
|
||||||
|
NodeDesc = FString::Printf(TEXT("StaticSwitchParam \"%s\" (default: %s)"),
|
||||||
|
*SSP->ParameterName.ToString(), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
|
||||||
|
}
|
||||||
|
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
|
||||||
|
{
|
||||||
|
NodeDesc = FString::Printf(TEXT("Constant(%.4f)"), SC->R);
|
||||||
|
}
|
||||||
|
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
|
||||||
|
{
|
||||||
|
NodeDesc = FString::Printf(TEXT("Constant3(R=%.2f G=%.2f B=%.2f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
|
||||||
|
}
|
||||||
|
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
|
||||||
|
{
|
||||||
|
NodeDesc = FString::Printf(TEXT("Constant4(R=%.2f G=%.2f B=%.2f A=%.2f)"), C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
|
||||||
|
}
|
||||||
|
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
|
||||||
|
{
|
||||||
|
FString TexName = TS->Texture ? TS->Texture->GetName() : TEXT("None");
|
||||||
|
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
|
||||||
|
}
|
||||||
|
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
|
||||||
|
{
|
||||||
|
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetName() : TEXT("None");
|
||||||
|
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NodeDesc = Expr->GetClass()->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the source node has input pins with connections, recurse
|
||||||
|
TArray<FString> InputDescs;
|
||||||
|
for (UEdGraphPin* InputPin : SourceNode->Pins)
|
||||||
|
{
|
||||||
|
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
|
||||||
|
FString InputDesc = TracePin(InputPin, Depth + 1);
|
||||||
|
InputDescs.Add(InputDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (InputDescs.Num() > 0)
|
||||||
|
{
|
||||||
|
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Non-material node (e.g., root, comment), just use title
|
||||||
|
NodeDesc = SourceNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
Sources.Add(NodeDesc);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Sources.Num() == 1)
|
||||||
|
return Sources[0];
|
||||||
|
|
||||||
|
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find root node and trace each input
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InputDescriptions;
|
||||||
|
|
||||||
|
UMaterialGraphNode_Root* RootNode = nullptr;
|
||||||
|
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
|
||||||
|
{
|
||||||
|
RootNode = Cast<UMaterialGraphNode_Root>(Node);
|
||||||
|
if (RootNode) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!RootNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not find root node in material graph"));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UEdGraphPin* Pin : RootNode->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || Pin->Direction != EGPD_Input) continue;
|
||||||
|
|
||||||
|
FString PinName = Pin->PinName.ToString();
|
||||||
|
FString Description;
|
||||||
|
|
||||||
|
if (Pin->LinkedTo.Num() == 0)
|
||||||
|
{
|
||||||
|
Description = TEXT("(unconnected)");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Description = TracePin(Pin, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
||||||
|
InputObj->SetStringField(TEXT("input"), PinName);
|
||||||
|
InputObj->SetStringField(TEXT("chain"), Description);
|
||||||
|
InputObj->SetBoolField(TEXT("connected"), Pin->LinkedTo.Num() > 0);
|
||||||
|
InputDescriptions.Add(MakeShared<FJsonValueObject>(InputObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
||||||
|
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
||||||
|
Result->SetArrayField(TEXT("inputs"), InputDescriptions);
|
||||||
|
|
||||||
|
// Also include a compact text description
|
||||||
|
FString TextDesc;
|
||||||
|
for (const TSharedPtr<FJsonValue>& Val : InputDescriptions)
|
||||||
|
{
|
||||||
|
TSharedPtr<FJsonObject> Obj = Val->AsObject();
|
||||||
|
if (!Obj.IsValid()) continue;
|
||||||
|
FString InputName = Obj->GetStringField(TEXT("input"));
|
||||||
|
FString Chain = Obj->GetStringField(TEXT("chain"));
|
||||||
|
bool bConnected = Obj->GetBoolField(TEXT("connected"));
|
||||||
|
if (bConnected)
|
||||||
|
{
|
||||||
|
TextDesc += FString::Printf(TEXT("%s <- %s\n"), *InputName, *Chain);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!TextDesc.IsEmpty())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("description"), TextDesc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -9,7 +9,8 @@
|
|||||||
#include "EdGraph/EdGraph.h"
|
#include "EdGraph/EdGraph.h"
|
||||||
#include "EdGraph/EdGraphNode.h"
|
#include "EdGraph/EdGraphNode.h"
|
||||||
#include "EdGraph/EdGraphPin.h"
|
#include "EdGraph/EdGraphPin.h"
|
||||||
#include "MCPHandlers_DiffBlueprints.generated.h"
|
#include "UMCPHandler_DiffTwoBlueprints.generated.h"
|
||||||
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_DisconnectBlueprintPins.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FDisconnectPinEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString PinName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional))
|
||||||
|
FString TargetNode;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional))
|
||||||
|
FString TargetPinName;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DisconnectBlueprintPins : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Array of {node, pinName, targetNode?, targetPinName?} objects. If target is omitted, all connections on the pin are broken."))
|
||||||
|
FMCPJsonArray Disconnections;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Disconnect pins in a Blueprint graph. "
|
||||||
|
"Can disconnect a specific link or all links on a pin.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
int32 SuccessCount = 0;
|
||||||
|
int32 TotalDisconnected = 0;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> EntryResult = MakeShared<FJsonObject>();
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(EntryResult));
|
||||||
|
|
||||||
|
FDisconnectPinEntry Entry;
|
||||||
|
if (!MCPUtils::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal, &*EntryResult)) continue;
|
||||||
|
|
||||||
|
UEdGraphNode* Node = MCPUtils::FindNodeByGuid(BP, Entry.Node);
|
||||||
|
if (!Node)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Node '%s' not found"), *Entry.Node));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* Pin = Node->FindPin(FName(*Entry.PinName));
|
||||||
|
if (!Pin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *Entry.PinName, *Entry.Node));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 DisconnectedCount = 0;
|
||||||
|
|
||||||
|
if (!Entry.TargetNode.IsEmpty() && !Entry.TargetPinName.IsEmpty())
|
||||||
|
{
|
||||||
|
UEdGraphNode* TargetNode = MCPUtils::FindNodeByGuid(BP, Entry.TargetNode);
|
||||||
|
if (!TargetNode)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target node '%s' not found"), *Entry.TargetNode));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* TargetPin = TargetNode->FindPin(FName(*Entry.TargetPinName));
|
||||||
|
if (!TargetPin)
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), FString::Printf(TEXT("Target pin '%s' not found on node '%s'"), *Entry.TargetPinName, *Entry.TargetNode));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Pin->LinkedTo.Contains(TargetPin))
|
||||||
|
{
|
||||||
|
EntryResult->SetStringField(TEXT("error"), TEXT("The specified pins are not connected to each other"));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Pin->BreakLinkTo(TargetPin);
|
||||||
|
DisconnectedCount = 1;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DisconnectedCount = Pin->LinkedTo.Num();
|
||||||
|
if (DisconnectedCount > 0)
|
||||||
|
{
|
||||||
|
Pin->BreakAllPinLinks(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
EntryResult->SetNumberField(TEXT("disconnectedCount"), DisconnectedCount);
|
||||||
|
SuccessCount++;
|
||||||
|
TotalDisconnected += DisconnectedCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TotalDisconnected > 0)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: DisconnectPin — %d/%d succeeded, %d links broken in '%s'"),
|
||||||
|
SuccessCount, Disconnections.Array.Num(), TotalDisconnected, *Blueprint);
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("successCount"), SuccessCount);
|
||||||
|
Result->SetNumberField(TEXT("totalCount"), Disconnections.Array.Num());
|
||||||
|
Result->SetNumberField(TEXT("totalDisconnected"), TotalDisconnected);
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_DisconnectMaterialExpressionPin.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DisconnectMaterialExpressionPin : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the node whose pin to disconnect"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Pin name to disconnect"))
|
||||||
|
FString PinName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Break all connections on a specific pin in a material or material function graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material or material function
|
||||||
|
UMaterial* MaterialObj = nullptr;
|
||||||
|
UMaterialFunction* MatFunc = nullptr;
|
||||||
|
FString AssetDisplayName;
|
||||||
|
|
||||||
|
if (!MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialFunction> MFAssets;
|
||||||
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MatFunc = MFAssets.Object();
|
||||||
|
AssetDisplayName = MatFunc->GetName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MaterialObj = MatAssets.Object();
|
||||||
|
AssetDisplayName = MaterialObj->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
||||||
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find node by GUID
|
||||||
|
UEdGraphNode* TargetGraphNode = nullptr;
|
||||||
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
||||||
|
{
|
||||||
|
if (!GraphNode) continue;
|
||||||
|
if (GraphNode->NodeGuid.ToString() == Node)
|
||||||
|
{
|
||||||
|
TargetGraphNode = GraphNode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetGraphNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find pin
|
||||||
|
UEdGraphPin* Pin = TargetGraphNode->FindPin(FName(*PinName));
|
||||||
|
if (!Pin)
|
||||||
|
{
|
||||||
|
TArray<TSharedPtr<FJsonValue>> PinNames;
|
||||||
|
for (UEdGraphPin* P : TargetGraphNode->Pins)
|
||||||
|
{
|
||||||
|
if (P) PinNames.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("%s (%s)"), *P->PinName.ToString(),
|
||||||
|
P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"))));
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"),
|
||||||
|
*PinName, *Node));
|
||||||
|
Result->SetArrayField(TEXT("availablePins"), PinNames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 BrokenCount = Pin->LinkedTo.Num();
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would disconnect pin '%s' on node '%s' in '%s' (%d links)"),
|
||||||
|
*PinName, *Node, *AssetDisplayName, BrokenCount);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Disconnecting pin '%s' on node '%s' in '%s' (%d links)"),
|
||||||
|
*PinName, *Node, *AssetDisplayName, BrokenCount);
|
||||||
|
|
||||||
|
// Break all links
|
||||||
|
Pin->BreakAllPinLinks();
|
||||||
|
|
||||||
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
||||||
|
Asset->PreEditChange(nullptr);
|
||||||
|
Asset->PostEditChange();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetNumberField(TEXT("brokenLinkCount"), BrokenCount);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "K2Node_BreakStruct.h"
|
||||||
|
#include "K2Node_MakeStruct.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "UMCPHandler_DumpBlueprint.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DumpBlueprint : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Load and serialize a Blueprint, returning its full structure including graphs, variables, and components.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Tmp = MCPUtils::SerializeBlueprint(Assets.Object());
|
||||||
|
MCPUtils::CopyJsonFields(&*Tmp, Result);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "K2Node_BreakStruct.h"
|
||||||
|
#include "K2Node_MakeStruct.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "UMCPHandler_DumpBlueprintGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DumpBlueprintGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Graph name to dump"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Dump the detailed node/pin structure of a specific graph within a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// URL-decode graph name to handle spaces encoded as '+' or '%20'
|
||||||
|
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
TArray<UEdGraph*> AllGraphs = MCPUtils::AllGraphs(BP);
|
||||||
|
|
||||||
|
for (UEdGraph* GraphObj : AllGraphs)
|
||||||
|
{
|
||||||
|
if (GraphObj->GetName().Equals(DecodedGraphName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(GraphObj);
|
||||||
|
if (GraphJson.IsValid())
|
||||||
|
{
|
||||||
|
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not found — list available graphs
|
||||||
|
TArray<TSharedPtr<FJsonValue>> GraphNames;
|
||||||
|
for (UEdGraph* GraphObj : AllGraphs)
|
||||||
|
{
|
||||||
|
GraphNames.Add(MakeShared<FJsonValueString>(GraphObj->GetName()));
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||||
|
Result->SetArrayField(TEXT("availableGraphs"), GraphNames);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_DumpMaterial.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DumpMaterial : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Get detailed info about a material or material instance, including parameters, usage flags, and referenced textures.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString DecodedName = MCPUtils::UrlDecode(Material);
|
||||||
|
|
||||||
|
// Try loading as UMaterial or UMaterialInstanceConstant
|
||||||
|
MCPAssets<UMaterialInterface> Assets;
|
||||||
|
Assets.Scan(UMaterial::StaticClass());
|
||||||
|
Assets.Scan(UMaterialInstanceConstant::StaticClass());
|
||||||
|
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterialInterface* LoadedObj = Assets.Object();
|
||||||
|
|
||||||
|
if (UMaterial* MaterialObj = Cast<UMaterial>(LoadedObj))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material '%s'"), *MaterialObj->GetName());
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("name"), MaterialObj->GetName());
|
||||||
|
Result->SetStringField(TEXT("path"), MaterialObj->GetPathName());
|
||||||
|
Result->SetStringField(TEXT("type"), TEXT("Material"));
|
||||||
|
|
||||||
|
// Material domain
|
||||||
|
FString DomainStr = TEXT("Unknown");
|
||||||
|
if (const UEnum* DomainEnum = StaticEnum<EMaterialDomain>())
|
||||||
|
{
|
||||||
|
DomainStr = DomainEnum->GetNameStringByValue((int64)MaterialObj->MaterialDomain);
|
||||||
|
}
|
||||||
|
Result->SetStringField(TEXT("domain"), DomainStr);
|
||||||
|
|
||||||
|
// Blend mode
|
||||||
|
FString BlendModeStr = TEXT("Unknown");
|
||||||
|
if (const UEnum* BlendEnum = StaticEnum<EBlendMode>())
|
||||||
|
{
|
||||||
|
BlendModeStr = BlendEnum->GetNameStringByValue((int64)MaterialObj->BlendMode);
|
||||||
|
}
|
||||||
|
Result->SetStringField(TEXT("blendMode"), BlendModeStr);
|
||||||
|
|
||||||
|
// Shading models
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ShadingModels;
|
||||||
|
FMaterialShadingModelField SMField = MaterialObj->GetShadingModels();
|
||||||
|
if (const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>())
|
||||||
|
{
|
||||||
|
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
|
||||||
|
{
|
||||||
|
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
|
||||||
|
if (SMField.HasShadingModel(SM))
|
||||||
|
{
|
||||||
|
ShadingModels.Add(MakeShared<FJsonValueString>(SMEnum->GetNameStringByIndex(i)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("shadingModels"), ShadingModels);
|
||||||
|
|
||||||
|
// Two-sided
|
||||||
|
Result->SetBoolField(TEXT("twoSided"), MaterialObj->IsTwoSided());
|
||||||
|
|
||||||
|
// Expression count
|
||||||
|
auto Expressions = MaterialObj->GetExpressions();
|
||||||
|
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
||||||
|
|
||||||
|
// Parameters — iterate expressions for parameter types
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Parameters;
|
||||||
|
for (UMaterialExpression* Expr : Expressions)
|
||||||
|
{
|
||||||
|
if (!Expr) continue;
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||||
|
bool bIsParam = false;
|
||||||
|
|
||||||
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||||
|
{
|
||||||
|
bIsParam = true;
|
||||||
|
ParamObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
||||||
|
ParamObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
||||||
|
ParamObj->SetStringField(TEXT("group"), SP->Group.ToString());
|
||||||
|
ParamObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
||||||
|
}
|
||||||
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||||
|
{
|
||||||
|
bIsParam = true;
|
||||||
|
ParamObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
||||||
|
ParamObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
||||||
|
ParamObj->SetStringField(TEXT("group"), VP->Group.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);
|
||||||
|
ParamObj->SetObjectField(TEXT("defaultValue"), DefVal);
|
||||||
|
}
|
||||||
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||||
|
{
|
||||||
|
bIsParam = true;
|
||||||
|
ParamObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
||||||
|
ParamObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
||||||
|
ParamObj->SetStringField(TEXT("group"), TP->Group.ToString());
|
||||||
|
if (TP->Texture)
|
||||||
|
ParamObj->SetStringField(TEXT("defaultValue"), TP->Texture->GetPathName());
|
||||||
|
}
|
||||||
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||||
|
{
|
||||||
|
bIsParam = true;
|
||||||
|
ParamObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
||||||
|
ParamObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
||||||
|
ParamObj->SetStringField(TEXT("group"), SSP->Group.ToString());
|
||||||
|
ParamObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bIsParam)
|
||||||
|
{
|
||||||
|
Parameters.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("parameters"), Parameters);
|
||||||
|
|
||||||
|
// Referenced textures
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ReferencedTextures;
|
||||||
|
auto RefTexObjs = MaterialObj->GetReferencedTextures();
|
||||||
|
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
|
||||||
|
{
|
||||||
|
if (TexObj)
|
||||||
|
{
|
||||||
|
ReferencedTextures.Add(MakeShared<FJsonValueString>(TexObj->GetPathName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("referencedTextures"), ReferencedTextures);
|
||||||
|
|
||||||
|
// Graph node count
|
||||||
|
int32 GraphNodeCount = 0;
|
||||||
|
if (MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
GraphNodeCount = MaterialObj->MaterialGraph->Nodes.Num();
|
||||||
|
}
|
||||||
|
Result->SetNumberField(TEXT("graphNodeCount"), GraphNodeCount);
|
||||||
|
|
||||||
|
// Usage flags
|
||||||
|
TSharedRef<FJsonObject> UsageFlags = MakeShared<FJsonObject>();
|
||||||
|
UsageFlags->SetBoolField(TEXT("bUsedWithSkeletalMesh"), MaterialObj->bUsedWithSkeletalMesh != 0);
|
||||||
|
UsageFlags->SetBoolField(TEXT("bUsedWithMorphTargets"), MaterialObj->bUsedWithMorphTargets != 0);
|
||||||
|
UsageFlags->SetBoolField(TEXT("bUsedWithNiagaraSprites"), MaterialObj->bUsedWithNiagaraSprites != 0);
|
||||||
|
UsageFlags->SetBoolField(TEXT("bUsedWithParticleSprites"), MaterialObj->bUsedWithParticleSprites != 0);
|
||||||
|
UsageFlags->SetBoolField(TEXT("bUsedWithStaticLighting"), MaterialObj->bUsedWithStaticLighting != 0);
|
||||||
|
Result->SetObjectField(TEXT("usageFlags"), UsageFlags);
|
||||||
|
|
||||||
|
// Opacity mask clip value
|
||||||
|
Result->SetNumberField(TEXT("opacityMaskClipValue"), MaterialObj->OpacityMaskClipValue);
|
||||||
|
|
||||||
|
// Additional settings
|
||||||
|
Result->SetBoolField(TEXT("ditheredLODTransition"), MaterialObj->DitheredLODTransition != 0);
|
||||||
|
Result->SetBoolField(TEXT("bAllowNegativeEmissiveColor"), MaterialObj->bAllowNegativeEmissiveColor != 0);
|
||||||
|
|
||||||
|
// Texture sample count (simple expression scan)
|
||||||
|
int32 TextureSampleCount = 0;
|
||||||
|
for (UMaterialExpression* Expr : Expressions)
|
||||||
|
{
|
||||||
|
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
|
||||||
|
{
|
||||||
|
TextureSampleCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result->SetNumberField(TEXT("textureSampleCount"), TextureSampleCount);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(LoadedObj))
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterial — loaded material instance '%s'"), *MI->GetName());
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("name"), MI->GetName());
|
||||||
|
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||||
|
Result->SetStringField(TEXT("type"), TEXT("MaterialInstance"));
|
||||||
|
|
||||||
|
if (MI->Parent)
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("parent"), MI->Parent->GetName());
|
||||||
|
Result->SetStringField(TEXT("parentPath"), MI->Parent->GetPathName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Overridden parameters
|
||||||
|
TArray<TSharedPtr<FJsonValue>> OverriddenParams;
|
||||||
|
|
||||||
|
// Scalar parameters
|
||||||
|
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetStringField(TEXT("type"), TEXT("Scalar"));
|
||||||
|
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
||||||
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vector parameters
|
||||||
|
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetStringField(TEXT("type"), TEXT("Vector"));
|
||||||
|
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
|
||||||
|
Val->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
||||||
|
Val->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
||||||
|
Val->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
||||||
|
Val->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
||||||
|
PObj->SetObjectField(TEXT("value"), Val);
|
||||||
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture parameters
|
||||||
|
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetStringField(TEXT("type"), TEXT("Texture"));
|
||||||
|
if (Param.ParameterValue)
|
||||||
|
PObj->SetStringField(TEXT("value"), Param.ParameterValue->GetPathName());
|
||||||
|
else
|
||||||
|
PObj->SetStringField(TEXT("value"), TEXT("None"));
|
||||||
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Static switch parameters
|
||||||
|
for (const FStaticSwitchParameter& Param : MI->GetStaticParameters().StaticSwitchParameters)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetStringField(TEXT("type"), TEXT("StaticSwitch"));
|
||||||
|
PObj->SetBoolField(TEXT("value"), Param.Value);
|
||||||
|
PObj->SetBoolField(TEXT("overridden"), Param.bOverride);
|
||||||
|
OverriddenParams.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetArrayField(TEXT("overriddenParameters"), OverriddenParams);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Material or MaterialInstance '%s' not found. Use list_materials to see available assets."), *DecodedName));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_DumpMaterialExpressionGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DumpMaterialExpressionGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material name or package path"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Get the serialized expression graph for a material, including all nodes and connections.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString DecodedName = MCPUtils::UrlDecode(Material);
|
||||||
|
|
||||||
|
MCPAssets<UMaterial> Assets;
|
||||||
|
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterial* MaterialObj = Assets.Object();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — material '%s'"), *MaterialObj->GetName());
|
||||||
|
|
||||||
|
// Ensure the material graph is built
|
||||||
|
if (!MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialGraph — MaterialGraph is null, attempting rebuild"));
|
||||||
|
// The material graph is built lazily by the material editor; force-create it
|
||||||
|
MaterialObj->MaterialGraph = CastChecked<UMaterialGraph>(
|
||||||
|
FBlueprintEditorUtils::CreateNewGraph(MaterialObj, NAME_None, UMaterialGraph::StaticClass(), UMaterialGraphSchema::StaticClass()));
|
||||||
|
MaterialObj->MaterialGraph->Material = MaterialObj;
|
||||||
|
MaterialObj->MaterialGraph->RebuildGraph();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MaterialObj->MaterialGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not build MaterialGraph for this material"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(MaterialObj->MaterialGraph);
|
||||||
|
if (!GraphJson.IsValid())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to serialize material graph"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::CopyJsonFields(GraphJson.Get(), Result);
|
||||||
|
|
||||||
|
// Add material name context
|
||||||
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
||||||
|
Result->SetStringField(TEXT("materialPath"), MaterialObj->GetPathName());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_DumpMaterialFunction.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DumpMaterialFunction : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="MaterialFunction name or package path"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Get detailed info about a material function, including its inputs, outputs, and expressions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString DecodedName = MCPUtils::UrlDecode(MaterialFunction);
|
||||||
|
|
||||||
|
MCPAssets<UMaterialFunction> Assets;
|
||||||
|
if (!Assets.Exact(DecodedName).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterialFunction* MF = Assets.Object();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: GetMaterialFunction — '%s'"), *MF->GetName());
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("name"), MF->GetName());
|
||||||
|
Result->SetStringField(TEXT("path"), MF->GetPathName());
|
||||||
|
Result->SetStringField(TEXT("description"), MF->GetDescription());
|
||||||
|
|
||||||
|
// Expression count
|
||||||
|
auto Expressions = MF->GetExpressions();
|
||||||
|
Result->SetNumberField(TEXT("expressionCount"), Expressions.Num());
|
||||||
|
|
||||||
|
// List function inputs and outputs from expressions
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Inputs;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Outputs;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ExpressionList;
|
||||||
|
|
||||||
|
{
|
||||||
|
for (UMaterialExpression* Expr : Expressions)
|
||||||
|
{
|
||||||
|
if (!Expr) continue;
|
||||||
|
|
||||||
|
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> InputObj = MakeShared<FJsonObject>();
|
||||||
|
InputObj->SetStringField(TEXT("name"), FI->InputName.ToString());
|
||||||
|
InputObj->SetStringField(TEXT("type"), TEXT("FunctionInput"));
|
||||||
|
InputObj->SetNumberField(TEXT("posX"), FI->MaterialExpressionEditorX);
|
||||||
|
InputObj->SetNumberField(TEXT("posY"), FI->MaterialExpressionEditorY);
|
||||||
|
Inputs.Add(MakeShared<FJsonValueObject>(InputObj));
|
||||||
|
}
|
||||||
|
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> OutputObj = MakeShared<FJsonObject>();
|
||||||
|
OutputObj->SetStringField(TEXT("name"), FO->OutputName.ToString());
|
||||||
|
OutputObj->SetStringField(TEXT("type"), TEXT("FunctionOutput"));
|
||||||
|
OutputObj->SetNumberField(TEXT("posX"), FO->MaterialExpressionEditorX);
|
||||||
|
OutputObj->SetNumberField(TEXT("posY"), FO->MaterialExpressionEditorY);
|
||||||
|
Outputs.Add(MakeShared<FJsonValueObject>(OutputObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize every expression
|
||||||
|
TSharedPtr<FJsonObject> ExprJson = MCPUtils::SerializeMaterialExpression(Expr);
|
||||||
|
if (ExprJson.IsValid())
|
||||||
|
{
|
||||||
|
ExpressionList.Add(MakeShared<FJsonValueObject>(ExprJson.ToSharedRef()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetArrayField(TEXT("inputs"), Inputs);
|
||||||
|
Result->SetArrayField(TEXT("outputs"), Outputs);
|
||||||
|
Result->SetArrayField(TEXT("expressions"), ExpressionList);
|
||||||
|
|
||||||
|
// If the function has an editor graph, serialize it
|
||||||
|
UEdGraph* FuncGraph = MF->MaterialGraph;
|
||||||
|
if (FuncGraph)
|
||||||
|
{
|
||||||
|
TSharedPtr<FJsonObject> GraphJson = MCPUtils::SerializeGraph(FuncGraph);
|
||||||
|
if (GraphJson.IsValid())
|
||||||
|
{
|
||||||
|
Result->SetObjectField(TEXT("graph"), GraphJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,264 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||||
|
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||||
|
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Engine/Texture.h"
|
||||||
|
#include "UMCPHandler_DumpMaterialInstanceParameters.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DumpMaterialInstanceParameters : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material Instance name or path to inspect"))
|
||||||
|
FString MaterialInstance;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all parameters on a Material Instance, including overridden and inherited parameters.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialInstanceConstant> Assets;
|
||||||
|
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterialInstanceConstant* MI = Assets.Object();
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("name"), MI->GetName());
|
||||||
|
Result->SetStringField(TEXT("path"), MI->GetPathName());
|
||||||
|
|
||||||
|
// Parent info
|
||||||
|
if (MI->Parent)
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("parent"), MI->Parent->GetPathName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build parent chain
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ParentChainArr;
|
||||||
|
{
|
||||||
|
UMaterialInterface* Current = MI->Parent;
|
||||||
|
while (Current)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> ParentObj = MakeShared<FJsonObject>();
|
||||||
|
ParentObj->SetStringField(TEXT("name"), Current->GetName());
|
||||||
|
ParentObj->SetStringField(TEXT("path"), Current->GetPathName());
|
||||||
|
ParentObj->SetStringField(TEXT("class"), Current->GetClass()->GetName());
|
||||||
|
ParentChainArr.Add(MakeShared<FJsonValueObject>(ParentObj));
|
||||||
|
|
||||||
|
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(Current);
|
||||||
|
if (ParentMI)
|
||||||
|
{
|
||||||
|
Current = ParentMI->Parent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break; // Reached the root Material
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("parentChain"), ParentChainArr);
|
||||||
|
|
||||||
|
// Scalar parameters
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ScalarArr;
|
||||||
|
for (const FScalarParameterValue& Param : MI->ScalarParameterValues)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetNumberField(TEXT("value"), Param.ParameterValue);
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), true); // Present in ScalarParameterValues means it's overridden
|
||||||
|
ScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
||||||
|
|
||||||
|
// Vector parameters
|
||||||
|
TArray<TSharedPtr<FJsonValue>> VectorArr;
|
||||||
|
for (const FVectorParameterValue& Param : MI->VectorParameterValues)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetNumberField(TEXT("r"), Param.ParameterValue.R);
|
||||||
|
PObj->SetNumberField(TEXT("g"), Param.ParameterValue.G);
|
||||||
|
PObj->SetNumberField(TEXT("b"), Param.ParameterValue.B);
|
||||||
|
PObj->SetNumberField(TEXT("a"), Param.ParameterValue.A);
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), true);
|
||||||
|
VectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
||||||
|
|
||||||
|
// Texture parameters
|
||||||
|
TArray<TSharedPtr<FJsonValue>> TextureArr;
|
||||||
|
for (const FTextureParameterValue& Param : MI->TextureParameterValues)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
if (Param.ParameterValue)
|
||||||
|
{
|
||||||
|
PObj->SetStringField(TEXT("texture"), Param.ParameterValue->GetPathName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PObj->SetStringField(TEXT("texture"), TEXT("None"));
|
||||||
|
}
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), true);
|
||||||
|
TextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
||||||
|
|
||||||
|
// Static switch parameters
|
||||||
|
TArray<TSharedPtr<FJsonValue>> StaticSwitchArr;
|
||||||
|
{
|
||||||
|
FStaticParameterSet StaticParams;
|
||||||
|
MI->GetStaticParameterValues(StaticParams);
|
||||||
|
|
||||||
|
for (const FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), Param.ParameterInfo.Name.ToString());
|
||||||
|
PObj->SetBoolField(TEXT("value"), Param.Value);
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), Param.bOverride);
|
||||||
|
StaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
||||||
|
|
||||||
|
// Also report inherited parameters from the parent material for discoverability
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InheritedScalarArr;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InheritedVectorArr;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InheritedTextureArr;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InheritedStaticSwitchArr;
|
||||||
|
{
|
||||||
|
UMaterial* BaseMat = MI->GetMaterial();
|
||||||
|
if (BaseMat)
|
||||||
|
{
|
||||||
|
// Collect names of already-overridden parameters for filtering
|
||||||
|
TSet<FString> OverriddenScalars;
|
||||||
|
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
|
||||||
|
{
|
||||||
|
OverriddenScalars.Add(P.ParameterInfo.Name.ToString());
|
||||||
|
}
|
||||||
|
TSet<FString> OverriddenVectors;
|
||||||
|
for (const FVectorParameterValue& P : MI->VectorParameterValues)
|
||||||
|
{
|
||||||
|
OverriddenVectors.Add(P.ParameterInfo.Name.ToString());
|
||||||
|
}
|
||||||
|
TSet<FString> OverriddenTextures;
|
||||||
|
for (const FTextureParameterValue& P : MI->TextureParameterValues)
|
||||||
|
{
|
||||||
|
OverriddenTextures.Add(P.ParameterInfo.Name.ToString());
|
||||||
|
}
|
||||||
|
TSet<FString> OverriddenStaticSwitches;
|
||||||
|
{
|
||||||
|
FStaticParameterSet SP;
|
||||||
|
MI->GetStaticParameterValues(SP);
|
||||||
|
for (const FStaticSwitchParameter& P : SP.StaticSwitchParameters)
|
||||||
|
{
|
||||||
|
if (P.bOverride)
|
||||||
|
{
|
||||||
|
OverriddenStaticSwitches.Add(P.ParameterInfo.Name.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
||||||
|
{
|
||||||
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||||
|
{
|
||||||
|
if (!OverriddenScalars.Contains(SP->ParameterName.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), SP->ParameterName.ToString());
|
||||||
|
PObj->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||||
|
InheritedScalarArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||||
|
{
|
||||||
|
if (!OverriddenVectors.Contains(VP->ParameterName.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), VP->ParameterName.ToString());
|
||||||
|
PObj->SetNumberField(TEXT("r"), VP->DefaultValue.R);
|
||||||
|
PObj->SetNumberField(TEXT("g"), VP->DefaultValue.G);
|
||||||
|
PObj->SetNumberField(TEXT("b"), VP->DefaultValue.B);
|
||||||
|
PObj->SetNumberField(TEXT("a"), VP->DefaultValue.A);
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||||
|
InheritedVectorArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||||
|
{
|
||||||
|
if (!OverriddenTextures.Contains(TP->ParameterName.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), TP->ParameterName.ToString());
|
||||||
|
if (TP->Texture)
|
||||||
|
{
|
||||||
|
PObj->SetStringField(TEXT("defaultTexture"), TP->Texture->GetPathName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
PObj->SetStringField(TEXT("defaultTexture"), TEXT("None"));
|
||||||
|
}
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||||
|
InheritedTextureArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||||
|
{
|
||||||
|
if (!OverriddenStaticSwitches.Contains(SSP->ParameterName.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PObj = MakeShared<FJsonObject>();
|
||||||
|
PObj->SetStringField(TEXT("name"), SSP->ParameterName.ToString());
|
||||||
|
PObj->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
|
||||||
|
PObj->SetBoolField(TEXT("isOverridden"), false);
|
||||||
|
InheritedStaticSwitchArr.Add(MakeShared<FJsonValueObject>(PObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge inherited (non-overridden) params into the arrays
|
||||||
|
for (const TSharedPtr<FJsonValue>& V : InheritedScalarArr)
|
||||||
|
{
|
||||||
|
ScalarArr.Add(V);
|
||||||
|
}
|
||||||
|
for (const TSharedPtr<FJsonValue>& V : InheritedVectorArr)
|
||||||
|
{
|
||||||
|
VectorArr.Add(V);
|
||||||
|
}
|
||||||
|
for (const TSharedPtr<FJsonValue>& V : InheritedTextureArr)
|
||||||
|
{
|
||||||
|
TextureArr.Add(V);
|
||||||
|
}
|
||||||
|
for (const TSharedPtr<FJsonValue>& V : InheritedStaticSwitchArr)
|
||||||
|
{
|
||||||
|
StaticSwitchArr.Add(V);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update arrays with merged data
|
||||||
|
Result->SetArrayField(TEXT("scalarParameters"), ScalarArr);
|
||||||
|
Result->SetArrayField(TEXT("vectorParameters"), VectorArr);
|
||||||
|
Result->SetArrayField(TEXT("textureParameters"), TextureArr);
|
||||||
|
Result->SetArrayField(TEXT("staticSwitchParameters"), StaticSwitchArr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,200 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_DuplicateNodesInGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_DuplicateNodesInGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Array of node GUIDs to duplicate"))
|
||||||
|
FMCPJsonArray Nodes;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes"))
|
||||||
|
int32 OffsetX = 50;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes"))
|
||||||
|
int32 OffsetY = 50;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Duplicate one or more nodes in a Blueprint graph. "
|
||||||
|
"Creates copies offset from the originals with new GUIDs. "
|
||||||
|
"Connections are not preserved on the duplicates.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find the target graph
|
||||||
|
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
||||||
|
TArray<UEdGraph*> MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName);
|
||||||
|
if (MatchingGraphs.Num() == 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||||
|
}
|
||||||
|
UEdGraph* TargetGraph = MatchingGraphs[0];
|
||||||
|
|
||||||
|
if (Nodes.Array.Num() == 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("nodeIds array is empty"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find all source nodes
|
||||||
|
TArray<UEdGraphNode*> SourceNodes;
|
||||||
|
TArray<FString> NotFound;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& IdVal : Nodes.Array)
|
||||||
|
{
|
||||||
|
FString Node = IdVal->AsString();
|
||||||
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
||||||
|
if (FoundNode)
|
||||||
|
{
|
||||||
|
if (FoundNode->GetGraph() == TargetGraph)
|
||||||
|
{
|
||||||
|
SourceNodes.Add(FoundNode);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NotFound.Add(FString::Printf(TEXT("%s (in different graph)"), *Node));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
NotFound.Add(Node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SourceNodes.Num() == 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("No valid nodes found to duplicate"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicating %d node(s) in graph '%s' of '%s'"),
|
||||||
|
SourceNodes.Num(), *DecodedGraphName, *Blueprint);
|
||||||
|
|
||||||
|
// Duplicate each node
|
||||||
|
TArray<TSharedPtr<FJsonValue>> DuplicatedNodes;
|
||||||
|
TMap<FGuid, FGuid> OldToNewGuidMap;
|
||||||
|
|
||||||
|
for (UEdGraphNode* SourceNode : SourceNodes)
|
||||||
|
{
|
||||||
|
UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph);
|
||||||
|
if (!NewNode)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||||
|
Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString());
|
||||||
|
Entry->SetStringField(TEXT("error"), TEXT("DuplicateObject failed"));
|
||||||
|
DuplicatedNodes.Add(MakeShared<FJsonValueObject>(Entry));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
NewNode->CreateNewGuid();
|
||||||
|
OldToNewGuidMap.Add(SourceNode->NodeGuid, NewNode->NodeGuid);
|
||||||
|
|
||||||
|
NewNode->NodePosX += OffsetX;
|
||||||
|
NewNode->NodePosY += OffsetY;
|
||||||
|
|
||||||
|
for (UEdGraphPin* Pin : NewNode->Pins)
|
||||||
|
{
|
||||||
|
if (Pin)
|
||||||
|
{
|
||||||
|
Pin->LinkedTo.Empty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TargetGraph->AddNode(NewNode, false, false);
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||||
|
Entry->SetStringField(TEXT("sourceNodeId"), SourceNode->NodeGuid.ToString());
|
||||||
|
Entry->SetStringField(TEXT("newNodeId"), NewNode->NodeGuid.ToString());
|
||||||
|
Entry->SetNumberField(TEXT("posX"), NewNode->NodePosX);
|
||||||
|
Entry->SetNumberField(TEXT("posY"), NewNode->NodePosY);
|
||||||
|
Entry->SetStringField(TEXT("nodeClass"), NewNode->GetClass()->GetName());
|
||||||
|
Entry->SetStringField(TEXT("nodeTitle"), NewNode->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
||||||
|
DuplicatedNodes.Add(MakeShared<FJsonValueObject>(Entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Duplicated %d node(s)"),
|
||||||
|
DuplicatedNodes.Num());
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("duplicatedCount"), DuplicatedNodes.Num());
|
||||||
|
Result->SetArrayField(TEXT("nodes"), DuplicatedNodes);
|
||||||
|
|
||||||
|
if (NotFound.Num() > 0)
|
||||||
|
{
|
||||||
|
TArray<TSharedPtr<FJsonValue>> NotFoundArr;
|
||||||
|
for (const FString& NF : NotFound)
|
||||||
|
{
|
||||||
|
NotFoundArr.Add(MakeShared<FJsonValueString>(NF));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("notFound"), NotFoundArr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "K2Node_BreakStruct.h"
|
||||||
|
#include "K2Node_MakeStruct.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "UMCPHandler_FindAssetReferences.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleFindReferences — find all Blueprints referencing an asset
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_FindAssetReferences : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Asset package path to find references for"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Find all assets that reference a given asset, categorized into Blueprint and non-Blueprint referencers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
||||||
|
|
||||||
|
TArray<FName> Referencers;
|
||||||
|
Registry.GetReferencers(FName(*AssetPath), Referencers);
|
||||||
|
|
||||||
|
// Build set of known Blueprint package names for filtering
|
||||||
|
MCPAssets<UBlueprint> AllBlueprints;
|
||||||
|
AllBlueprints.Info();
|
||||||
|
TSet<FString> BlueprintPackages;
|
||||||
|
for (const FAssetData& Asset : AllBlueprints.AllData())
|
||||||
|
{
|
||||||
|
BlueprintPackages.Add(Asset.PackageName.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> BPRefs;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> OtherRefs;
|
||||||
|
for (const FName& Ref : Referencers)
|
||||||
|
{
|
||||||
|
FString RefStr = Ref.ToString();
|
||||||
|
if (BlueprintPackages.Contains(RefStr))
|
||||||
|
{
|
||||||
|
BPRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OtherRefs.Add(MakeShared<FJsonValueString>(RefStr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("totalReferencers"), Referencers.Num());
|
||||||
|
Result->SetNumberField(TEXT("blueprintReferencerCount"), BPRefs.Num());
|
||||||
|
Result->SetArrayField(TEXT("blueprintReferencers"), BPRefs);
|
||||||
|
Result->SetNumberField(TEXT("otherReferencerCount"), OtherRefs.Num());
|
||||||
|
Result->SetArrayField(TEXT("otherReferencers"), OtherRefs);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_FindMaterialReferences.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_FindMaterialReferences : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material or MaterialInstance name or package path"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Find all assets that reference a given material or material instance.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Try to find the material's package path (search both Material and MaterialInstance)
|
||||||
|
MCPAssets<UMaterial> Assets;
|
||||||
|
Assets.Scan<UMaterial>().Scan<UMaterialInstanceConstant>();
|
||||||
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Info()) return;
|
||||||
|
FString PackagePath = Assets.OneData().PackageName.ToString();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: FindMaterialReferences — '%s' (package: %s)"), *Material, *PackagePath);
|
||||||
|
|
||||||
|
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
||||||
|
|
||||||
|
TArray<FName> Referencers;
|
||||||
|
Registry.GetReferencers(FName(*PackagePath), Referencers);
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> RefArray;
|
||||||
|
for (const FName& Ref : Referencers)
|
||||||
|
{
|
||||||
|
FString RefStr = Ref.ToString();
|
||||||
|
// Skip self-reference
|
||||||
|
if (RefStr == PackagePath) continue;
|
||||||
|
RefArray.Add(MakeShared<FJsonValueString>(RefStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("packagePath"), PackagePath);
|
||||||
|
Result->SetNumberField(TEXT("totalReferencers"), RefArray.Num());
|
||||||
|
Result->SetArrayField(TEXT("referencers"), RefArray);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_GetNodeComment.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_GetNodeComment : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Get the comment text and bubble visibility of a node.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node);
|
||||||
|
if (!FoundNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("comment"), FoundNode->NodeComment);
|
||||||
|
Result->SetBoolField(TEXT("commentBubbleVisible"), FoundNode->bCommentBubbleVisible);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_GetPinDetails.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleGetPinInfo — detailed information about a specific pin
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_GetPinDetails : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node to look up (GUID)"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Pin name on the node"))
|
||||||
|
FString PinName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Get detailed information about a specific pin on a blueprint node, "
|
||||||
|
"including type, connections, and default values.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
UEdGraph* Graph = nullptr;
|
||||||
|
UEdGraphNode* FoundNode = MCPUtils::FindNodeByGuid(BP, Node, &Graph);
|
||||||
|
if (!FoundNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* Pin = FoundNode->FindPin(FName(*PinName));
|
||||||
|
if (!Pin)
|
||||||
|
{
|
||||||
|
// List available pins
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AvailPins;
|
||||||
|
for (UEdGraphPin* P : FoundNode->Pins)
|
||||||
|
{
|
||||||
|
if (P)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> PinObj = MakeShared<FJsonObject>();
|
||||||
|
PinObj->SetStringField(TEXT("name"), P->PinName.ToString());
|
||||||
|
PinObj->SetStringField(TEXT("direction"), P->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
|
||||||
|
PinObj->SetStringField(TEXT("type"), P->PinType.PinCategory.ToString());
|
||||||
|
AvailPins.Add(MakeShared<FJsonValueObject>(PinObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Pin '%s' not found on node '%s'"), *PinName, *Node));
|
||||||
|
Result->SetArrayField(TEXT("availablePins"), AvailPins);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
||||||
|
Result->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
|
||||||
|
Result->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
|
||||||
|
|
||||||
|
if (!Pin->PinType.PinSubCategory.IsNone())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("subCategory"), Pin->PinType.PinSubCategory.ToString());
|
||||||
|
}
|
||||||
|
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("isArray"), Pin->PinType.IsArray());
|
||||||
|
Result->SetBoolField(TEXT("isSet"), Pin->PinType.IsSet());
|
||||||
|
Result->SetBoolField(TEXT("isMap"), Pin->PinType.IsMap());
|
||||||
|
Result->SetBoolField(TEXT("isReference"), Pin->PinType.bIsReference);
|
||||||
|
Result->SetBoolField(TEXT("isConst"), Pin->PinType.bIsConst);
|
||||||
|
|
||||||
|
if (!Pin->DefaultValue.IsEmpty())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
|
||||||
|
}
|
||||||
|
if (!Pin->DefaultTextValue.IsEmpty())
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("defaultTextValue"), Pin->DefaultTextValue.ToString());
|
||||||
|
}
|
||||||
|
if (Pin->DefaultObject)
|
||||||
|
{
|
||||||
|
Result->SetStringField(TEXT("defaultObject"), Pin->DefaultObject->GetPathName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connected pins
|
||||||
|
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());
|
||||||
|
CJ->SetStringField(TEXT("nodeTitle"), Linked->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
|
||||||
|
Conns.Add(MakeShared<FJsonValueObject>(CJ));
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("connectedTo"), Conns);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Dom/JsonValue.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimBlueprintGeneratedClass.h"
|
||||||
|
#include "Animation/Skeleton.h"
|
||||||
|
#include "AnimGraphNode_Base.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "UMCPHandler_ListAnimSlotNames.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListAnimSlotNames : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all animation slot names used in an Animation Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Blueprint.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
// Walk all anim nodes to collect slot names
|
||||||
|
TSet<FString> SlotNames;
|
||||||
|
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
|
||||||
|
{
|
||||||
|
// Check for SlotName property via reflection
|
||||||
|
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot")))
|
||||||
|
{
|
||||||
|
FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
|
||||||
|
if (!SlotValue.IsNone())
|
||||||
|
{
|
||||||
|
SlotNames.Add(SlotValue.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> SlotsArr;
|
||||||
|
for (const FString& Slot : SlotNames)
|
||||||
|
{
|
||||||
|
SlotsArr.Add(MakeShared<FJsonValueString>(Slot));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetArrayField(TEXT("slots"), SlotsArr);
|
||||||
|
Result->SetNumberField(TEXT("count"), SlotsArr.Num());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Dom/JsonValue.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimBlueprintGeneratedClass.h"
|
||||||
|
#include "Animation/Skeleton.h"
|
||||||
|
#include "AnimGraphNode_Base.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "UMCPHandler_ListAnimSyncGroups.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListAnimSyncGroups : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all sync group names used in an Animation Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Blueprint.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blueprint"));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
// Walk all anim nodes to collect sync group names
|
||||||
|
TSet<FString> SyncGroupNames;
|
||||||
|
for (UAnimGraphNode_Base* AnimNode : MCPUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
|
||||||
|
{
|
||||||
|
// Check for SyncGroup/GroupName property via reflection
|
||||||
|
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
if (PropIt->GetName().Contains(TEXT("SyncGroup")) || PropIt->GetName().Contains(TEXT("GroupName")))
|
||||||
|
{
|
||||||
|
FName GroupValue = PropIt->GetPropertyValue_InContainer(AnimNode);
|
||||||
|
if (!GroupValue.IsNone())
|
||||||
|
{
|
||||||
|
SyncGroupNames.Add(GroupValue.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> GroupsArr;
|
||||||
|
for (const FString& Group : SyncGroupNames)
|
||||||
|
{
|
||||||
|
GroupsArr.Add(MakeShared<FJsonValueString>(Group));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetArrayField(TEXT("syncGroups"), GroupsArr);
|
||||||
|
Result->SetNumberField(TEXT("count"), GroupsArr.Num());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "K2Node_BreakStruct.h"
|
||||||
|
#include "K2Node_MakeStruct.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "UMCPHandler_ListBlueprintAssets.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListBlueprintAssets : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Filter by parent class name (substring match)"))
|
||||||
|
FString ParentClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
|
||||||
|
bool IncludeRegular = true;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Include regular blueprints (default true)"))
|
||||||
|
bool IncludeLevel = true;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all Blueprint assets in the project, with optional filtering by name, parent class, or type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UObject> Assets;
|
||||||
|
Assets.NoScans().Substring(Filter).Limit(500);
|
||||||
|
if (IncludeRegular) Assets.Scan<UBlueprint>();
|
||||||
|
if (IncludeLevel) Assets.Scan<UWorld>();
|
||||||
|
Assets.Info();
|
||||||
|
for (const FAssetData& Asset : Assets.AllData())
|
||||||
|
{
|
||||||
|
// Extract parent class name
|
||||||
|
FString ParentClassName;
|
||||||
|
if (Asset.AssetClassPath == UWorld::StaticClass()->GetClassPathName())
|
||||||
|
{
|
||||||
|
ParentClassName = TEXT("LevelScriptActor");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset.GetTagValue(FName(TEXT("ParentClass")), ParentClassName);
|
||||||
|
int32 DotIndex;
|
||||||
|
if (ParentClassName.FindLastChar('.', DotIndex))
|
||||||
|
{
|
||||||
|
ParentClassName = ParentClassName.Mid(DotIndex + 1);
|
||||||
|
}
|
||||||
|
ParentClassName.RemoveFromEnd(TEXT("'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply parent class filter
|
||||||
|
if (!ParentClass.IsEmpty())
|
||||||
|
{
|
||||||
|
if (!ParentClassName.Equals(ParentClass, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.Appendf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/SimpleConstructionScript.h"
|
||||||
|
#include "Engine/SCS_Node.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_ListBlueprintComponents.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListBlueprintComponents : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all components in a Blueprint's SimpleConstructionScript, "
|
||||||
|
"including hierarchy and scene root information.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||||
|
if (!SCS)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||||
|
*Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ComponentsArr;
|
||||||
|
for (USCS_Node* Node : AllNodes)
|
||||||
|
{
|
||||||
|
if (!Node)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> CompObj = MakeShared<FJsonObject>();
|
||||||
|
CompObj->SetStringField(TEXT("name"), Node->GetVariableName().ToString());
|
||||||
|
|
||||||
|
if (Node->ComponentClass)
|
||||||
|
{
|
||||||
|
CompObj->SetStringField(TEXT("componentClass"), Node->ComponentClass->GetName());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CompObj->SetStringField(TEXT("componentClass"), TEXT("None"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parent component info
|
||||||
|
USCS_Node* ParentNode = nullptr;
|
||||||
|
for (USCS_Node* Candidate : AllNodes)
|
||||||
|
{
|
||||||
|
if (Candidate && Candidate->GetChildNodes().Contains(Node))
|
||||||
|
{
|
||||||
|
ParentNode = Candidate;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ParentNode)
|
||||||
|
{
|
||||||
|
CompObj->SetStringField(TEXT("parentComponent"), ParentNode->GetVariableName().ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this is a default scene root (first root node with SceneComponent class)
|
||||||
|
bool bIsSceneRoot = false;
|
||||||
|
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
||||||
|
if (RootNodes.Num() > 0 && RootNodes[0] == Node)
|
||||||
|
{
|
||||||
|
bIsSceneRoot = true;
|
||||||
|
}
|
||||||
|
CompObj->SetBoolField(TEXT("isSceneRoot"), bIsSceneRoot);
|
||||||
|
|
||||||
|
// List child count for informational purposes
|
||||||
|
CompObj->SetNumberField(TEXT("childCount"), Node->GetChildNodes().Num());
|
||||||
|
|
||||||
|
ComponentsArr.Add(MakeShared<FJsonValueObject>(CompObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), ComponentsArr.Num());
|
||||||
|
Result->SetArrayField(TEXT("components"), ComponentsArr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_ListBlueprintInterfaces.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListBlueprintInterfaces : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all Blueprint Interfaces implemented by a Blueprint, "
|
||||||
|
"including their function graphs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> InterfacesArr;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (!IfaceDesc.Interface)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> IfaceObj = MakeShared<FJsonObject>();
|
||||||
|
IfaceObj->SetStringField(TEXT("name"), IfaceDesc.Interface->GetName());
|
||||||
|
IfaceObj->SetStringField(TEXT("classPath"), IfaceDesc.Interface->GetPathName());
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> FuncArr;
|
||||||
|
for (const UEdGraph* Graph : IfaceDesc.Graphs)
|
||||||
|
{
|
||||||
|
if (Graph)
|
||||||
|
{
|
||||||
|
FuncArr.Add(MakeShared<FJsonValueString>(Graph->GetName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
IfaceObj->SetArrayField(TEXT("functions"), FuncArr);
|
||||||
|
|
||||||
|
InterfacesArr.Add(MakeShared<FJsonValueObject>(IfaceObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), InterfacesArr.Num());
|
||||||
|
Result->SetArrayField(TEXT("interfaces"), InterfacesArr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_ListClassFunctions.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleListFunctions — list Blueprint-callable functions on a class
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListClassFunctions : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Class name to list functions for"))
|
||||||
|
FString ClassName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Substring filter for function names"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List Blueprint-callable functions on a UClass, including parameter info and flags.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Find the class
|
||||||
|
UClass* FoundClass = nullptr;
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
|
||||||
|
{
|
||||||
|
FoundClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!FoundClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName));
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> FuncList;
|
||||||
|
|
||||||
|
for (TFieldIterator<UFunction> FuncIt(FoundClass); FuncIt; ++FuncIt)
|
||||||
|
{
|
||||||
|
UFunction* Func = *FuncIt;
|
||||||
|
if (!Func) continue;
|
||||||
|
|
||||||
|
// Only include Blueprint-callable functions
|
||||||
|
if (!Func->HasAnyFunctionFlags(FUNC_BlueprintCallable | FUNC_BlueprintPure | FUNC_BlueprintEvent)) continue;
|
||||||
|
|
||||||
|
FString FuncName = Func->GetName();
|
||||||
|
|
||||||
|
// Apply filter
|
||||||
|
if (!Filter.IsEmpty() && !FuncName.Contains(Filter, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> FuncObj = MakeShared<FJsonObject>();
|
||||||
|
FuncObj->SetStringField(TEXT("name"), FuncName);
|
||||||
|
|
||||||
|
// Determine the owning class
|
||||||
|
UClass* OwnerClass = Func->GetOwnerClass();
|
||||||
|
if (OwnerClass)
|
||||||
|
{
|
||||||
|
FuncObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function flags
|
||||||
|
FuncObj->SetBoolField(TEXT("isPure"), Func->HasAnyFunctionFlags(FUNC_BlueprintPure));
|
||||||
|
FuncObj->SetBoolField(TEXT("isStatic"), Func->HasAnyFunctionFlags(FUNC_Static));
|
||||||
|
FuncObj->SetBoolField(TEXT("isEvent"), Func->HasAnyFunctionFlags(FUNC_BlueprintEvent));
|
||||||
|
FuncObj->SetBoolField(TEXT("isConst"), Func->HasAnyFunctionFlags(FUNC_Const));
|
||||||
|
|
||||||
|
// Parameters
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Params;
|
||||||
|
FString ReturnType;
|
||||||
|
for (TFieldIterator<FProperty> PropIt(Func); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
FProperty* Prop = *PropIt;
|
||||||
|
if (!Prop) continue;
|
||||||
|
|
||||||
|
FString PropType = Prop->GetCPPType();
|
||||||
|
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_ReturnParm))
|
||||||
|
{
|
||||||
|
ReturnType = PropType;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_Parm))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||||
|
ParamObj->SetStringField(TEXT("name"), Prop->GetName());
|
||||||
|
ParamObj->SetStringField(TEXT("type"), PropType);
|
||||||
|
ParamObj->SetBoolField(TEXT("isOutput"), Prop->HasAnyPropertyFlags(CPF_OutParm) && !Prop->HasAnyPropertyFlags(CPF_ReferenceParm));
|
||||||
|
ParamObj->SetBoolField(TEXT("isReference"), Prop->HasAnyPropertyFlags(CPF_ReferenceParm));
|
||||||
|
Params.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FuncObj->SetArrayField(TEXT("parameters"), Params);
|
||||||
|
if (!ReturnType.IsEmpty())
|
||||||
|
{
|
||||||
|
FuncObj->SetStringField(TEXT("returnType"), ReturnType);
|
||||||
|
}
|
||||||
|
|
||||||
|
FuncList.Add(MakeShared<FJsonValueObject>(FuncObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("className"), FoundClass->GetName());
|
||||||
|
Result->SetNumberField(TEXT("count"), FuncList.Num());
|
||||||
|
Result->SetArrayField(TEXT("functions"), FuncList);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_ListClassProperties.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleListProperties — list properties on a class
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListClassProperties : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Class name to list properties for"))
|
||||||
|
FString ClassName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List properties on a UClass, including type, owning class, and property flags.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Find the class
|
||||||
|
UClass* FoundClass = nullptr;
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == ClassName || It->GetName() == ClassName + TEXT("_C"))
|
||||||
|
{
|
||||||
|
FoundClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!FoundClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Class '%s' not found"), *ClassName));
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> PropList;
|
||||||
|
|
||||||
|
for (TFieldIterator<FProperty> PropIt(FoundClass); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
FProperty* Prop = *PropIt;
|
||||||
|
if (!Prop) continue;
|
||||||
|
|
||||||
|
FString PropName = Prop->GetName();
|
||||||
|
|
||||||
|
// Apply filter
|
||||||
|
if (!Filter.IsEmpty() && !PropName.Contains(Filter, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> PropObj = MakeShared<FJsonObject>();
|
||||||
|
PropObj->SetStringField(TEXT("name"), PropName);
|
||||||
|
PropObj->SetStringField(TEXT("type"), Prop->GetCPPType());
|
||||||
|
|
||||||
|
// Determine the owning class
|
||||||
|
UClass* OwnerClass = Prop->GetOwnerClass();
|
||||||
|
if (OwnerClass)
|
||||||
|
{
|
||||||
|
PropObj->SetStringField(TEXT("definedIn"), OwnerClass->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Property flags
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Flags;
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Add(MakeShared<FJsonValueString>(TEXT("BlueprintVisible")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Add(MakeShared<FJsonValueString>(TEXT("BlueprintReadOnly")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Add(MakeShared<FJsonValueString>(TEXT("EditAnywhere")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Add(MakeShared<FJsonValueString>(TEXT("VisibleOnly")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Config")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Add(MakeShared<FJsonValueString>(TEXT("SaveGame")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Transient")));
|
||||||
|
if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Add(MakeShared<FJsonValueString>(TEXT("RepNotify")));
|
||||||
|
if (Flags.Num() > 0)
|
||||||
|
{
|
||||||
|
PropObj->SetArrayField(TEXT("flags"), Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
PropList.Add(MakeShared<FJsonValueObject>(PropObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("className"), FoundClass->GetName());
|
||||||
|
Result->SetNumberField(TEXT("count"), PropList.Num());
|
||||||
|
Result->SetArrayField(TEXT("properties"), PropList);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_ListEventDispatchers.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListEventDispatchers : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
TSet<FName> DelegateNameSet;
|
||||||
|
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet);
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> DispatchersArr;
|
||||||
|
|
||||||
|
for (const FName& DelegateName : DelegateNameSet)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> DispObj = MakeShared<FJsonObject>();
|
||||||
|
DispObj->SetStringField(TEXT("name"), DelegateName.ToString());
|
||||||
|
|
||||||
|
// Get parameter info from the signature graph
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ParamsArr;
|
||||||
|
|
||||||
|
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
|
||||||
|
if (SigGraph)
|
||||||
|
{
|
||||||
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(SigGraph))
|
||||||
|
{
|
||||||
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (!PinInfo.IsValid()) continue;
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> ParamObj = MakeShared<FJsonObject>();
|
||||||
|
ParamObj->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
|
||||||
|
|
||||||
|
// Build a human-readable type name from the pin type
|
||||||
|
FString TypeStr = PinInfo->PinType.PinCategory.ToString();
|
||||||
|
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
{
|
||||||
|
TypeStr = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||||
|
}
|
||||||
|
ParamObj->SetStringField(TEXT("type"), TypeStr);
|
||||||
|
|
||||||
|
ParamsArr.Add(MakeShared<FJsonValueObject>(ParamObj));
|
||||||
|
}
|
||||||
|
break; // only need the first entry node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DispObj->SetArrayField(TEXT("parameters"), ParamsArr);
|
||||||
|
DispatchersArr.Add(MakeShared<FJsonValueObject>(DispObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), DispatchersArr.Num());
|
||||||
|
Result->SetArrayField(TEXT("dispatchers"), DispatchersArr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_ListMaterialAssets.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListMaterialAssets : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Filter string to match against material name or path"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Type filter: 'all', 'material', or 'instance'"))
|
||||||
|
FString Type;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List Material and MaterialInstance assets, optionally filtered by name and type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
bool bIncludeMaterials = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("material");
|
||||||
|
bool bIncludeInstances = Type.IsEmpty() || Type == TEXT("all") || Type == TEXT("instance");
|
||||||
|
|
||||||
|
MCPAssets<UMaterial> Assets;
|
||||||
|
if (bIncludeMaterials) Assets.Scan(UMaterial::StaticClass());
|
||||||
|
if (bIncludeInstances) Assets.Scan(UMaterialInstanceConstant::StaticClass());
|
||||||
|
Assets.Substring(Filter).NoDerived().Info();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||||
|
for (const FAssetData& Asset : Assets.AllData())
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||||
|
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
|
||||||
|
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
|
||||||
|
Entry->SetStringField(TEXT("type"),
|
||||||
|
Asset.AssetClassPath.GetAssetName() == TEXT("MaterialInstanceConstant")
|
||||||
|
? TEXT("MaterialInstance") : TEXT("Material"));
|
||||||
|
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||||
|
Result->SetArrayField(TEXT("materials"), Entries);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_ListMaterialFunctionAssets.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ListMaterialFunctionAssets : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Filter string to match against function name or path"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("List MaterialFunction assets, optionally filtered by name or path.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialFunction> Assets;
|
||||||
|
Assets.Substring(Filter).Info();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Entries;
|
||||||
|
for (const FAssetData& Asset : Assets.AllData())
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> Entry = MakeShared<FJsonObject>();
|
||||||
|
Entry->SetStringField(TEXT("name"), Asset.AssetName.ToString());
|
||||||
|
Entry->SetStringField(TEXT("path"), Asset.PackageName.ToString());
|
||||||
|
Entries.Add(MakeShared<FJsonValueObject>(Entry));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), Entries.Num());
|
||||||
|
Result->SetArrayField(TEXT("functions"), Entries);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_RefreshAllNodesInGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RefreshAllNodesInGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. "
|
||||||
|
"Reports compiler warnings and errors.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
// Load Blueprint
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Count graphs and nodes before refresh
|
||||||
|
int32 GraphCount = MCPUtils::AllGraphs(BP).Num();
|
||||||
|
int32 NodeCount = MCPUtils::AllNodes(BP).Num();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Refreshing all nodes in '%s' (%d graphs, %d nodes)"),
|
||||||
|
*Blueprint, GraphCount, NodeCount);
|
||||||
|
|
||||||
|
// Refresh all nodes
|
||||||
|
FBlueprintEditorUtils::RefreshAllNodes(BP);
|
||||||
|
|
||||||
|
// Remove orphaned pins from all nodes
|
||||||
|
int32 OrphanedPinsRemoved = 0;
|
||||||
|
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
|
||||||
|
{
|
||||||
|
for (int32 i = Node->Pins.Num() - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
UEdGraphPin* Pin = Node->Pins[i];
|
||||||
|
if (Pin && Pin->bOrphanedPin)
|
||||||
|
{
|
||||||
|
Pin->BreakAllPinLinks();
|
||||||
|
Node->Pins.RemoveAt(i);
|
||||||
|
OrphanedPinsRemoved++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OrphanedPinsRemoved > 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed %d orphaned pins"), OrphanedPinsRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark as modified and recompile after orphan removal
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: RefreshAllNodes complete"));
|
||||||
|
|
||||||
|
// Collect compiler warnings and errors from the blueprint status
|
||||||
|
TArray<TSharedPtr<FJsonValue>> WarningsArr;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ErrorsArr;
|
||||||
|
|
||||||
|
if (BP->Status == BS_Error)
|
||||||
|
{
|
||||||
|
ErrorsArr.Add(MakeShared<FJsonValueString>(TEXT("Blueprint has compiler errors after refresh")));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each graph for nodes with error/warning status
|
||||||
|
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
|
||||||
|
{
|
||||||
|
if (Node->bHasCompilerMessage)
|
||||||
|
{
|
||||||
|
FString NodeTitle = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
FString NodeMsg = FString::Printf(TEXT("[%s] %s: %s"),
|
||||||
|
*Node->GetGraph()->GetName(), *NodeTitle, *Node->ErrorMsg);
|
||||||
|
if (Node->ErrorType == EMessageSeverity::Error)
|
||||||
|
{
|
||||||
|
ErrorsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WarningsArr.Add(MakeShared<FJsonValueString>(NodeMsg));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("graphCount"), GraphCount);
|
||||||
|
Result->SetNumberField(TEXT("nodeCount"), NodeCount);
|
||||||
|
Result->SetNumberField(TEXT("orphanedPinsRemoved"), OrphanedPinsRemoved);
|
||||||
|
Result->SetArrayField(TEXT("warnings"), WarningsArr);
|
||||||
|
Result->SetArrayField(TEXT("errors"), ErrorsArr);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
|
#include "AnimStateNode.h"
|
||||||
|
#include "AnimStateTransitionNode.h"
|
||||||
|
#include "AnimationStateMachineGraph.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "UMCPHandler_RemoveAnimStateFromMachine.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RemoveAnimStateFromMachine : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="State machine graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the state to remove"))
|
||||||
|
FString StateName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove a state and its connected transitions from an animation state machine graph.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
||||||
|
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||||
|
if (!StateNode) return;
|
||||||
|
|
||||||
|
// Collect and remove transitions connected to this state
|
||||||
|
TArray<UAnimStateTransitionNode*> TransitionsToRemove;
|
||||||
|
for (UEdGraphNode* Node : SMGraph->Nodes)
|
||||||
|
{
|
||||||
|
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
|
||||||
|
{
|
||||||
|
if ((TransNode->GetPreviousState() == StateNode) || (TransNode->GetNextState() == StateNode))
|
||||||
|
{
|
||||||
|
TransitionsToRemove.Add(TransNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int32 RemovedTransitions = TransitionsToRemove.Num();
|
||||||
|
for (UAnimStateTransitionNode* Trans : TransitionsToRemove)
|
||||||
|
{
|
||||||
|
Trans->BreakAllNodeLinks();
|
||||||
|
SMGraph->RemoveNode(Trans);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the state
|
||||||
|
StateNode->BreakAllNodeLinks();
|
||||||
|
SMGraph->RemoveNode(StateNode);
|
||||||
|
|
||||||
|
// Compile and save
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("removedTransitions"), RemovedTransitions);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/SimpleConstructionScript.h"
|
||||||
|
#include "Engine/SCS_Node.h"
|
||||||
|
#include "Components/ActorComponent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_RemoveBlueprintComponent.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RemoveBlueprintComponent : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Component to remove"))
|
||||||
|
FString Component;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove a component from a Blueprint's SimpleConstructionScript.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
|
||||||
|
if (!SCS)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)"),
|
||||||
|
*Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the node to remove
|
||||||
|
USCS_Node* NodeToRemove = nullptr;
|
||||||
|
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
|
||||||
|
for (USCS_Node* Node : AllNodes)
|
||||||
|
{
|
||||||
|
if (Node && Node->GetVariableName().ToString().Equals(Component, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
NodeToRemove = Node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NodeToRemove)
|
||||||
|
{
|
||||||
|
// Build list of component names for the error message
|
||||||
|
TArray<TSharedPtr<FJsonValue>> CompList;
|
||||||
|
for (USCS_Node* Node : AllNodes)
|
||||||
|
{
|
||||||
|
if (Node)
|
||||||
|
{
|
||||||
|
CompList.Add(MakeShared<FJsonValueString>(Node->GetVariableName().ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Component '%s' not found in Blueprint '%s'"),
|
||||||
|
*Component, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("existingComponents"), CompList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent removing the root scene component if it has children
|
||||||
|
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
|
||||||
|
if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot remove component '%s' because it is a root component with %d child(ren). "
|
||||||
|
"Remove or re-parent the children first."),
|
||||||
|
*Component, NodeToRemove->GetChildNodes().Num()));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing component '%s' from Blueprint '%s'"),
|
||||||
|
*Component, *Blueprint);
|
||||||
|
|
||||||
|
// Remove the node (promotes children to parent if it has any — but we've guarded root above)
|
||||||
|
SCS->RemoveNodeAndPromoteChildren(NodeToRemove);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed component '%s' from '%s' (saved: %s)"),
|
||||||
|
*Component, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_RemoveBlueprintInterface.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RemoveBlueprintInterface : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Interface name to remove"))
|
||||||
|
FString InterfaceName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions"))
|
||||||
|
bool PreserveFunctions = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove a Blueprint Interface implementation from a Blueprint. "
|
||||||
|
"Optionally preserve the function graphs as regular functions.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find the interface in ImplementedInterfaces by name (case-insensitive)
|
||||||
|
UClass* FoundInterface = nullptr;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (!IfaceDesc.Interface)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ClassName = IfaceDesc.Interface->GetName();
|
||||||
|
if (ClassName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
FoundInterface = IfaceDesc.Interface;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// Strip "_C" suffix for comparison
|
||||||
|
FString TrimmedName = ClassName;
|
||||||
|
if (TrimmedName.EndsWith(TEXT("_C")))
|
||||||
|
{
|
||||||
|
TrimmedName = TrimmedName.LeftChop(2);
|
||||||
|
}
|
||||||
|
if (TrimmedName.Equals(InterfaceName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
FoundInterface = IfaceDesc.Interface;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!FoundInterface)
|
||||||
|
{
|
||||||
|
// Build helpful error with list of implemented interfaces
|
||||||
|
TArray<TSharedPtr<FJsonValue>> IfaceList;
|
||||||
|
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
|
||||||
|
{
|
||||||
|
if (IfaceDesc.Interface)
|
||||||
|
{
|
||||||
|
IfaceList.Add(MakeShared<FJsonValueString>(IfaceDesc.Interface->GetName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Interface '%s' is not implemented by Blueprint '%s'"),
|
||||||
|
*InterfaceName, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("implementedInterfaces"), IfaceList);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing interface '%s' from Blueprint '%s' (preserveFunctions: %s)"),
|
||||||
|
*FoundInterface->GetName(), *Blueprint, PreserveFunctions ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed interface '%s' from '%s'"),
|
||||||
|
*FoundInterface->GetName(), *Blueprint);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("interfaceName"), FoundInterface->GetName());
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_RemoveBlueprintVariable.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RemoveBlueprintVariable : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the variable to remove"))
|
||||||
|
FString VariableName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove a member variable from a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find variable by name (case-insensitive)
|
||||||
|
FName VarFName(*VariableName);
|
||||||
|
bool bVarFound = false;
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName.ToString().Equals(VariableName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
VarFName = Var.VarName; // Use the exact name found
|
||||||
|
bVarFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bVarFound)
|
||||||
|
{
|
||||||
|
// Build available variables list for helpful error message
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AvailVars;
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
AvailVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Variable '%s' not found in Blueprint '%s'"), *VariableName, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("availableVariables"), AvailVars);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing variable '%s' from Blueprint '%s'"),
|
||||||
|
*VariableName, *Blueprint);
|
||||||
|
|
||||||
|
// Use the editor utility to remove the variable (also cleans up Get/Set nodes)
|
||||||
|
FBlueprintEditorUtils::RemoveMemberVariable(BP, VarFName);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removed variable '%s' from '%s' (saved: %s)"),
|
||||||
|
*VariableName, *Blueprint, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_RemoveFunctionParameter.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RemoveFunctionParameter : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the function or custom event"))
|
||||||
|
FString FunctionName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the parameter to remove"))
|
||||||
|
FString ParamName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove a parameter from a function or custom event in a Blueprint.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find the entry node
|
||||||
|
UK2Node_EditablePinBase* EntryNode = nullptr;
|
||||||
|
FString FoundNodeType;
|
||||||
|
|
||||||
|
// Strategy 1: Look for a K2Node_FunctionEntry in a function graph matching the name
|
||||||
|
for (UK2Node_FunctionEntry* FuncEntry : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
||||||
|
{
|
||||||
|
if (FuncEntry->GetGraph()->GetName().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
EntryNode = FuncEntry;
|
||||||
|
FoundNodeType = TEXT("FunctionEntry");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: Search for a K2Node_CustomEvent with matching CustomFunctionName
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
for (UK2Node_CustomEvent* CustomEvent : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
if (CustomEvent->CustomFunctionName.ToString().Equals(FunctionName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
EntryNode = CustomEvent;
|
||||||
|
FoundNodeType = TEXT("CustomEvent");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EntryNode)
|
||||||
|
{
|
||||||
|
// List available functions/events for debugging
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Available;
|
||||||
|
for (UK2Node_FunctionEntry* FE : MCPUtils::AllNodes<UK2Node_FunctionEntry>(BP))
|
||||||
|
{
|
||||||
|
Available.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("function:%s"), *FE->GetGraph()->GetName())));
|
||||||
|
}
|
||||||
|
for (UK2Node_CustomEvent* CE : MCPUtils::AllNodes<UK2Node_CustomEvent>(BP))
|
||||||
|
{
|
||||||
|
Available.Add(MakeShared<FJsonValueString>(
|
||||||
|
FString::Printf(TEXT("event:%s"), *CE->CustomFunctionName.ToString())));
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Function or custom event '%s' not found in Blueprint '%s'"),
|
||||||
|
*FunctionName, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("availableFunctionsAndEvents"), Available);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and remove the UserDefinedPin matching paramName
|
||||||
|
int32 RemovedIndex = INDEX_NONE;
|
||||||
|
for (int32 i = 0; i < EntryNode->UserDefinedPins.Num(); ++i)
|
||||||
|
{
|
||||||
|
if (EntryNode->UserDefinedPins[i].IsValid() &&
|
||||||
|
EntryNode->UserDefinedPins[i]->PinName.ToString().Equals(ParamName, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
RemovedIndex = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (RemovedIndex == INDEX_NONE)
|
||||||
|
{
|
||||||
|
// List available params for debugging
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ParamNames;
|
||||||
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : EntryNode->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (PinInfo.IsValid())
|
||||||
|
{
|
||||||
|
ParamNames.Add(MakeShared<FJsonValueString>(PinInfo->PinName.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Parameter '%s' not found in %s '%s'"),
|
||||||
|
*ParamName, *FoundNodeType, *FunctionName));
|
||||||
|
Result->SetArrayField(TEXT("availableParams"), ParamNames);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Removing param '%s' from %s '%s' in '%s'"),
|
||||||
|
*ParamName, *FoundNodeType, *FunctionName, *Blueprint);
|
||||||
|
|
||||||
|
// Remove the pin
|
||||||
|
EntryNode->UserDefinedPins.RemoveAt(RemovedIndex);
|
||||||
|
|
||||||
|
// Reconstruct the node to update output pins (use schema for MinimalAPI compat)
|
||||||
|
if (UEdGraph* OwningGraph = EntryNode->GetGraph())
|
||||||
|
{
|
||||||
|
if (const UEdGraphSchema* Schema = OwningGraph->GetSchema())
|
||||||
|
{
|
||||||
|
Schema->ReconstructNode(*EntryNode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Parameter removed, save %s"),
|
||||||
|
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeType"), FoundNodeType);
|
||||||
|
Result->SetStringField(TEXT("nodeId"), EntryNode->NodeGuid.ToString());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "StructUtils/UserDefinedStruct.h"
|
||||||
|
#include "Engine/UserDefinedEnum.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
|
||||||
|
#include "Kismet2/EnumEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Factories/StructureFactory.h"
|
||||||
|
#include "Factories/EnumFactory.h"
|
||||||
|
#include "UMCPHandler_RemoveStructField.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RemoveStructField : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Name or package path of the struct asset"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the field to remove"))
|
||||||
|
FString Name;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Remove a field from a UserDefinedStruct asset.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Find the struct
|
||||||
|
MCPAssets<UUserDefinedStruct> Assets;
|
||||||
|
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UUserDefinedStruct* Struct = Assets.Object();
|
||||||
|
|
||||||
|
// Find the property GUID by name
|
||||||
|
FGuid TargetGuid;
|
||||||
|
bool bFound = false;
|
||||||
|
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
||||||
|
{
|
||||||
|
if (Var.FriendlyName == Name || Var.VarName.ToString() == Name)
|
||||||
|
{
|
||||||
|
TargetGuid = Var.VarGuid;
|
||||||
|
bFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bFound)
|
||||||
|
{
|
||||||
|
// List available properties
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AvailProps;
|
||||||
|
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(Struct))
|
||||||
|
{
|
||||||
|
AvailProps.Add(MakeShared<FJsonValueString>(Var.FriendlyName));
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found in struct '%s'"), *Name, *AssetPath));
|
||||||
|
Result->SetArrayField(TEXT("availableProperties"), AvailProps);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bRemoved = FStructureEditorUtils::RemoveVariable(Struct, TargetGuid);
|
||||||
|
if (!bRemoved)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to remove property '%s'"), *Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(Struct);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "FileHelpers.h"
|
||||||
|
#include "UMCPHandler_RenameAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RenameAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Current package path of the asset"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New package path or new asset name"))
|
||||||
|
FString NewPath;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Rename or move an asset with reference fixup.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming asset '%s' -> '%s'"), *AssetPath, *NewPath);
|
||||||
|
|
||||||
|
// Use FAssetToolsModule to perform the rename with reference fixup
|
||||||
|
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
|
||||||
|
IAssetTools& AssetTools = AssetToolsModule.Get();
|
||||||
|
|
||||||
|
// Build the source/dest arrays
|
||||||
|
TArray<FAssetRenameData> RenameData;
|
||||||
|
|
||||||
|
// We need to load the asset to get the object
|
||||||
|
MCPAssets<UObject> Assets;
|
||||||
|
if (!Assets.Exact(AssetPath).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UObject* AssetObj = Assets.Object();
|
||||||
|
|
||||||
|
// Parse new path into package path and asset name
|
||||||
|
FString NewPackagePath, NewAssetName;
|
||||||
|
int32 LastSlash;
|
||||||
|
if (NewPath.FindLastChar(TEXT('/'), LastSlash))
|
||||||
|
{
|
||||||
|
NewPackagePath = NewPath.Left(LastSlash);
|
||||||
|
NewAssetName = NewPath.Mid(LastSlash + 1);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If no slash, assume same directory with new name
|
||||||
|
FString OldPackagePath;
|
||||||
|
if (AssetPath.FindLastChar(TEXT('/'), LastSlash))
|
||||||
|
{
|
||||||
|
OldPackagePath = AssetPath.Left(LastSlash);
|
||||||
|
}
|
||||||
|
NewPackagePath = OldPackagePath;
|
||||||
|
NewAssetName = NewPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
FAssetRenameData RenameEntry(AssetObj, NewPackagePath, NewAssetName);
|
||||||
|
RenameData.Add(RenameEntry);
|
||||||
|
|
||||||
|
bool bSuccess = AssetTools.RenameAssets(RenameData);
|
||||||
|
|
||||||
|
if (bSuccess)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Rename %s"), bSuccess ? TEXT("succeeded") : TEXT("failed"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("newPackagePath"), NewPackagePath);
|
||||||
|
Result->SetStringField(TEXT("newAssetName"), NewAssetName);
|
||||||
|
if (!bSuccess)
|
||||||
|
{
|
||||||
|
MCPUtils::MakeErrorJson(Result, TEXT("Asset rename failed. The target path may be invalid or a conflicting asset may exist."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_RenameBlueprintGraph.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RenameBlueprintGraph : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Current name of the graph to rename"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New name for the graph"))
|
||||||
|
FString NewName;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Check if it's an UbergraphPage — disallow rename
|
||||||
|
for (UEdGraph* CandidateGraph : BP->UbergraphPages)
|
||||||
|
{
|
||||||
|
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot rename UbergraphPage '%s'. EventGraph and other Ubergraph pages cannot be renamed."),
|
||||||
|
*Graph));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the graph in FunctionGraphs or MacroGraphs
|
||||||
|
UEdGraph* TargetGraph = nullptr;
|
||||||
|
FString GraphType;
|
||||||
|
|
||||||
|
for (UEdGraph* CandidateGraph : BP->FunctionGraphs)
|
||||||
|
{
|
||||||
|
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
TargetGraph = CandidateGraph;
|
||||||
|
GraphType = TEXT("function");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!TargetGraph)
|
||||||
|
{
|
||||||
|
for (UEdGraph* CandidateGraph : BP->MacroGraphs)
|
||||||
|
{
|
||||||
|
if (CandidateGraph && CandidateGraph->GetName().Equals(Graph, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
TargetGraph = CandidateGraph;
|
||||||
|
GraphType = TEXT("macro");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found in Blueprint '%s'"), *Graph, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for name collision
|
||||||
|
for (UEdGraph* Existing : MCPUtils::AllGraphsNamed(BP, NewName))
|
||||||
|
{
|
||||||
|
if (Existing != TargetGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("A graph named '%s' already exists in Blueprint '%s'"), *NewName, *Blueprint));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renaming %s graph '%s' to '%s' in Blueprint '%s'"),
|
||||||
|
*GraphType, *Graph, *NewName, *Blueprint);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
|
||||||
|
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Renamed graph '%s' to '%s', save %s"),
|
||||||
|
*Graph, *NewName, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("newName"), TargetGraph->GetName());
|
||||||
|
Result->SetStringField(TEXT("graphType"), GraphType);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_ReparentBlueprint.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ReparentBlueprint : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the new parent class (C++ class name or Blueprint name)"))
|
||||||
|
FString NewParentClass;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint names.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Load Blueprint
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
FString OldParentName = BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None");
|
||||||
|
|
||||||
|
// Find the new parent class
|
||||||
|
// Try C++ class first (e.g. "WebUIHUD" finds /Script/ModuleName.WebUIHUD)
|
||||||
|
UClass* NewParentClassObj = nullptr;
|
||||||
|
|
||||||
|
// Search across all packages for native classes
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == NewParentClass)
|
||||||
|
{
|
||||||
|
NewParentClassObj = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If not found as C++ class, try loading as a Blueprint asset
|
||||||
|
if (!NewParentClassObj)
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> ParentAssets;
|
||||||
|
if (!ParentAssets.Exact(NewParentClass).AllContent().Errors(Result).ETwo().Load()) return;
|
||||||
|
if (!ParentAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
if (ParentAssets.Object()->GeneratedClass)
|
||||||
|
NewParentClassObj = ParentAssets.Object()->GeneratedClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NewParentClassObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Could not find class '%s'. Provide a C++ class name (e.g. 'WebUIHUD') or Blueprint name."),
|
||||||
|
*NewParentClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate: new parent must be compatible
|
||||||
|
if (BP->ParentClass && !NewParentClassObj->IsChildOf(BP->ParentClass->GetSuperClass()) &&
|
||||||
|
BP->ParentClass != NewParentClassObj)
|
||||||
|
{
|
||||||
|
// Just warn, don't block — the user may intentionally reparent to a sibling
|
||||||
|
UE_LOG(LogTemp, Warning,
|
||||||
|
TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s' — classes are not in a direct hierarchy"),
|
||||||
|
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparenting '%s' from '%s' to '%s'"),
|
||||||
|
*Blueprint, *OldParentName, *NewParentClassObj->GetName());
|
||||||
|
|
||||||
|
// Perform reparent
|
||||||
|
BP->PreEditChange(nullptr);
|
||||||
|
BP->ParentClass = NewParentClassObj;
|
||||||
|
BP->PostEditChange();
|
||||||
|
|
||||||
|
// Refresh all nodes to pick up new parent's functions/variables
|
||||||
|
FBlueprintEditorUtils::RefreshAllNodes(BP);
|
||||||
|
|
||||||
|
// Compile
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(BP);
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
FString NewParentActualName = NewParentClassObj->GetName();
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparent complete, save %s"),
|
||||||
|
bSaved ? TEXT("succeeded") : TEXT("failed"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("oldParentClass"), OldParentName);
|
||||||
|
Result->SetStringField(TEXT("newParentClass"), NewParentActualName);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||||
|
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||||
|
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Engine/Texture.h"
|
||||||
|
#include "UMCPHandler_ReparentMaterialInstance.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ReparentMaterialInstance : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material Instance name or path to reparent"))
|
||||||
|
FString MaterialInstance;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New parent material name or path (Material or Material Instance)"))
|
||||||
|
FString NewParent;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, validate without applying changes"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Change the parent material of a Material Instance. "
|
||||||
|
"Validates against circular parent chains.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
// Load the Material Instance
|
||||||
|
MCPAssets<UMaterialInstanceConstant> Assets;
|
||||||
|
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterialInstanceConstant* MI = Assets.Object();
|
||||||
|
|
||||||
|
// Capture old parent
|
||||||
|
FString OldParentPath = MI->Parent ? MI->Parent->GetPathName() : TEXT("None");
|
||||||
|
|
||||||
|
// Load new parent — try as Material first, then as Material Instance
|
||||||
|
UMaterialInterface* NewParentObj = nullptr;
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (MatAssets.Exact(NewParent).ETwo().Load() && !MatAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
NewParentObj = MatAssets.Object();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialInstanceConstant> MIAssets;
|
||||||
|
if (MIAssets.Exact(NewParent).ETwo().Load() && !MIAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
NewParentObj = MIAssets.Object();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NewParentObj)
|
||||||
|
{
|
||||||
|
// Try LoadObject as a fallback
|
||||||
|
NewParentObj = LoadObject<UMaterialInterface>(nullptr, *NewParent);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NewParentObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("New parent material '%s' not found. Provide a Material or Material Instance name/path."),
|
||||||
|
*NewParent));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent circular parenting — check if NewParent is this MI or has this MI in its chain
|
||||||
|
{
|
||||||
|
UMaterialInterface* Check = NewParentObj;
|
||||||
|
while (Check)
|
||||||
|
{
|
||||||
|
if (Check == MI)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Cannot reparent '%s' to '%s' — this would create a circular parent chain."),
|
||||||
|
*MaterialInstance, *NewParent));
|
||||||
|
}
|
||||||
|
UMaterialInstanceConstant* CheckMI = Cast<UMaterialInstanceConstant>(Check);
|
||||||
|
if (CheckMI)
|
||||||
|
{
|
||||||
|
Check = CheckMI->Parent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s Material Instance '%s': parent '%s' -> '%s'"),
|
||||||
|
DryRun ? TEXT("[DRY RUN] Reparenting") : TEXT("Reparenting"),
|
||||||
|
*MaterialInstance, *OldParentPath, *NewParentObj->GetPathName());
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MI->PreEditChange(nullptr);
|
||||||
|
MI->Parent = NewParentObj;
|
||||||
|
MI->PostEditChange();
|
||||||
|
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(MI);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Reparented Material Instance '%s' (saved: %s)"),
|
||||||
|
*MaterialInstance, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("oldParent"), OldParentPath);
|
||||||
|
Result->SetStringField(TEXT("newParent"), NewParentObj->GetPathName());
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_ReplaceFunctionCallsInBlueprint.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_ReplaceFunctionCallsInBlueprint : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Old class name to match"))
|
||||||
|
FString OldClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New class name to redirect to"))
|
||||||
|
FString NewClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, report what would change without modifying"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Redirect function call nodes from one class to another. "
|
||||||
|
"Supports dry-run to preview impact before applying.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
// Load Blueprint
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find the new class — try several search strategies
|
||||||
|
UClass* NewClassPtr = nullptr;
|
||||||
|
|
||||||
|
// Try finding the class across all loaded modules
|
||||||
|
NewClassPtr = FindFirstObject<UClass>(*NewClass);
|
||||||
|
|
||||||
|
// Try with U prefix stripped/added
|
||||||
|
if (!NewClassPtr && NewClass.StartsWith(TEXT("U")))
|
||||||
|
{
|
||||||
|
NewClassPtr = FindFirstObject<UClass>(*NewClass.Mid(1));
|
||||||
|
}
|
||||||
|
if (!NewClassPtr && !NewClass.StartsWith(TEXT("U")))
|
||||||
|
{
|
||||||
|
NewClassPtr = FindFirstObject<UClass>(*FString::Printf(TEXT("U%s"), *NewClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broader search across all modules
|
||||||
|
if (!NewClassPtr)
|
||||||
|
{
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == NewClass || It->GetName() == FString::Printf(TEXT("U%s"), *NewClass))
|
||||||
|
{
|
||||||
|
NewClassPtr = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!NewClassPtr)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not find class '%s'"), *NewClass));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s function calls in '%s': %s -> %s (%s)"),
|
||||||
|
DryRun ? TEXT("[DRY RUN] Analyzing replacement of") : TEXT("Replacing"),
|
||||||
|
*Blueprint, *OldClass, *NewClass, *NewClassPtr->GetPathName());
|
||||||
|
|
||||||
|
// Find all CallFunction nodes
|
||||||
|
int32 ReplacedCount = 0;
|
||||||
|
TArray<TSharedPtr<FJsonValue>> BrokenConnections;
|
||||||
|
|
||||||
|
for (UK2Node_CallFunction* CallNode : MCPUtils::AllNodes<UK2Node_CallFunction>(BP))
|
||||||
|
{
|
||||||
|
UClass* ParentClass = CallNode->FunctionReference.GetMemberParentClass();
|
||||||
|
if (!ParentClass)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match by class name (with or without U prefix, and _C suffix for BP classes)
|
||||||
|
FString ParentName = ParentClass->GetName();
|
||||||
|
bool bMatch = (ParentName == OldClass) ||
|
||||||
|
(ParentName == FString::Printf(TEXT("%s_C"), *OldClass)) ||
|
||||||
|
(ParentName == FString::Printf(TEXT("U%s"), *OldClass)) ||
|
||||||
|
(OldClass.StartsWith(TEXT("U")) && (ParentName == OldClass.Mid(1))) ||
|
||||||
|
(OldClass.EndsWith(TEXT("_C")) && (ParentName == OldClass.LeftChop(2)));
|
||||||
|
|
||||||
|
if (!bMatch)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
FName FuncName = CallNode->FunctionReference.GetMemberName();
|
||||||
|
|
||||||
|
// Find the matching function in the new class
|
||||||
|
UFunction* NewFunc = NewClassPtr->FindFunctionByName(FuncName);
|
||||||
|
if (!NewFunc)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Function '%s' not found in '%s', skipping node"),
|
||||||
|
*FuncName.ToString(), *NewClass);
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Warning = MakeShared<FJsonObject>();
|
||||||
|
Warning->SetStringField(TEXT("type"), TEXT("functionNotFound"));
|
||||||
|
Warning->SetStringField(TEXT("functionName"), FuncName.ToString());
|
||||||
|
Warning->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
|
||||||
|
BrokenConnections.Add(MakeShared<FJsonValueObject>(Warning));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
// In dry run mode: report what would be affected without modifying
|
||||||
|
ReplacedCount++;
|
||||||
|
|
||||||
|
// Check which pins have connections that might break
|
||||||
|
for (UEdGraphPin* Pin : CallNode->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || Pin->LinkedTo.Num() == 0) continue;
|
||||||
|
|
||||||
|
// Check if the new function has a matching parameter
|
||||||
|
bool bPinExistsInNew = false;
|
||||||
|
for (TFieldIterator<FProperty> PropIt(NewFunc); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
if (PropIt->GetFName() == Pin->PinName ||
|
||||||
|
Pin->PinName == UEdGraphSchema_K2::PN_Execute ||
|
||||||
|
Pin->PinName == UEdGraphSchema_K2::PN_Then ||
|
||||||
|
Pin->PinName == UEdGraphSchema_K2::PN_Self ||
|
||||||
|
Pin->PinName == UEdGraphSchema_K2::PN_ReturnValue)
|
||||||
|
{
|
||||||
|
bPinExistsInNew = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bPinExistsInNew)
|
||||||
|
{
|
||||||
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (Linked && Linked->GetOwningNode())
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> AtRisk = MakeShared<FJsonObject>();
|
||||||
|
AtRisk->SetStringField(TEXT("type"), TEXT("connectionAtRisk"));
|
||||||
|
AtRisk->SetStringField(TEXT("functionName"), FuncName.ToString());
|
||||||
|
AtRisk->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
|
||||||
|
AtRisk->SetStringField(TEXT("pinName"), Pin->PinName.ToString());
|
||||||
|
AtRisk->SetStringField(TEXT("connectedToNode"), Linked->GetOwningNode()->NodeGuid.ToString());
|
||||||
|
AtRisk->SetStringField(TEXT("connectedToPin"), Linked->PinName.ToString());
|
||||||
|
BrokenConnections.Add(MakeShared<FJsonValueObject>(AtRisk));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Record existing pin connections before replacement
|
||||||
|
TMap<FString, TArray<TPair<FString, FString>>> OldPinConnections;
|
||||||
|
for (UEdGraphPin* Pin : CallNode->Pins)
|
||||||
|
{
|
||||||
|
if (Pin->LinkedTo.Num() > 0)
|
||||||
|
{
|
||||||
|
TArray<TPair<FString, FString>> Links;
|
||||||
|
for (UEdGraphPin* Linked : Pin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (Linked && Linked->GetOwningNode())
|
||||||
|
{
|
||||||
|
Links.Add(TPair<FString, FString>(
|
||||||
|
Linked->GetOwningNode()->NodeGuid.ToString(),
|
||||||
|
Linked->PinName.ToString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
OldPinConnections.Add(Pin->PinName.ToString(), Links);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replace the function reference
|
||||||
|
CallNode->SetFromFunction(NewFunc);
|
||||||
|
ReplacedCount++;
|
||||||
|
|
||||||
|
// Check which connections survived
|
||||||
|
for (auto& Pair : OldPinConnections)
|
||||||
|
{
|
||||||
|
const FString& PinName = Pair.Key;
|
||||||
|
const TArray<TPair<FString, FString>>& OldLinks = Pair.Value;
|
||||||
|
|
||||||
|
UEdGraphPin* NewPin = CallNode->FindPin(FName(*PinName));
|
||||||
|
for (auto& Link : OldLinks)
|
||||||
|
{
|
||||||
|
bool bStillConnected = false;
|
||||||
|
if (NewPin)
|
||||||
|
{
|
||||||
|
for (UEdGraphPin* L : NewPin->LinkedTo)
|
||||||
|
{
|
||||||
|
if (L && L->GetOwningNode() &&
|
||||||
|
L->GetOwningNode()->NodeGuid.ToString() == Link.Key &&
|
||||||
|
L->PinName.ToString() == Link.Value)
|
||||||
|
{
|
||||||
|
bStillConnected = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bStillConnected)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> Broken = MakeShared<FJsonObject>();
|
||||||
|
Broken->SetStringField(TEXT("type"), TEXT("connectionLost"));
|
||||||
|
Broken->SetStringField(TEXT("functionName"), FuncName.ToString());
|
||||||
|
Broken->SetStringField(TEXT("nodeId"), CallNode->NodeGuid.ToString());
|
||||||
|
Broken->SetStringField(TEXT("pinName"), PinName);
|
||||||
|
Broken->SetStringField(TEXT("wasConnectedToNode"), Link.Key);
|
||||||
|
Broken->SetStringField(TEXT("wasConnectedToPin"), Link.Value);
|
||||||
|
BrokenConnections.Add(MakeShared<FJsonValueObject>(Broken));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
Result->SetNumberField(TEXT("wouldReplaceCount"), ReplacedCount);
|
||||||
|
Result->SetNumberField(TEXT("connectionsAtRisk"), BrokenConnections.Num());
|
||||||
|
Result->SetArrayField(TEXT("connectionsAtRisk"), BrokenConnections);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReplacedCount > 0)
|
||||||
|
{
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsModified(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Replaced %d function call(s)"), ReplacedCount);
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("replacedCount"), ReplacedCount);
|
||||||
|
Result->SetNumberField(TEXT("brokenConnectionCount"), BrokenConnections.Num());
|
||||||
|
Result->SetArrayField(TEXT("brokenConnections"), BrokenConnections);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("replacedCount"), 0);
|
||||||
|
Result->SetStringField(TEXT("message"), FString::Printf(
|
||||||
|
TEXT("No function call nodes found targeting class '%s'"), *OldClass));
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "FileHelpers.h"
|
||||||
|
#include "UMCPHandler_RestoreAsset.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_RestoreAsset : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Full package path of the asset (e.g. /Game/Widgets/WB_Hotkeys)"))
|
||||||
|
FString AssetPath;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString Filename = FPaths::ConvertRelativePathToFull(
|
||||||
|
FPackageName::LongPackageNameToFilename(AssetPath, FPackageName::GetAssetPackageExtension()));
|
||||||
|
FString BackupFilename = Filename + TEXT(".bak");
|
||||||
|
|
||||||
|
if (!IFileManager::Get().FileExists(*BackupFilename))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Backup file not found: %s"), *BackupFilename));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release file handles if the package is loaded
|
||||||
|
UPackage* Package = FindPackage(nullptr, *AssetPath);
|
||||||
|
if (Package)
|
||||||
|
{
|
||||||
|
ResetLoaders(Package);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy backup over the original
|
||||||
|
uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true);
|
||||||
|
if (CopyResult != COPY_OK)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Failed to restore %s"), *Filename));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload the package if it was loaded
|
||||||
|
if (Package)
|
||||||
|
{
|
||||||
|
bool bReloaded = false;
|
||||||
|
FText ErrorMessage;
|
||||||
|
UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("restoredFrom"), BackupFilename);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,118 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_SearchSpawnableNodeTypes.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SearchSpawnableNodeTypes : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Search query string"))
|
||||||
|
FString Query;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)"))
|
||||||
|
int32 MaxResults = 50;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Blueprint name or path. If specified with graph, only returns nodes compatible with that graph."))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Graph name to filter by compatibility. Requires blueprint."))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Search the Blueprint action database for node spawners matching a query. "
|
||||||
|
"Returns full action names for use with spawn_node. "
|
||||||
|
"Optionally filter by blueprint+graph to only show compatible node types.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
|
||||||
|
|
||||||
|
// Optionally resolve a graph to filter by compatibility
|
||||||
|
UEdGraph* GraphFilter = nullptr;
|
||||||
|
if (!Blueprint.IsEmpty() && !Graph.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> BPAssets;
|
||||||
|
if (!BPAssets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = BPAssets.Object();
|
||||||
|
|
||||||
|
FString DecodedGraphName = MCPUtils::UrlDecode(Graph);
|
||||||
|
TArray<UEdGraph*> MatchingGraphs = MCPUtils::AllGraphsNamed(BP, DecodedGraphName);
|
||||||
|
if (MatchingGraphs.Num() > 0)
|
||||||
|
{
|
||||||
|
GraphFilter = MatchingGraphs[0];
|
||||||
|
}
|
||||||
|
if (!GraphFilter)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Graph '%s' not found"), *DecodedGraphName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<UBlueprintNodeSpawner*> Spawners = MCPUtils::SearchNodeSpawners(Query, ClampedMax, /*ExactMatch=*/false, GraphFilter);
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ResultArray;
|
||||||
|
for (UBlueprintNodeSpawner* Spawner : Spawners)
|
||||||
|
{
|
||||||
|
ResultArray.Add(MakeShared<FJsonValueString>(MCPUtils::NodeSpawnerFullName(Spawner)));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), ResultArray.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), ResultArray);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,283 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "K2Node_BreakStruct.h"
|
||||||
|
#include "K2Node_MakeStruct.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "UMCPHandler_SearchTypeUsageInBlueprints.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleSearchByType — find all usages of a type across blueprints
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SearchTypeUsageInBlueprints : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Type name to search for (e.g. 'FVector', 'MyStruct'). F/E/U prefix is stripped for matching."))
|
||||||
|
FString TypeName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Filter to blueprints whose name or path contains this substring"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 200, max 500)"))
|
||||||
|
int32 MaxResults = 0;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Search all Blueprints for usages of a specific type in variables, function parameters, struct nodes, and pin connections.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
|
||||||
|
FString FilterStr = Filter.IsEmpty() ? FString() : MCPUtils::UrlDecode(Filter);
|
||||||
|
|
||||||
|
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 500) : 200;
|
||||||
|
|
||||||
|
// Strip F/E/U prefix for comparison
|
||||||
|
FString TypeNameNoPrefix = DecodedTypeName;
|
||||||
|
if (TypeNameNoPrefix.StartsWith(TEXT("F")) || TypeNameNoPrefix.StartsWith(TEXT("E")) || TypeNameNoPrefix.StartsWith(TEXT("U")))
|
||||||
|
{
|
||||||
|
TypeNameNoPrefix = TypeNameNoPrefix.Mid(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto MatchesType = [&DecodedTypeName, &TypeNameNoPrefix](const FString& TestType) -> bool
|
||||||
|
{
|
||||||
|
return TestType.Equals(DecodedTypeName, ESearchCase::IgnoreCase) ||
|
||||||
|
TestType.Equals(TypeNameNoPrefix, ESearchCase::IgnoreCase);
|
||||||
|
};
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
|
||||||
|
// Lambda that searches a single Blueprint for type usages
|
||||||
|
auto SearchOneBlueprint = [&](const FString& BPName, const FString& BPPath, UBlueprint* BP, bool bIsLevel)
|
||||||
|
{
|
||||||
|
// Check variables
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Results.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
FString VarSubtype;
|
||||||
|
if (Var.VarType.PinSubCategoryObject.IsValid())
|
||||||
|
{
|
||||||
|
VarSubtype = Var.VarType.PinSubCategoryObject->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MatchesType(VarSubtype) || MatchesType(Var.VarType.PinCategory.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), BPName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||||
|
R->SetStringField(TEXT("usage"), TEXT("variable"));
|
||||||
|
R->SetStringField(TEXT("location"), Var.VarName.ToString());
|
||||||
|
R->SetStringField(TEXT("currentType"), Var.VarType.PinCategory.ToString());
|
||||||
|
if (!VarSubtype.IsEmpty())
|
||||||
|
R->SetStringField(TEXT("currentSubtype"), VarSubtype);
|
||||||
|
if (bIsLevel)
|
||||||
|
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check graphs for function/event params, struct nodes, and pin connections
|
||||||
|
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
|
||||||
|
{
|
||||||
|
if (Results.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
// Check FunctionEntry/CustomEvent parameters
|
||||||
|
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
|
||||||
|
{
|
||||||
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (!PinInfo.IsValid()) continue;
|
||||||
|
FString ParamSubtype;
|
||||||
|
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||||
|
|
||||||
|
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), BPName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||||
|
R->SetStringField(TEXT("usage"), TEXT("functionParameter"));
|
||||||
|
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||||
|
*Node->GetGraph()->GetName(), *PinInfo->PinName.ToString()));
|
||||||
|
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||||
|
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
||||||
|
if (!ParamSubtype.IsEmpty())
|
||||||
|
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
||||||
|
if (bIsLevel)
|
||||||
|
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
|
||||||
|
{
|
||||||
|
for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
|
||||||
|
{
|
||||||
|
if (!PinInfo.IsValid()) continue;
|
||||||
|
FString ParamSubtype;
|
||||||
|
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
ParamSubtype = PinInfo->PinType.PinSubCategoryObject->GetName();
|
||||||
|
|
||||||
|
if (MatchesType(ParamSubtype) || MatchesType(PinInfo->PinType.PinCategory.ToString()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), BPName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||||
|
R->SetStringField(TEXT("usage"), TEXT("eventParameter"));
|
||||||
|
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||||
|
*CustomEvent->CustomFunctionName.ToString(), *PinInfo->PinName.ToString()));
|
||||||
|
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||||
|
R->SetStringField(TEXT("currentType"), PinInfo->PinType.PinCategory.ToString());
|
||||||
|
if (!ParamSubtype.IsEmpty())
|
||||||
|
R->SetStringField(TEXT("currentSubtype"), ParamSubtype);
|
||||||
|
if (bIsLevel)
|
||||||
|
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Check Break/Make struct nodes
|
||||||
|
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
|
||||||
|
{
|
||||||
|
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), BPName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||||
|
R->SetStringField(TEXT("usage"), TEXT("breakStruct"));
|
||||||
|
R->SetStringField(TEXT("location"), Node->GetGraph()->GetName());
|
||||||
|
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||||
|
R->SetStringField(TEXT("structType"), BreakNode->StructType->GetName());
|
||||||
|
if (bIsLevel)
|
||||||
|
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
|
||||||
|
{
|
||||||
|
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), BPName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||||
|
R->SetStringField(TEXT("usage"), TEXT("makeStruct"));
|
||||||
|
R->SetStringField(TEXT("location"), Node->GetGraph()->GetName());
|
||||||
|
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||||
|
R->SetStringField(TEXT("structType"), MakeNode->StructType->GetName());
|
||||||
|
if (bIsLevel)
|
||||||
|
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pin connections carrying the type
|
||||||
|
for (UEdGraphPin* Pin : Node->Pins)
|
||||||
|
{
|
||||||
|
if (!Pin || Pin->bHidden || Results.Num() >= EffectiveMaxResults) continue;
|
||||||
|
|
||||||
|
FString PinSubtype;
|
||||||
|
if (Pin->PinType.PinSubCategoryObject.IsValid())
|
||||||
|
PinSubtype = Pin->PinType.PinSubCategoryObject->GetName();
|
||||||
|
|
||||||
|
if ((Pin->LinkedTo.Num() > 0) &&
|
||||||
|
(MatchesType(PinSubtype) || MatchesType(Pin->PinType.PinCategory.ToString())))
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), BPName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), BPPath);
|
||||||
|
R->SetStringField(TEXT("usage"), TEXT("pinConnection"));
|
||||||
|
R->SetStringField(TEXT("location"), FString::Printf(TEXT("%s.%s"),
|
||||||
|
*Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString(),
|
||||||
|
*Pin->PinName.ToString()));
|
||||||
|
R->SetStringField(TEXT("nodeId"), Node->NodeGuid.ToString());
|
||||||
|
R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
|
||||||
|
R->SetStringField(TEXT("pinType"), Pin->PinType.PinCategory.ToString());
|
||||||
|
if (!PinSubtype.IsEmpty())
|
||||||
|
R->SetStringField(TEXT("pinSubtype"), PinSubtype);
|
||||||
|
R->SetNumberField(TEXT("connectionCount"), Pin->LinkedTo.Num());
|
||||||
|
if (bIsLevel)
|
||||||
|
R->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> AllBlueprints;
|
||||||
|
AllBlueprints.Info();
|
||||||
|
MCPAssets<UWorld> AllWorlds;
|
||||||
|
AllWorlds.Info();
|
||||||
|
|
||||||
|
// Search regular blueprints
|
||||||
|
for (const FAssetData& Asset : AllBlueprints.AllData())
|
||||||
|
{
|
||||||
|
if (Results.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
FString AssetPath = Asset.PackageName.ToString();
|
||||||
|
FString BPName = Asset.AssetName.ToString();
|
||||||
|
|
||||||
|
if (!FilterStr.IsEmpty() && !BPName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
||||||
|
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||||
|
if (!BP) continue;
|
||||||
|
|
||||||
|
SearchOneBlueprint(BPName, AssetPath, BP, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search level blueprints from maps
|
||||||
|
for (const FAssetData& MapAsset : AllWorlds.AllData())
|
||||||
|
{
|
||||||
|
if (Results.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
FString AssetPath = MapAsset.PackageName.ToString();
|
||||||
|
FString MapName = MapAsset.AssetName.ToString();
|
||||||
|
|
||||||
|
if (!FilterStr.IsEmpty() && !MapName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
|
||||||
|
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
||||||
|
if (!World || !World->PersistentLevel) continue;
|
||||||
|
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
||||||
|
if (!LevelBP) continue;
|
||||||
|
|
||||||
|
SearchOneBlueprint(MapName, AssetPath, LevelBP, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "UMCPHandler_SearchUnrealClasses.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// HandleListClasses — discover available UClasses
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SearchUnrealClasses : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
|
||||||
|
FString Filter;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Parent class name to restrict results to subclasses"))
|
||||||
|
FString ParentClass;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (1-500, default 100)"))
|
||||||
|
int32 Limit = 100;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Search for available UClasses by name substring and/or parent class. "
|
||||||
|
"Returns class metadata including flags, parent class, and package.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Json->HasField(TEXT("limit")))
|
||||||
|
{
|
||||||
|
Limit = FMath::Clamp(Limit, 1, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
UClass* ParentClassObj = nullptr;
|
||||||
|
if (!ParentClass.IsEmpty())
|
||||||
|
{
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == ParentClass || It->GetName() == ParentClass + TEXT("_C"))
|
||||||
|
{
|
||||||
|
ParentClassObj = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!ParentClassObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Parent class '%s' not found"), *ParentClass));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> ClassList;
|
||||||
|
int32 TotalMatched = 0;
|
||||||
|
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
UClass* Class = *It;
|
||||||
|
if (!Class) continue;
|
||||||
|
|
||||||
|
// Skip internal/deprecated classes
|
||||||
|
if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue;
|
||||||
|
|
||||||
|
// Apply parent filter
|
||||||
|
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
|
||||||
|
|
||||||
|
FString ClassName = Class->GetName();
|
||||||
|
|
||||||
|
// Apply name filter
|
||||||
|
if (!Filter.IsEmpty() && !ClassName.Contains(Filter, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
TotalMatched++;
|
||||||
|
if (ClassList.Num() >= Limit) continue; // Count but don't add beyond limit
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> ClassObj = MakeShared<FJsonObject>();
|
||||||
|
ClassObj->SetStringField(TEXT("name"), ClassName);
|
||||||
|
ClassObj->SetStringField(TEXT("fullPath"), Class->GetPathName());
|
||||||
|
|
||||||
|
// Determine if it's a Blueprint-generated class
|
||||||
|
bool bIsBlueprint = Class->ClassGeneratedBy != nullptr;
|
||||||
|
ClassObj->SetBoolField(TEXT("isBlueprint"), bIsBlueprint);
|
||||||
|
|
||||||
|
// Parent class
|
||||||
|
if (Class->GetSuperClass())
|
||||||
|
{
|
||||||
|
ClassObj->SetStringField(TEXT("parentClass"), Class->GetSuperClass()->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Module/package info
|
||||||
|
UPackage* Package = Class->GetOuterUPackage();
|
||||||
|
if (Package)
|
||||||
|
{
|
||||||
|
ClassObj->SetStringField(TEXT("package"), Package->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Flags;
|
||||||
|
if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Abstract")));
|
||||||
|
if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Add(MakeShared<FJsonValueString>(TEXT("Interface")));
|
||||||
|
if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Add(MakeShared<FJsonValueString>(TEXT("MinimalAPI")));
|
||||||
|
if (Flags.Num() > 0)
|
||||||
|
{
|
||||||
|
ClassObj->SetArrayField(TEXT("flags"), Flags);
|
||||||
|
}
|
||||||
|
|
||||||
|
ClassList.Add(MakeShared<FJsonValueObject>(ClassObj));
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("count"), ClassList.Num());
|
||||||
|
Result->SetNumberField(TEXT("totalMatched"), TotalMatched);
|
||||||
|
if (TotalMatched > Limit)
|
||||||
|
{
|
||||||
|
Result->SetBoolField(TEXT("truncated"), true);
|
||||||
|
Result->SetNumberField(TEXT("limit"), Limit);
|
||||||
|
}
|
||||||
|
Result->SetArrayField(TEXT("classes"), ClassList);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/Level.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "K2Node_CallFunction.h"
|
||||||
|
#include "K2Node_Event.h"
|
||||||
|
#include "K2Node_CustomEvent.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "K2Node_BreakStruct.h"
|
||||||
|
#include "K2Node_MakeStruct.h"
|
||||||
|
#include "K2Node_FunctionEntry.h"
|
||||||
|
#include "K2Node_EditablePinBase.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "UMCPHandler_SearchWithinBlueprints.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SearchWithinBlueprints : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Search query string to match against node titles, function names, event names, and variable names"))
|
||||||
|
FString Query;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Filter results to blueprints whose path contains this substring"))
|
||||||
|
FString Path;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
|
||||||
|
int32 MaxResults = 0;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Search across all Blueprint graphs for nodes matching a query string.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
int32 EffectiveMaxResults = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50;
|
||||||
|
|
||||||
|
// Build a combined list of all searchable blueprints (regular + level)
|
||||||
|
auto SearchBlueprint = [&](const FString& AssetName, const FString& AssetPath, UBlueprint* BP, TArray<TSharedPtr<FJsonValue>>& OutResults)
|
||||||
|
{
|
||||||
|
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
|
||||||
|
{
|
||||||
|
if (OutResults.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
|
||||||
|
|
||||||
|
FString FuncName, EventName, VarName;
|
||||||
|
if (auto* CF = Cast<UK2Node_CallFunction>(Node))
|
||||||
|
{
|
||||||
|
FuncName = CF->FunctionReference.GetMemberName().ToString();
|
||||||
|
}
|
||||||
|
else if (auto* Ev = Cast<UK2Node_Event>(Node))
|
||||||
|
{
|
||||||
|
EventName = Ev->EventReference.GetMemberName().ToString();
|
||||||
|
}
|
||||||
|
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
|
||||||
|
{
|
||||||
|
EventName = CE->CustomFunctionName.ToString();
|
||||||
|
}
|
||||||
|
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
|
||||||
|
{
|
||||||
|
VarName = VG->GetVarName().ToString();
|
||||||
|
}
|
||||||
|
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
|
||||||
|
{
|
||||||
|
VarName = VS->GetVarName().ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bMatch = Title.Contains(Query, ESearchCase::IgnoreCase) ||
|
||||||
|
(!FuncName.IsEmpty() && FuncName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
||||||
|
(!EventName.IsEmpty() && EventName.Contains(Query, ESearchCase::IgnoreCase)) ||
|
||||||
|
(!VarName.IsEmpty() && VarName.Contains(Query, ESearchCase::IgnoreCase));
|
||||||
|
|
||||||
|
if (bMatch)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("blueprint"), AssetName);
|
||||||
|
R->SetStringField(TEXT("blueprintPath"), AssetPath);
|
||||||
|
R->SetStringField(TEXT("graph"), Node->GetGraph()->GetName());
|
||||||
|
R->SetStringField(TEXT("nodeTitle"), Title);
|
||||||
|
R->SetStringField(TEXT("nodeClass"), Node->GetClass()->GetName());
|
||||||
|
if (!FuncName.IsEmpty()) R->SetStringField(TEXT("functionName"), FuncName);
|
||||||
|
if (!EventName.IsEmpty()) R->SetStringField(TEXT("eventName"), EventName);
|
||||||
|
if (!VarName.IsEmpty()) R->SetStringField(TEXT("variableName"), VarName);
|
||||||
|
OutResults.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
MCPAssets<UBlueprint> AllBlueprints;
|
||||||
|
AllBlueprints.Info();
|
||||||
|
MCPAssets<UWorld> AllWorlds;
|
||||||
|
AllWorlds.Info();
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
for (const FAssetData& Asset : AllBlueprints.AllData())
|
||||||
|
{
|
||||||
|
if (Results.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
FString AssetPath = Asset.PackageName.ToString();
|
||||||
|
if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||||
|
if (!BP) continue;
|
||||||
|
|
||||||
|
SearchBlueprint(Asset.AssetName.ToString(), AssetPath, BP, Results);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also search level blueprints
|
||||||
|
for (const FAssetData& MapAsset : AllWorlds.AllData())
|
||||||
|
{
|
||||||
|
if (Results.Num() >= EffectiveMaxResults) break;
|
||||||
|
|
||||||
|
FString AssetPath = MapAsset.PackageName.ToString();
|
||||||
|
if (!Path.IsEmpty() && !AssetPath.Contains(Path, ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
UWorld* World = Cast<UWorld>(MapAsset.GetAsset());
|
||||||
|
if (!World || !World->PersistentLevel) continue;
|
||||||
|
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
|
||||||
|
if (!LevelBP) continue;
|
||||||
|
|
||||||
|
int32 BeforeCount = Results.Num();
|
||||||
|
SearchBlueprint(MapAsset.AssetName.ToString(), AssetPath, LevelBP, Results);
|
||||||
|
// Tag newly-added entries as level blueprint results
|
||||||
|
for (int32 i = BeforeCount; i < Results.Num(); ++i)
|
||||||
|
{
|
||||||
|
Results[i]->AsObject()->SetBoolField(TEXT("isLevelBlueprint"), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode_Root.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "UMCPHandler_SearchWithinMaterials.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SearchWithinMaterials : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Search query string to match against material names, expression classes, and parameter names"))
|
||||||
|
FString Query;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum number of results to return (default 50, max 200)"))
|
||||||
|
int32 MaxResults = 50;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Search across all materials for matching material names, expression types, and parameter names.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
FString DecodedQuery = MCPUtils::UrlDecode(Query);
|
||||||
|
|
||||||
|
MaxResults = FMath::Clamp(MaxResults, 1, 200);
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Results;
|
||||||
|
|
||||||
|
MCPAssets<UMaterial> AllMaterials;
|
||||||
|
AllMaterials.Info();
|
||||||
|
|
||||||
|
for (const FAssetData& Asset : AllMaterials.AllData())
|
||||||
|
{
|
||||||
|
if (Results.Num() >= MaxResults) break;
|
||||||
|
|
||||||
|
FString MatName = Asset.AssetName.ToString();
|
||||||
|
|
||||||
|
// Check material name first
|
||||||
|
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
|
||||||
|
|
||||||
|
UMaterial* MaterialObj = Cast<UMaterial>(const_cast<FAssetData&>(Asset).GetAsset());
|
||||||
|
if (!MaterialObj) continue;
|
||||||
|
|
||||||
|
auto Expressions = MaterialObj->GetExpressions();
|
||||||
|
|
||||||
|
if (bNameMatch)
|
||||||
|
{
|
||||||
|
// Add a match for the material itself
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("material"), MatName);
|
||||||
|
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
||||||
|
R->SetStringField(TEXT("matchType"), TEXT("materialName"));
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search expressions
|
||||||
|
for (UMaterialExpression* Expr : Expressions)
|
||||||
|
{
|
||||||
|
if (!Expr || Results.Num() >= MaxResults) continue;
|
||||||
|
|
||||||
|
FString ExprDesc = Expr->GetDescription();
|
||||||
|
FString ExprClass = Expr->GetClass()->GetName();
|
||||||
|
|
||||||
|
// Check parameter name
|
||||||
|
FString ParamName;
|
||||||
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||||
|
ParamName = SP->ParameterName.ToString();
|
||||||
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||||
|
ParamName = VP->ParameterName.ToString();
|
||||||
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||||
|
ParamName = TP->ParameterName.ToString();
|
||||||
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||||
|
ParamName = SSP->ParameterName.ToString();
|
||||||
|
|
||||||
|
bool bExprMatch = ExprDesc.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
||||||
|
ExprClass.Contains(DecodedQuery, ESearchCase::IgnoreCase) ||
|
||||||
|
(!ParamName.IsEmpty() && ParamName.Contains(DecodedQuery, ESearchCase::IgnoreCase));
|
||||||
|
|
||||||
|
if (bExprMatch)
|
||||||
|
{
|
||||||
|
TSharedRef<FJsonObject> R = MakeShared<FJsonObject>();
|
||||||
|
R->SetStringField(TEXT("material"), MatName);
|
||||||
|
R->SetStringField(TEXT("materialPath"), Asset.PackageName.ToString());
|
||||||
|
R->SetStringField(TEXT("matchType"), TEXT("expression"));
|
||||||
|
R->SetStringField(TEXT("expressionClass"), ExprClass);
|
||||||
|
if (!ExprDesc.IsEmpty())
|
||||||
|
R->SetStringField(TEXT("description"), ExprDesc);
|
||||||
|
if (!ParamName.IsEmpty())
|
||||||
|
R->SetStringField(TEXT("parameterName"), ParamName);
|
||||||
|
Results.Add(MakeShared<FJsonValueObject>(R));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("query"), DecodedQuery);
|
||||||
|
Result->SetNumberField(TEXT("resultCount"), Results.Num());
|
||||||
|
Result->SetArrayField(TEXT("results"), Results);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
|
#include "AnimStateNode.h"
|
||||||
|
#include "AnimStateTransitionNode.h"
|
||||||
|
#include "AnimationStateMachineGraph.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "UMCPHandler_SetAnimStateAnimation.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetAnimStateAnimation : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="State machine graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the state to modify"))
|
||||||
|
FString StateName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Animation asset name to assign"))
|
||||||
|
FString AnimationAsset;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set or replace the animation sequence played by a state in an animation state machine.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
||||||
|
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||||
|
if (!StateNode) return;
|
||||||
|
|
||||||
|
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
||||||
|
if (!InnerGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the animation asset
|
||||||
|
MCPAssets<UAnimSequence> AnimAssets;
|
||||||
|
if (!AnimAssets.Exact(AnimationAsset).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimSequence* AnimSeq = AnimAssets.Object();
|
||||||
|
|
||||||
|
// Find existing SequencePlayer or create one
|
||||||
|
UAnimGraphNode_SequencePlayer* SeqNode = nullptr;
|
||||||
|
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||||
|
{
|
||||||
|
SeqNode = Cast<UAnimGraphNode_SequencePlayer>(Node);
|
||||||
|
if (SeqNode) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool bCreatedNew = false;
|
||||||
|
if (!SeqNode)
|
||||||
|
{
|
||||||
|
SeqNode = NewObject<UAnimGraphNode_SequencePlayer>(InnerGraph);
|
||||||
|
SeqNode->CreateNewGuid();
|
||||||
|
SeqNode->PostPlacedNewNode();
|
||||||
|
SeqNode->AllocateDefaultPins();
|
||||||
|
SeqNode->NodePosX = 0;
|
||||||
|
SeqNode->NodePosY = 0;
|
||||||
|
InnerGraph->AddNode(SeqNode, false, false);
|
||||||
|
bCreatedNew = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
SeqNode->SetAnimationAsset(AnimSeq);
|
||||||
|
|
||||||
|
// Compile and save
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("createdNewNode"), bCreatedNew);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
|
#include "AnimStateNode.h"
|
||||||
|
#include "AnimStateTransitionNode.h"
|
||||||
|
#include "AnimationStateMachineGraph.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "UMCPHandler_SetAnimStateBlendSpace.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetAnimStateBlendSpace : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="State machine graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the state to modify"))
|
||||||
|
FString StateName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Blend Space asset name or path"))
|
||||||
|
FString BlendSpace;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the X axis input"))
|
||||||
|
FString XVariable;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Blueprint variable name to wire to the Y axis input"))
|
||||||
|
FString YVariable;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Place a BlendSpacePlayer in a state's inner graph, connect it to the output pose, "
|
||||||
|
"and optionally wire blueprint variables to the X and Y axis inputs.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
||||||
|
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
UAnimStateNode* StateNode = MCPUtils::FindStateByName(SMGraph, StateName, Result);
|
||||||
|
if (!StateNode) return;
|
||||||
|
|
||||||
|
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
|
||||||
|
if (!InnerGraph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State '%s' has no bound graph"), *StateName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the blend space asset
|
||||||
|
MCPAssets<UBlendSpace> BlendSpaceAssets;
|
||||||
|
if (!BlendSpaceAssets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlendSpace* BlendSpaceAsset = BlendSpaceAssets.Object();
|
||||||
|
|
||||||
|
// Find existing BlendSpacePlayer or create one
|
||||||
|
UAnimGraphNode_BlendSpacePlayer* BSNode = nullptr;
|
||||||
|
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||||
|
{
|
||||||
|
BSNode = Cast<UAnimGraphNode_BlendSpacePlayer>(Node);
|
||||||
|
if (BSNode) break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!BSNode)
|
||||||
|
{
|
||||||
|
BSNode = NewObject<UAnimGraphNode_BlendSpacePlayer>(InnerGraph);
|
||||||
|
BSNode->CreateNewGuid();
|
||||||
|
BSNode->PostPlacedNewNode();
|
||||||
|
BSNode->AllocateDefaultPins();
|
||||||
|
BSNode->NodePosX = 0;
|
||||||
|
BSNode->NodePosY = 0;
|
||||||
|
InnerGraph->AddNode(BSNode, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
BSNode->SetAnimationAsset(BlendSpaceAsset);
|
||||||
|
|
||||||
|
// Connect BlendSpacePlayer output to the Output Animation Pose node
|
||||||
|
{
|
||||||
|
// Find the AnimGraphNode_Root (Output Pose) in the inner graph
|
||||||
|
UEdGraphNode* ResultNode = nullptr;
|
||||||
|
for (UEdGraphNode* Node : InnerGraph->Nodes)
|
||||||
|
{
|
||||||
|
if (Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_Root")) ||
|
||||||
|
Node->GetClass()->GetName().Contains(TEXT("AnimGraphNode_StateResult")))
|
||||||
|
{
|
||||||
|
ResultNode = Node;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ResultNode)
|
||||||
|
{
|
||||||
|
// Find output pose pin on BlendSpacePlayer and input pose pin on result node
|
||||||
|
UEdGraphPin* BSOutputPin = nullptr;
|
||||||
|
for (UEdGraphPin* Pin : BSNode->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct))
|
||||||
|
{
|
||||||
|
BSOutputPin = Pin;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
UEdGraphPin* ResultInputPin = nullptr;
|
||||||
|
for (UEdGraphPin* Pin : ResultNode->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && (Pin->Direction == EGPD_Input) && (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Struct))
|
||||||
|
{
|
||||||
|
ResultInputPin = Pin;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BSOutputPin && ResultInputPin)
|
||||||
|
{
|
||||||
|
// Break existing connections on the result input
|
||||||
|
ResultInputPin->BreakAllPinLinks();
|
||||||
|
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
||||||
|
if (Schema)
|
||||||
|
{
|
||||||
|
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wire X and Y variables if provided
|
||||||
|
auto WireVariable = [&](const FString& VarName, const FString& PinName) -> bool
|
||||||
|
{
|
||||||
|
if (VarName.IsEmpty()) return false;
|
||||||
|
|
||||||
|
// Verify the variable exists in the blueprint
|
||||||
|
FName VarFName(*VarName);
|
||||||
|
bool bVarFound = false;
|
||||||
|
for (FBPVariableDescription& Var : AnimBP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName == VarFName)
|
||||||
|
{
|
||||||
|
bVarFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bVarFound)
|
||||||
|
{
|
||||||
|
// Also check parent class properties
|
||||||
|
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
|
||||||
|
{
|
||||||
|
if (FProperty* Prop = GenClass->FindPropertyByName(VarFName))
|
||||||
|
{
|
||||||
|
bVarFound = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!bVarFound)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Warning, TEXT("BlueprintMCP: Variable '%s' not found in '%s', skipping wire"),
|
||||||
|
*VarName, *Blueprint);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a VariableGet node
|
||||||
|
UK2Node_VariableGet* GetNode = NewObject<UK2Node_VariableGet>(InnerGraph);
|
||||||
|
GetNode->VariableReference.SetSelfMember(VarFName);
|
||||||
|
GetNode->NodePosX = BSNode->NodePosX - 250;
|
||||||
|
GetNode->NodePosY = BSNode->NodePosY;
|
||||||
|
InnerGraph->AddNode(GetNode, false, false);
|
||||||
|
GetNode->AllocateDefaultPins();
|
||||||
|
|
||||||
|
// Find the variable output pin
|
||||||
|
UEdGraphPin* VarOutPin = nullptr;
|
||||||
|
for (UEdGraphPin* Pin : GetNode->Pins)
|
||||||
|
{
|
||||||
|
if (Pin && (Pin->Direction == EGPD_Output) && (Pin->PinName == VarFName))
|
||||||
|
{
|
||||||
|
VarOutPin = Pin;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the target pin on the BlendSpacePlayer
|
||||||
|
UEdGraphPin* TargetPin = BSNode->FindPin(FName(*PinName));
|
||||||
|
|
||||||
|
if (VarOutPin && TargetPin)
|
||||||
|
{
|
||||||
|
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
|
||||||
|
if (Schema)
|
||||||
|
{
|
||||||
|
Schema->TryCreateConnection(VarOutPin, TargetPin);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
WireVariable(XVariable, TEXT("X"));
|
||||||
|
WireVariable(YVariable, TEXT("Y"));
|
||||||
|
|
||||||
|
// Compile and save
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("nodeId"), BSNode->NodeGuid.ToString());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "AnimGraphNode_SequencePlayer.h"
|
||||||
|
#include "AnimGraphNode_BlendSpacePlayer.h"
|
||||||
|
#include "AnimStateNode.h"
|
||||||
|
#include "AnimStateTransitionNode.h"
|
||||||
|
#include "AnimationStateMachineGraph.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "UMCPHandler_SetAnimTransitionRule.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetAnimTransitionRule : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Animation Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="State machine graph name"))
|
||||||
|
FString Graph;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the source state"))
|
||||||
|
FString FromState;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the target state"))
|
||||||
|
FString ToState;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Crossfade duration in seconds"))
|
||||||
|
float CrossfadeDuration = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Blend mode (as integer enum value)"))
|
||||||
|
int32 BlendMode = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Transition priority order"))
|
||||||
|
int32 PriorityOrder = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Logic type (as integer enum value)"))
|
||||||
|
int32 LogicType = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Whether the transition is bidirectional"))
|
||||||
|
bool BBidirectional = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Update properties on an existing transition between two states in an animation state machine.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UAnimationStateMachineGraph* SMGraph = MCPUtils::FindStateMachineGraph(Assets.Object(), Graph);
|
||||||
|
if (!SMGraph) { MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("State machine graph '%s' not found in '%s'"), *Graph, *Blueprint)); return; }
|
||||||
|
UAnimBlueprint* AnimBP = Assets.Object();
|
||||||
|
|
||||||
|
UAnimStateTransitionNode* TransNode = MCPUtils::FindTransition(SMGraph, FromState, ToState);
|
||||||
|
if (!TransNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Transition from '%s' to '%s' not found in graph '%s'"),
|
||||||
|
*FromState, *ToState, *Graph));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update properties
|
||||||
|
int32 ChangedCount = 0;
|
||||||
|
TransNode->PreEditChange(nullptr);
|
||||||
|
|
||||||
|
if (Json->HasField(TEXT("crossfadeDuration")))
|
||||||
|
{
|
||||||
|
TransNode->CrossfadeDuration = CrossfadeDuration;
|
||||||
|
ChangedCount++;
|
||||||
|
}
|
||||||
|
if (Json->HasField(TEXT("blendMode")))
|
||||||
|
{
|
||||||
|
TransNode->BlendMode = (EAlphaBlendOption)BlendMode;
|
||||||
|
ChangedCount++;
|
||||||
|
}
|
||||||
|
if (Json->HasField(TEXT("priorityOrder")))
|
||||||
|
{
|
||||||
|
TransNode->PriorityOrder = PriorityOrder;
|
||||||
|
ChangedCount++;
|
||||||
|
}
|
||||||
|
if (Json->HasField(TEXT("logicType")))
|
||||||
|
{
|
||||||
|
TransNode->LogicType = (ETransitionLogicType::Type)LogicType;
|
||||||
|
ChangedCount++;
|
||||||
|
}
|
||||||
|
if (Json->HasField(TEXT("bBidirectional")))
|
||||||
|
{
|
||||||
|
TransNode->Bidirectional = BBidirectional;
|
||||||
|
ChangedCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ChangedCount == 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("No properties to update. Provide at least one of: crossfadeDuration, blendMode, priorityOrder, logicType, bBidirectional"));
|
||||||
|
}
|
||||||
|
TransNode->PostEditChange();
|
||||||
|
|
||||||
|
// Compile and save
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(AnimBP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(AnimBP);
|
||||||
|
|
||||||
|
Result->SetNumberField(TEXT("propertiesChanged"), ChangedCount);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Dom/JsonValue.h"
|
||||||
|
#include "Animation/AnimBlueprint.h"
|
||||||
|
#include "Animation/AnimBlueprintGeneratedClass.h"
|
||||||
|
#include "Animation/Skeleton.h"
|
||||||
|
#include "AnimGraphNode_Base.h"
|
||||||
|
#include "Animation/AnimSequence.h"
|
||||||
|
#include "Animation/BlendSpace.h"
|
||||||
|
#include "UMCPHandler_SetBlendSpaceSamplePoints.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
USTRUCT()
|
||||||
|
struct FBlendSpaceSampleEntry
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
FString AnimationAsset;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
float X = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY()
|
||||||
|
float Y = 0.0f;
|
||||||
|
};
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetBlendSpaceSamplePoints : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blend Space asset name or package path"))
|
||||||
|
FString BlendSpace;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Display name for the X axis"))
|
||||||
|
FString AxisXName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Display name for the Y axis"))
|
||||||
|
FString AxisYName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Minimum value for X axis"))
|
||||||
|
float AxisXMin = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum value for X axis"))
|
||||||
|
float AxisXMax = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Minimum value for Y axis"))
|
||||||
|
float AxisYMin = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Maximum value for Y axis"))
|
||||||
|
float AxisYMax = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Array of sample points, each with animationAsset, x, y"))
|
||||||
|
FMCPJsonArray Samples;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set axis parameters and animation sample points on a Blend Space. "
|
||||||
|
"Replaces all existing samples.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (BlendSpace.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: blendSpace"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the blend space
|
||||||
|
MCPAssets<UBlendSpace> Assets;
|
||||||
|
if (!Assets.Exact(BlendSpace).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlendSpace* BS = Assets.Object();
|
||||||
|
|
||||||
|
// Set axis parameters
|
||||||
|
BS->PreEditChange(nullptr);
|
||||||
|
|
||||||
|
const FBlendParameter& ParamX = BS->GetBlendParameter(0);
|
||||||
|
const FBlendParameter& ParamY = BS->GetBlendParameter(1);
|
||||||
|
|
||||||
|
// We need to modify BlendParameters directly — use const_cast since there's no setter API
|
||||||
|
FBlendParameter& MutableParamX = const_cast<FBlendParameter&>(ParamX);
|
||||||
|
FBlendParameter& MutableParamY = const_cast<FBlendParameter&>(ParamY);
|
||||||
|
|
||||||
|
if (!AxisXName.IsEmpty()) MutableParamX.DisplayName = AxisXName;
|
||||||
|
if (Json->HasField(TEXT("axisXMin"))) MutableParamX.Min = AxisXMin;
|
||||||
|
if (Json->HasField(TEXT("axisXMax"))) MutableParamX.Max = AxisXMax;
|
||||||
|
|
||||||
|
if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName;
|
||||||
|
if (Json->HasField(TEXT("axisYMin"))) MutableParamY.Min = AxisYMin;
|
||||||
|
if (Json->HasField(TEXT("axisYMax"))) MutableParamY.Max = AxisYMax;
|
||||||
|
|
||||||
|
// Clear existing samples (delete from end to start)
|
||||||
|
int32 NumExisting = BS->GetNumberOfBlendSamples();
|
||||||
|
for (int32 i = NumExisting - 1; i >= 0; --i)
|
||||||
|
{
|
||||||
|
BS->DeleteSample(i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add new samples
|
||||||
|
int32 SamplesSet = 0;
|
||||||
|
|
||||||
|
for (const TSharedPtr<FJsonValue>& SampleVal : Samples.Array)
|
||||||
|
{
|
||||||
|
FBlendSpaceSampleEntry Entry;
|
||||||
|
if (!MCPUtils::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal, Result)) return;
|
||||||
|
|
||||||
|
UAnimSequence* AnimSeq = nullptr;
|
||||||
|
if (!Entry.AnimationAsset.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UAnimSequence> AnimAssets;
|
||||||
|
if (AnimAssets.Exact(Entry.AnimationAsset).Load())
|
||||||
|
AnimSeq = AnimAssets.Object();
|
||||||
|
}
|
||||||
|
|
||||||
|
FVector SampleValue(Entry.X, Entry.Y, 0.0f);
|
||||||
|
if (AnimSeq)
|
||||||
|
{
|
||||||
|
BS->AddSample(AnimSeq, SampleValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
BS->AddSample(SampleValue);
|
||||||
|
}
|
||||||
|
SamplesSet++;
|
||||||
|
}
|
||||||
|
|
||||||
|
BS->ValidateSampleData();
|
||||||
|
BS->PostEditChange();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
BS->MarkPackageDirty();
|
||||||
|
bool bSaved = MCPUtils::SaveGenericPackage(BS);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set %d samples on Blend Space '%s' (saved: %s)"),
|
||||||
|
SamplesSet, *BS->GetName(), bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("blendSpace"), BS->GetPathName());
|
||||||
|
Result->SetNumberField(TEXT("samplesSet"), SamplesSet);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "K2Node_VariableGet.h"
|
||||||
|
#include "K2Node_VariableSet.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_SetBlueprintVariableMetadata.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetBlueprintVariableMetadata : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Name of the variable to modify"))
|
||||||
|
FString Variable;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Category to assign the variable to"))
|
||||||
|
FString Category;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Tooltip text for the variable"))
|
||||||
|
FString Tooltip;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Replication mode: none, replicated, or repNotify"))
|
||||||
|
FString Replication;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, expose this variable on spawn"))
|
||||||
|
bool ExposeOnSpawn = false;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, mark the variable as private"))
|
||||||
|
bool IsPrivate = false;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Editability mode: editAnywhere, editDefaultsOnly, editInstanceOnly, or none"))
|
||||||
|
FString Editability;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set variable metadata properties such as category, tooltip, "
|
||||||
|
"replication, editability, and visibility flags.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
// Find the variable
|
||||||
|
FName VarFName(*Variable);
|
||||||
|
FBPVariableDescription* VarDesc = nullptr;
|
||||||
|
for (FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
if (Var.VarName == VarFName)
|
||||||
|
{
|
||||||
|
VarDesc = &Var;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VarDesc)
|
||||||
|
{
|
||||||
|
TArray<TSharedPtr<FJsonValue>> AvailableVars;
|
||||||
|
for (const FBPVariableDescription& Var : BP->NewVariables)
|
||||||
|
{
|
||||||
|
AvailableVars.Add(MakeShared<FJsonValueString>(Var.VarName.ToString()));
|
||||||
|
}
|
||||||
|
MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Variable '%s' not found in Blueprint '%s'"), *Variable, *Blueprint));
|
||||||
|
Result->SetArrayField(TEXT("availableVariables"), AvailableVars);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TArray<TSharedPtr<FJsonValue>> Changes;
|
||||||
|
|
||||||
|
// Category
|
||||||
|
if (Json->HasField(TEXT("category")))
|
||||||
|
{
|
||||||
|
FString OldCategory = VarDesc->Category.ToString();
|
||||||
|
VarDesc->Category = FText::FromString(Category);
|
||||||
|
FBlueprintEditorUtils::SetBlueprintVariableCategory(BP, VarFName, nullptr, FText::FromString(Category));
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||||
|
Change->SetStringField(TEXT("field"), TEXT("category"));
|
||||||
|
Change->SetStringField(TEXT("oldValue"), OldCategory);
|
||||||
|
Change->SetStringField(TEXT("newValue"), Category);
|
||||||
|
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip
|
||||||
|
if (Json->HasField(TEXT("tooltip")))
|
||||||
|
{
|
||||||
|
FString OldTooltip;
|
||||||
|
FBlueprintEditorUtils::GetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), OldTooltip);
|
||||||
|
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr, TEXT("tooltip"), Tooltip);
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||||
|
Change->SetStringField(TEXT("field"), TEXT("tooltip"));
|
||||||
|
Change->SetStringField(TEXT("oldValue"), OldTooltip);
|
||||||
|
Change->SetStringField(TEXT("newValue"), Tooltip);
|
||||||
|
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Replication
|
||||||
|
if (Json->HasField(TEXT("replication")))
|
||||||
|
{
|
||||||
|
uint64 OldFlags = VarDesc->PropertyFlags;
|
||||||
|
|
||||||
|
if (Replication == TEXT("none"))
|
||||||
|
{
|
||||||
|
VarDesc->PropertyFlags &= ~CPF_Net;
|
||||||
|
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
||||||
|
VarDesc->RepNotifyFunc = NAME_None;
|
||||||
|
}
|
||||||
|
else if (Replication == TEXT("replicated"))
|
||||||
|
{
|
||||||
|
VarDesc->PropertyFlags |= CPF_Net;
|
||||||
|
VarDesc->PropertyFlags &= ~CPF_RepNotify;
|
||||||
|
VarDesc->RepNotifyFunc = NAME_None;
|
||||||
|
}
|
||||||
|
else if (Replication == TEXT("repNotify"))
|
||||||
|
{
|
||||||
|
VarDesc->PropertyFlags |= CPF_Net | CPF_RepNotify;
|
||||||
|
// Auto-generate RepNotify function name
|
||||||
|
VarDesc->RepNotifyFunc = FName(*FString::Printf(TEXT("OnRep_%s"), *Variable));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Invalid replication value '%s'. Valid: none, replicated, repNotify"), *Replication));
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||||
|
Change->SetStringField(TEXT("field"), TEXT("replication"));
|
||||||
|
Change->SetStringField(TEXT("newValue"), Replication);
|
||||||
|
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExposeOnSpawn
|
||||||
|
if (Json->HasField(TEXT("exposeOnSpawn")))
|
||||||
|
{
|
||||||
|
bool bOld = (VarDesc->PropertyFlags & CPF_ExposeOnSpawn) != 0;
|
||||||
|
if (ExposeOnSpawn)
|
||||||
|
VarDesc->PropertyFlags |= CPF_ExposeOnSpawn;
|
||||||
|
else
|
||||||
|
VarDesc->PropertyFlags &= ~CPF_ExposeOnSpawn;
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||||
|
Change->SetStringField(TEXT("field"), TEXT("exposeOnSpawn"));
|
||||||
|
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
||||||
|
Change->SetStringField(TEXT("newValue"), ExposeOnSpawn ? TEXT("true") : TEXT("false"));
|
||||||
|
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||||
|
}
|
||||||
|
|
||||||
|
// isPrivate
|
||||||
|
if (Json->HasField(TEXT("isPrivate")))
|
||||||
|
{
|
||||||
|
bool bOld = (VarDesc->PropertyFlags & CPF_DisableEditOnInstance) != 0;
|
||||||
|
// In UE5, "private" for Blueprint variables is represented via metadata
|
||||||
|
FBlueprintEditorUtils::SetBlueprintVariableMetaData(BP, VarFName, nullptr,
|
||||||
|
TEXT("BlueprintPrivate"), IsPrivate ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||||
|
Change->SetStringField(TEXT("field"), TEXT("isPrivate"));
|
||||||
|
Change->SetStringField(TEXT("oldValue"), bOld ? TEXT("true") : TEXT("false"));
|
||||||
|
Change->SetStringField(TEXT("newValue"), IsPrivate ? TEXT("true") : TEXT("false"));
|
||||||
|
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Editability (EditAnywhere, EditDefaultsOnly, EditInstanceOnly)
|
||||||
|
if (Json->HasField(TEXT("editability")))
|
||||||
|
{
|
||||||
|
// Clear all edit flags first
|
||||||
|
VarDesc->PropertyFlags &= ~(CPF_Edit | CPF_DisableEditOnInstance | CPF_DisableEditOnTemplate);
|
||||||
|
|
||||||
|
if (Editability == TEXT("editAnywhere"))
|
||||||
|
{
|
||||||
|
VarDesc->PropertyFlags |= CPF_Edit;
|
||||||
|
}
|
||||||
|
else if (Editability == TEXT("editDefaultsOnly"))
|
||||||
|
{
|
||||||
|
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnInstance;
|
||||||
|
}
|
||||||
|
else if (Editability == TEXT("editInstanceOnly"))
|
||||||
|
{
|
||||||
|
VarDesc->PropertyFlags |= CPF_Edit | CPF_DisableEditOnTemplate;
|
||||||
|
}
|
||||||
|
else if (Editability == TEXT("none"))
|
||||||
|
{
|
||||||
|
// All edit flags already cleared
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Invalid editability value '%s'. Valid: editAnywhere, editDefaultsOnly, editInstanceOnly, none"),
|
||||||
|
*Editability));
|
||||||
|
}
|
||||||
|
|
||||||
|
TSharedRef<FJsonObject> Change = MakeShared<FJsonObject>();
|
||||||
|
Change->SetStringField(TEXT("field"), TEXT("editability"));
|
||||||
|
Change->SetStringField(TEXT("newValue"), Editability);
|
||||||
|
Changes.Add(MakeShared<FJsonValueObject>(Change));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Changes.Num() == 0)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("No metadata fields specified. Provide at least one of: category, tooltip, replication, exposeOnSpawn, isPrivate, editability"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: SetVariableMetadata on '%s.%s' — %d field(s) changed"),
|
||||||
|
*Blueprint, *Variable, Changes.Num());
|
||||||
|
|
||||||
|
BP->PreEditChange(nullptr);
|
||||||
|
BP->PostEditChange();
|
||||||
|
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
Result->SetArrayField(TEXT("changes"), Changes);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,219 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPServer.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Engine/Blueprint.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.h"
|
||||||
|
#include "Engine/World.h"
|
||||||
|
#include "Engine/LevelScriptBlueprint.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "EdGraph/EdGraphPin.h"
|
||||||
|
#include "EdGraphSchema_K2.h"
|
||||||
|
#include "K2Node.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_DynamicCast.h"
|
||||||
|
#include "K2Node_CallParentFunction.h"
|
||||||
|
#include "K2Node_IfThenElse.h"
|
||||||
|
#include "K2Node_ExecutionSequence.h"
|
||||||
|
#include "K2Node_MacroInstance.h"
|
||||||
|
#include "K2Node_SpawnActorFromClass.h"
|
||||||
|
#include "K2Node_Select.h"
|
||||||
|
#include "K2Node_Knot.h"
|
||||||
|
#include "EdGraphNode_Comment.h"
|
||||||
|
#include "GameFramework/Actor.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "Kismet2/KismetEditorUtilities.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Misc/PackageName.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "AssetRegistry/IAssetRegistry.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "BlueprintNodeSpawner.h"
|
||||||
|
#include "UMCPHandler_SetClassDefaultValue.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetClassDefaultValue : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Blueprint name or package path"))
|
||||||
|
FString Blueprint;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Property name on the Class Default Object"))
|
||||||
|
FString Property;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New value (parsed according to property type)"))
|
||||||
|
FString Value;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set a default property value on a Blueprint's Class Default Object. "
|
||||||
|
"Handles class references, object references, and simple types.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
|
||||||
|
// Load Blueprint
|
||||||
|
MCPAssets<UBlueprint> Assets;
|
||||||
|
if (!Assets.Exact(Blueprint).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UBlueprint* BP = Assets.Object();
|
||||||
|
|
||||||
|
if (!BP->GeneratedClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Blueprint has no GeneratedClass"));
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* CDO = BP->GeneratedClass->GetDefaultObject();
|
||||||
|
if (!CDO)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Could not get Class Default Object"));
|
||||||
|
}
|
||||||
|
|
||||||
|
FProperty* Prop = BP->GeneratedClass->FindPropertyByName(*Property);
|
||||||
|
if (!Prop)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Property '%s' not found on '%s'"), *Property, *Blueprint));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString OldValue;
|
||||||
|
Prop->ExportTextItem_Direct(OldValue, Prop->ContainerPtrToValuePtr<void>(CDO), nullptr, CDO, PPF_None);
|
||||||
|
|
||||||
|
bool bSuccess = false;
|
||||||
|
FString ActualNewValue;
|
||||||
|
|
||||||
|
// Handle class/soft-class properties (TSubclassOf, TSoftClassPtr)
|
||||||
|
FClassProperty* ClassProp = CastField<FClassProperty>(Prop);
|
||||||
|
FSoftClassProperty* SoftClassProp = CastField<FSoftClassProperty>(Prop);
|
||||||
|
|
||||||
|
if (ClassProp || SoftClassProp)
|
||||||
|
{
|
||||||
|
// Resolve the value to a UClass*
|
||||||
|
UClass* ResolvedClass = nullptr;
|
||||||
|
|
||||||
|
// Try as a C++ class name first
|
||||||
|
for (TObjectIterator<UClass> It; It; ++It)
|
||||||
|
{
|
||||||
|
if (It->GetName() == Value || It->GetName() == Value + TEXT("_C"))
|
||||||
|
{
|
||||||
|
ResolvedClass = *It;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try loading as a Blueprint asset
|
||||||
|
if (!ResolvedClass)
|
||||||
|
{
|
||||||
|
MCPAssets<UBlueprint> ValueAssets;
|
||||||
|
if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ETwo().Load()) return;
|
||||||
|
if (!ValueAssets.Objects().IsEmpty())
|
||||||
|
{
|
||||||
|
if (ValueAssets.Object()->GeneratedClass)
|
||||||
|
ResolvedClass = ValueAssets.Object()->GeneratedClass;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ResolvedClass)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not resolve '%s' to a class"), *Value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate meta class compatibility
|
||||||
|
if (ClassProp)
|
||||||
|
{
|
||||||
|
UClass* MetaClass = ClassProp->MetaClass;
|
||||||
|
if (MetaClass && !ResolvedClass->IsChildOf(MetaClass))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("'%s' is not a subclass of '%s' (required by property '%s')"),
|
||||||
|
*ResolvedClass->GetName(), *MetaClass->GetName(), *Property));
|
||||||
|
}
|
||||||
|
ClassProp->SetPropertyValue_InContainer(CDO, ResolvedClass);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
FSoftObjectPtr SoftPtr(ResolvedClass);
|
||||||
|
SoftClassProp->SetPropertyValue_InContainer(CDO, SoftPtr);
|
||||||
|
}
|
||||||
|
ActualNewValue = ResolvedClass->GetName();
|
||||||
|
bSuccess = true;
|
||||||
|
}
|
||||||
|
// Handle object properties (TObjectPtr, UObject*)
|
||||||
|
else if (FObjectProperty* ObjProp = CastField<FObjectProperty>(Prop))
|
||||||
|
{
|
||||||
|
// Try finding an existing object/asset by name
|
||||||
|
UObject* ResolvedObj = nullptr;
|
||||||
|
|
||||||
|
// Try loading as a Blueprint asset
|
||||||
|
MCPAssets<UBlueprint> ValueAssets;
|
||||||
|
if (!ValueAssets.Exact(Value).AllContent().Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
if (ValueAssets.Object()->GeneratedClass)
|
||||||
|
ResolvedObj = ValueAssets.Object()->GeneratedClass->GetDefaultObject();
|
||||||
|
|
||||||
|
ObjProp->SetPropertyValue_InContainer(CDO, ResolvedObj);
|
||||||
|
ActualNewValue = ResolvedObj->GetName();
|
||||||
|
bSuccess = true;
|
||||||
|
}
|
||||||
|
// Handle simple types via ImportText
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, Prop->ContainerPtrToValuePtr<void>(CDO), CDO, PPF_None);
|
||||||
|
if (ImportResult)
|
||||||
|
{
|
||||||
|
Prop->ExportTextItem_Direct(ActualNewValue, Prop->ContainerPtrToValuePtr<void>(CDO), nullptr, CDO, PPF_None);
|
||||||
|
bSuccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Failed to set property '%s' to '%s' — value could not be parsed for type '%s'"),
|
||||||
|
*Property, *Value, *Prop->GetCPPType()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bSuccess)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Failed to set property value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark modified and save
|
||||||
|
CDO->MarkPackageDirty();
|
||||||
|
BP->Modify();
|
||||||
|
|
||||||
|
FKismetEditorUtilities::CompileBlueprint(BP);
|
||||||
|
bool bSaved = MCPUtils::SaveBlueprintPackage(BP);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set '%s.%s' from '%s' to '%s' (saved: %s)"),
|
||||||
|
*Blueprint, *Property, *OldValue, *ActualNewValue, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("oldValue"), OldValue);
|
||||||
|
Result->SetStringField(TEXT("newValue"), ActualNewValue);
|
||||||
|
Result->SetStringField(TEXT("propertyType"), Prop->GetCPPType());
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_SetMaterialExpressionPosition.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetMaterialExpressionPosition : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the expression to reposition"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New X position"))
|
||||||
|
int32 PosX = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="New Y position"))
|
||||||
|
int32 PosY = 0;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Reposition a material expression node in the material graph editor.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material or material function
|
||||||
|
UMaterial* MaterialObj = nullptr;
|
||||||
|
UMaterialFunction* MatFunc = nullptr;
|
||||||
|
FString AssetDisplayName;
|
||||||
|
|
||||||
|
if (!MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialFunction> MFAssets;
|
||||||
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MatFunc = MFAssets.Object();
|
||||||
|
AssetDisplayName = MatFunc->GetName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MaterialObj = MatAssets.Object();
|
||||||
|
AssetDisplayName = MaterialObj->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
||||||
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find node by GUID
|
||||||
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
||||||
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
||||||
|
{
|
||||||
|
if (!GraphNode) continue;
|
||||||
|
if (GraphNode->NodeGuid.ToString() == Node)
|
||||||
|
{
|
||||||
|
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetMatNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: [DRY RUN] Would move node '%s' to (%d, %d) in '%s'"),
|
||||||
|
*Node, PosX, PosY, *AssetDisplayName);
|
||||||
|
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set position on the graph node
|
||||||
|
TargetMatNode->NodePosX = PosX;
|
||||||
|
TargetMatNode->NodePosY = PosY;
|
||||||
|
|
||||||
|
// Also update the underlying expression position
|
||||||
|
if (TargetMatNode->MaterialExpression)
|
||||||
|
{
|
||||||
|
TargetMatNode->MaterialExpression->MaterialExpressionEditorX = PosX;
|
||||||
|
TargetMatNode->MaterialExpression->MaterialExpressionEditorY = PosY;
|
||||||
|
}
|
||||||
|
|
||||||
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
||||||
|
Asset->PreEditChange(nullptr);
|
||||||
|
Asset->PostEditChange();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Moved node '%s' to (%d, %d) in '%s' (saved: %s)"),
|
||||||
|
*Node, PosX, PosY, *AssetDisplayName, bSaved ? TEXT("true") : TEXT("false"));
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,330 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_SetMaterialExpressionProperty.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetMaterialExpressionProperty : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material name or package path (specify this or materialFunction)"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Material function name or package path (specify this or material)"))
|
||||||
|
FString MaterialFunction;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Node GUID of the expression to modify"))
|
||||||
|
FString Node;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set the value or properties on a material expression node. "
|
||||||
|
"The 'value' field in the JSON payload provides the new value, whose format depends on the expression type.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: 'material' or 'materialFunction'"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Json->HasField(TEXT("value")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material or material function
|
||||||
|
UMaterial* MaterialObj = nullptr;
|
||||||
|
UMaterialFunction* MatFunc = nullptr;
|
||||||
|
FString AssetDisplayName;
|
||||||
|
|
||||||
|
if (!MaterialFunction.IsEmpty())
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterialFunction> MFAssets;
|
||||||
|
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MatFunc = MFAssets.Object();
|
||||||
|
AssetDisplayName = MatFunc->GetName();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
MCPAssets<UMaterial> MatAssets;
|
||||||
|
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
MaterialObj = MatAssets.Object();
|
||||||
|
AssetDisplayName = MaterialObj->GetName();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
|
||||||
|
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
|
||||||
|
if (!Graph)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("'%s' has no material graph"), *AssetDisplayName));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the node by GUID
|
||||||
|
UMaterialGraphNode* TargetMatNode = nullptr;
|
||||||
|
for (UEdGraphNode* GraphNode : Graph->Nodes)
|
||||||
|
{
|
||||||
|
if (!GraphNode) continue;
|
||||||
|
if (GraphNode->NodeGuid.ToString() == Node)
|
||||||
|
{
|
||||||
|
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TargetMatNode)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' not found in material graph"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
|
||||||
|
if (!Expr)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Node '%s' has no associated material expression"), *Node));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ExprType;
|
||||||
|
FString NewValueStr;
|
||||||
|
|
||||||
|
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
|
||||||
|
Asset->PreEditChange(nullptr);
|
||||||
|
|
||||||
|
// Handle based on expression type
|
||||||
|
if (UMaterialExpressionConstant* ConstExpr = Cast<UMaterialExpressionConstant>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("Constant");
|
||||||
|
double Value = Json->GetNumberField(TEXT("value"));
|
||||||
|
ConstExpr->R = (float)Value;
|
||||||
|
NewValueStr = FString::Printf(TEXT("%f"), Value);
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionConstant3Vector* C3Expr = Cast<UMaterialExpressionConstant3Vector>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("Constant3Vector");
|
||||||
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||||
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
||||||
|
{
|
||||||
|
double R = 0, G = 0, B = 0;
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
|
||||||
|
C3Expr->Constant = FLinearColor((float)R, (float)G, (float)B);
|
||||||
|
NewValueStr = FString::Printf(TEXT("(%f, %f, %f)"), R, G, B);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset->PostEditChange();
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Constant3Vector requires value as object {r, g, b}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionConstant4Vector* C4Expr = Cast<UMaterialExpressionConstant4Vector>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("Constant4Vector");
|
||||||
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||||
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
||||||
|
{
|
||||||
|
double R = 0, G = 0, B = 0, A = 1;
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("a"), A);
|
||||||
|
C4Expr->Constant = FLinearColor((float)R, (float)G, (float)B, (float)A);
|
||||||
|
NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset->PostEditChange();
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Constant4Vector requires value as object {r, g, b, a}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionScalarParameter* SPExpr = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("ScalarParameter");
|
||||||
|
double Value = Json->GetNumberField(TEXT("value"));
|
||||||
|
SPExpr->DefaultValue = (float)Value;
|
||||||
|
NewValueStr = FString::Printf(TEXT("%f"), Value);
|
||||||
|
|
||||||
|
FString ParamName;
|
||||||
|
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
|
||||||
|
{
|
||||||
|
SPExpr->ParameterName = FName(*ParamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionVectorParameter* VPExpr = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("VectorParameter");
|
||||||
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||||
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
||||||
|
{
|
||||||
|
double R = 0, G = 0, B = 0, A = 1;
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("a"), A);
|
||||||
|
VPExpr->DefaultValue = FLinearColor((float)R, (float)G, (float)B, (float)A);
|
||||||
|
NewValueStr = FString::Printf(TEXT("(%f, %f, %f, %f)"), R, G, B, A);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset->PostEditChange();
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("VectorParameter requires value as object {r, g, b, a}"));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString ParamName;
|
||||||
|
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
|
||||||
|
{
|
||||||
|
VPExpr->ParameterName = FName(*ParamName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionTextureCoordinate* TCExpr = Cast<UMaterialExpressionTextureCoordinate>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("TextureCoordinate");
|
||||||
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||||
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
||||||
|
{
|
||||||
|
double CoordIndex = 0, UTiling = 1, VTiling = 1;
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling);
|
||||||
|
(*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling);
|
||||||
|
TCExpr->CoordinateIndex = (int32)CoordIndex;
|
||||||
|
TCExpr->UTiling = (float)UTiling;
|
||||||
|
TCExpr->VTiling = (float)VTiling;
|
||||||
|
NewValueStr = FString::Printf(TEXT("(index=%d, uTiling=%f, vTiling=%f)"), (int32)CoordIndex, UTiling, VTiling);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset->PostEditChange();
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("TextureCoordinate requires value as object {coordinateIndex, uTiling, vTiling}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionCustom* CustomExpr = Cast<UMaterialExpressionCustom>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("Custom");
|
||||||
|
FString Code;
|
||||||
|
if (Json->TryGetStringField(TEXT("code"), Code))
|
||||||
|
{
|
||||||
|
CustomExpr->Code = Code;
|
||||||
|
NewValueStr = FString::Printf(TEXT("Code: %d chars"), Code.Len());
|
||||||
|
}
|
||||||
|
else if (Json->HasField(TEXT("value")))
|
||||||
|
{
|
||||||
|
// Also accept code via value field as string
|
||||||
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
||||||
|
if (!ValueStr.IsEmpty())
|
||||||
|
{
|
||||||
|
CustomExpr->Code = ValueStr;
|
||||||
|
NewValueStr = FString::Printf(TEXT("Code: %d chars"), ValueStr.Len());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FString OutputTypeStr;
|
||||||
|
if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty())
|
||||||
|
{
|
||||||
|
int64 EnumVal = StaticEnum<ECustomMaterialOutputType>()->GetValueByNameString(OutputTypeStr);
|
||||||
|
if (EnumVal != INDEX_NONE)
|
||||||
|
{
|
||||||
|
CustomExpr->OutputType = (ECustomMaterialOutputType)EnumVal;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UMaterialExpressionComponentMask* CMExpr = Cast<UMaterialExpressionComponentMask>(Expr))
|
||||||
|
{
|
||||||
|
ExprType = TEXT("ComponentMask");
|
||||||
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||||
|
if (Json->TryGetObjectField(TEXT("value"), ValueObj) && ValueObj && (*ValueObj).IsValid())
|
||||||
|
{
|
||||||
|
bool bR = false, bG = false, bB = false, bA = false;
|
||||||
|
(*ValueObj)->TryGetBoolField(TEXT("r"), bR);
|
||||||
|
(*ValueObj)->TryGetBoolField(TEXT("g"), bG);
|
||||||
|
(*ValueObj)->TryGetBoolField(TEXT("b"), bB);
|
||||||
|
(*ValueObj)->TryGetBoolField(TEXT("a"), bA);
|
||||||
|
CMExpr->R = bR ? 1 : 0;
|
||||||
|
CMExpr->G = bG ? 1 : 0;
|
||||||
|
CMExpr->B = bB ? 1 : 0;
|
||||||
|
CMExpr->A = bA ? 1 : 0;
|
||||||
|
NewValueStr = FString::Printf(TEXT("(R=%s, G=%s, B=%s, A=%s)"),
|
||||||
|
bR ? TEXT("true") : TEXT("false"),
|
||||||
|
bG ? TEXT("true") : TEXT("false"),
|
||||||
|
bB ? TEXT("true") : TEXT("false"),
|
||||||
|
bA ? TEXT("true") : TEXT("false"));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset->PostEditChange();
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("ComponentMask requires value as object {r, g, b, a} (booleans)"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Asset->PostEditChange();
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Expression type '%s' does not support direct value setting. Supported types: Constant, "
|
||||||
|
"Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, TextureCoordinate, "
|
||||||
|
"Custom, ComponentMask"),
|
||||||
|
*Expr->GetClass()->GetName()));
|
||||||
|
}
|
||||||
|
|
||||||
|
Asset->PostEditChange();
|
||||||
|
Asset->MarkPackageDirty();
|
||||||
|
|
||||||
|
// Save
|
||||||
|
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: Set expression value on node '%s' (%s) in '%s': %s"),
|
||||||
|
*Node, *ExprType, *AssetDisplayName, *NewValueStr);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), AssetDisplayName);
|
||||||
|
Result->SetStringField(TEXT("expressionType"), ExprType);
|
||||||
|
Result->SetStringField(TEXT("newValue"), NewValueStr);
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "Materials/MaterialInterface.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialExpressionScalarParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionVectorParameter.h"
|
||||||
|
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
|
||||||
|
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
|
||||||
|
#include "Factories/MaterialInstanceConstantFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "Engine/Texture.h"
|
||||||
|
#include "UMCPHandler_SetMaterialInstanceParameter.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetMaterialInstanceParameter : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material Instance name or path"))
|
||||||
|
FString MaterialInstance;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Parameter name to set"))
|
||||||
|
FString ParameterName;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Value to set (number for scalar, object with r/g/b/a for vector, string path for texture, bool for staticSwitch)"))
|
||||||
|
FMCPJsonObject Value;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="Parameter type: scalar, vector, texture, staticSwitch. Auto-detected from parent if omitted."))
|
||||||
|
FString Type;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, validate without applying changes"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set a parameter override on a Material Instance. "
|
||||||
|
"Supports scalar, vector, texture, and static switch parameter types.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (!Json->HasField(TEXT("value")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the Material Instance
|
||||||
|
MCPAssets<UMaterialInstanceConstant> Assets;
|
||||||
|
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterialInstanceConstant* MI = Assets.Object();
|
||||||
|
|
||||||
|
// Determine the parameter type — explicit or auto-detect from parent
|
||||||
|
FString TypeStr = Type;
|
||||||
|
|
||||||
|
// Auto-detect type from parent material's parameters if not provided
|
||||||
|
if (TypeStr.IsEmpty())
|
||||||
|
{
|
||||||
|
UMaterialInterface* ParentMat = MI->Parent;
|
||||||
|
while (ParentMat)
|
||||||
|
{
|
||||||
|
UMaterial* BaseMat = ParentMat->GetMaterial();
|
||||||
|
if (BaseMat)
|
||||||
|
{
|
||||||
|
// Check scalar parameters
|
||||||
|
for (UMaterialExpression* Expr : BaseMat->GetExpressions())
|
||||||
|
{
|
||||||
|
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
|
||||||
|
{
|
||||||
|
if (SP->ParameterName.ToString() == ParameterName)
|
||||||
|
{
|
||||||
|
TypeStr = TEXT("scalar");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
|
||||||
|
{
|
||||||
|
if (VP->ParameterName.ToString() == ParameterName)
|
||||||
|
{
|
||||||
|
TypeStr = TEXT("vector");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
|
||||||
|
{
|
||||||
|
if (TP->ParameterName.ToString() == ParameterName)
|
||||||
|
{
|
||||||
|
TypeStr = TEXT("texture");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
|
||||||
|
{
|
||||||
|
if (SSP->ParameterName.ToString() == ParameterName)
|
||||||
|
{
|
||||||
|
TypeStr = TEXT("staticSwitch");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break; // Only need to check the base material
|
||||||
|
}
|
||||||
|
// Walk up the parent chain if it's an MI parented to another MI
|
||||||
|
UMaterialInstanceConstant* ParentMI = Cast<UMaterialInstanceConstant>(ParentMat);
|
||||||
|
if (ParentMI)
|
||||||
|
{
|
||||||
|
ParentMat = ParentMI->Parent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TypeStr.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Could not determine parameter type for '%s'. Specify the 'type' field explicitly (scalar, vector, texture, staticSwitch)."),
|
||||||
|
*ParameterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
FString NewValueDescription;
|
||||||
|
FMaterialParameterInfo ParamInfo(*ParameterName);
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' (type=%s) on Material Instance '%s'"),
|
||||||
|
DryRun ? TEXT("[DRY RUN] Setting") : TEXT("Setting"),
|
||||||
|
*ParameterName, *TypeStr, *MaterialInstance);
|
||||||
|
|
||||||
|
if (TypeStr.Equals(TEXT("scalar"), ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
// Scalar parameter — value is a number
|
||||||
|
double FloatValue = Json->GetNumberField(TEXT("value"));
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MI->SetScalarParameterValueEditorOnly(ParamInfo, (float)FloatValue);
|
||||||
|
}
|
||||||
|
NewValueDescription = FString::Printf(TEXT("%f"), FloatValue);
|
||||||
|
}
|
||||||
|
else if (TypeStr.Equals(TEXT("vector"), ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
// Vector parameter — value is { r, g, b, a? }
|
||||||
|
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
|
||||||
|
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("For vector parameters, 'value' must be an object with r, g, b (and optional a) fields."));
|
||||||
|
}
|
||||||
|
|
||||||
|
double R = (*ValueObj)->GetNumberField(TEXT("r"));
|
||||||
|
double G = (*ValueObj)->GetNumberField(TEXT("g"));
|
||||||
|
double B = (*ValueObj)->GetNumberField(TEXT("b"));
|
||||||
|
double A = (*ValueObj)->HasField(TEXT("a")) ? (*ValueObj)->GetNumberField(TEXT("a")) : 1.0;
|
||||||
|
|
||||||
|
FLinearColor Color((float)R, (float)G, (float)B, (float)A);
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MI->SetVectorParameterValueEditorOnly(ParamInfo, Color);
|
||||||
|
}
|
||||||
|
NewValueDescription = FString::Printf(TEXT("(R=%f, G=%f, B=%f, A=%f)"), R, G, B, A);
|
||||||
|
}
|
||||||
|
else if (TypeStr.Equals(TEXT("texture"), ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
// Texture parameter — value is a texture path string
|
||||||
|
FString TexturePath = Json->GetStringField(TEXT("value"));
|
||||||
|
if (TexturePath.IsEmpty())
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("For texture parameters, 'value' must be a texture asset path string."));
|
||||||
|
}
|
||||||
|
|
||||||
|
UTexture* TextureObj = LoadObject<UTexture>(nullptr, *TexturePath);
|
||||||
|
if (!TextureObj)
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(TEXT("Could not load texture at path '%s'"), *TexturePath));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MI->SetTextureParameterValueEditorOnly(ParamInfo, TextureObj);
|
||||||
|
}
|
||||||
|
NewValueDescription = TexturePath;
|
||||||
|
}
|
||||||
|
else if (TypeStr.Equals(TEXT("staticSwitch"), ESearchCase::IgnoreCase))
|
||||||
|
{
|
||||||
|
// Static switch parameter — value is a bool
|
||||||
|
bool bSwitchValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
// Modify static parameters
|
||||||
|
FStaticParameterSet StaticParams;
|
||||||
|
MI->GetStaticParameterValues(StaticParams);
|
||||||
|
|
||||||
|
bool bFound = false;
|
||||||
|
for (FStaticSwitchParameter& Param : StaticParams.StaticSwitchParameters)
|
||||||
|
{
|
||||||
|
if (Param.ParameterInfo.Name == FName(*ParameterName))
|
||||||
|
{
|
||||||
|
Param.Value = bSwitchValue;
|
||||||
|
Param.bOverride = true;
|
||||||
|
bFound = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!bFound)
|
||||||
|
{
|
||||||
|
// Add new static switch parameter entry
|
||||||
|
FStaticSwitchParameter NewParam;
|
||||||
|
NewParam.ParameterInfo.Name = FName(*ParameterName);
|
||||||
|
NewParam.Value = bSwitchValue;
|
||||||
|
NewParam.bOverride = true;
|
||||||
|
StaticParams.StaticSwitchParameters.Add(NewParam);
|
||||||
|
}
|
||||||
|
|
||||||
|
MI->UpdateStaticPermutation(StaticParams);
|
||||||
|
}
|
||||||
|
NewValueDescription = bSwitchValue ? TEXT("true") : TEXT("false");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Unknown parameter type '%s'. Valid types: scalar, vector, texture, staticSwitch"),
|
||||||
|
*TypeStr));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MI->PreEditChange(nullptr);
|
||||||
|
MI->PostEditChange();
|
||||||
|
MI->MarkPackageDirty();
|
||||||
|
MCPUtils::SaveGenericPackage(MI);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %s parameter '%s' = %s on '%s'"),
|
||||||
|
DryRun ? TEXT("[DRY RUN] Would set") : TEXT("Set"),
|
||||||
|
*ParameterName, *NewValueDescription, *MaterialInstance);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("type"), TypeStr);
|
||||||
|
Result->SetStringField(TEXT("newValue"), NewValueDescription);
|
||||||
|
if (DryRun)
|
||||||
|
{
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,257 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "CoreMinimal.h"
|
||||||
|
#include "MCPHandler.h"
|
||||||
|
#include "MCPAssetFinder.h"
|
||||||
|
#include "MCPUtils.h"
|
||||||
|
#include "Materials/Material.h"
|
||||||
|
#include "MaterialDomain.h"
|
||||||
|
#include "Materials/MaterialInstanceConstant.h"
|
||||||
|
#include "Materials/MaterialFunction.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 "MaterialGraph/MaterialGraph.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphNode.h"
|
||||||
|
#include "MaterialGraph/MaterialGraphSchema.h"
|
||||||
|
#include "Factories/MaterialFactoryNew.h"
|
||||||
|
#include "Factories/MaterialFunctionFactoryNew.h"
|
||||||
|
#include "AssetToolsModule.h"
|
||||||
|
#include "IAssetTools.h"
|
||||||
|
#include "AssetRegistry/AssetRegistryModule.h"
|
||||||
|
#include "EdGraph/EdGraph.h"
|
||||||
|
#include "EdGraph/EdGraphNode.h"
|
||||||
|
#include "Serialization/JsonReader.h"
|
||||||
|
#include "Serialization/JsonWriter.h"
|
||||||
|
#include "Serialization/JsonSerializer.h"
|
||||||
|
#include "Misc/Guid.h"
|
||||||
|
#include "Misc/FileHelper.h"
|
||||||
|
#include "Misc/Paths.h"
|
||||||
|
#include "UObject/SavePackage.h"
|
||||||
|
#include "UObject/UObjectIterator.h"
|
||||||
|
#include "Kismet2/BlueprintEditorUtils.h"
|
||||||
|
#include "UMCPHandler_SetMaterialProperty.generated.h"
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
UCLASS()
|
||||||
|
class UMCPHandler_SetMaterialProperty : public UObject, public IMCPHandler
|
||||||
|
{
|
||||||
|
GENERATED_BODY()
|
||||||
|
|
||||||
|
public:
|
||||||
|
UPROPERTY(meta=(Description="Material name or package path"))
|
||||||
|
FString Material;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Description="Property name to set (domain, blendMode, twoSided, shadingModel, opacity, opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, ditheredLODTransition, bAllowNegativeEmissiveColor)"))
|
||||||
|
FString Property;
|
||||||
|
|
||||||
|
UPROPERTY(meta=(Optional, Description="If true, preview the change without applying it"))
|
||||||
|
bool DryRun = false;
|
||||||
|
|
||||||
|
virtual FString GetDescription() const override
|
||||||
|
{
|
||||||
|
return TEXT("Set a top-level material property such as domain, blend mode, shading model, or usage flags. "
|
||||||
|
"The 'value' field in the JSON payload provides the new value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
virtual void Handle(const FJsonObject* Json, FJsonObject* Result) override
|
||||||
|
{
|
||||||
|
if (!Json->HasField(TEXT("value")))
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, TEXT("Missing required field: value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load material
|
||||||
|
MCPAssets<UMaterial> Assets;
|
||||||
|
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
|
||||||
|
UMaterial* MaterialObj = Assets.Object();
|
||||||
|
|
||||||
|
FString OldValue;
|
||||||
|
FString NewValue;
|
||||||
|
|
||||||
|
if (Property == TEXT("domain"))
|
||||||
|
{
|
||||||
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
||||||
|
OldValue = MCPUtils::EnumToString(MaterialObj->MaterialDomain, TEXT("MD_"));
|
||||||
|
|
||||||
|
EMaterialDomain NewDomain;
|
||||||
|
if (!MCPUtils::StringToEnum(ValueStr, NewDomain, Result, TEXT("MD_"))) return;
|
||||||
|
NewValue = MCPUtils::EnumToString(NewDomain, TEXT("MD_"));
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->MaterialDomain = NewDomain;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("blendMode"))
|
||||||
|
{
|
||||||
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
||||||
|
OldValue = MCPUtils::EnumToString(MaterialObj->BlendMode, TEXT("BLEND_"));
|
||||||
|
|
||||||
|
EBlendMode NewBlend;
|
||||||
|
if (!MCPUtils::StringToEnum(ValueStr, NewBlend, Result, TEXT("BLEND_"))) return;
|
||||||
|
NewValue = MCPUtils::EnumToString(NewBlend, TEXT("BLEND_"));
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->BlendMode = NewBlend;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("twoSided"))
|
||||||
|
{
|
||||||
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
OldValue = MaterialObj->TwoSided ? TEXT("true") : TEXT("false");
|
||||||
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->TwoSided = bValue ? 1 : 0;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("shadingModel"))
|
||||||
|
{
|
||||||
|
FString ValueStr = Json->GetStringField(TEXT("value"));
|
||||||
|
OldValue = MCPUtils::EnumToString(MaterialObj->GetShadingModels().GetFirstShadingModel(), TEXT("MSM_"));
|
||||||
|
|
||||||
|
EMaterialShadingModel NewModel;
|
||||||
|
if (!MCPUtils::StringToEnum(ValueStr, NewModel, Result, TEXT("MSM_"))) return;
|
||||||
|
NewValue = MCPUtils::EnumToString(NewModel, TEXT("MSM_"));
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->SetShadingModel(NewModel);
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("opacity") || Property == TEXT("opacityMaskClipValue"))
|
||||||
|
{
|
||||||
|
double OpacityValue = Json->GetNumberField(TEXT("value"));
|
||||||
|
OldValue = FString::Printf(TEXT("%f"), MaterialObj->OpacityMaskClipValue);
|
||||||
|
NewValue = FString::Printf(TEXT("%f"), OpacityValue);
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->OpacityMaskClipValue = (float)OpacityValue;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("bUsedWithSkeletalMesh"))
|
||||||
|
{
|
||||||
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
OldValue = MaterialObj->bUsedWithSkeletalMesh ? TEXT("true") : TEXT("false");
|
||||||
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->bUsedWithSkeletalMesh = bValue ? 1 : 0;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("bUsedWithMorphTargets"))
|
||||||
|
{
|
||||||
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
OldValue = MaterialObj->bUsedWithMorphTargets ? TEXT("true") : TEXT("false");
|
||||||
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->bUsedWithMorphTargets = bValue ? 1 : 0;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("bUsedWithNiagaraSprites"))
|
||||||
|
{
|
||||||
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
OldValue = MaterialObj->bUsedWithNiagaraSprites ? TEXT("true") : TEXT("false");
|
||||||
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->bUsedWithNiagaraSprites = bValue ? 1 : 0;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("ditheredLODTransition") || Property == TEXT("DitheredLODTransition"))
|
||||||
|
{
|
||||||
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
OldValue = MaterialObj->DitheredLODTransition ? TEXT("true") : TEXT("false");
|
||||||
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->DitheredLODTransition = bValue ? 1 : 0;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Property == TEXT("bAllowNegativeEmissiveColor"))
|
||||||
|
{
|
||||||
|
bool bValue = Json->GetBoolField(TEXT("value"));
|
||||||
|
OldValue = MaterialObj->bAllowNegativeEmissiveColor ? TEXT("true") : TEXT("false");
|
||||||
|
NewValue = bValue ? TEXT("true") : TEXT("false");
|
||||||
|
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
MaterialObj->PreEditChange(nullptr);
|
||||||
|
MaterialObj->bAllowNegativeEmissiveColor = bValue ? 1 : 0;
|
||||||
|
MaterialObj->PostEditChange();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
return MCPUtils::MakeErrorJson(Result, FString::Printf(
|
||||||
|
TEXT("Unknown property '%s'. Valid properties: domain, blendMode, twoSided, shadingModel, opacity, "
|
||||||
|
"opacityMaskClipValue, bUsedWithSkeletalMesh, bUsedWithMorphTargets, bUsedWithNiagaraSprites, "
|
||||||
|
"ditheredLODTransition, bAllowNegativeEmissiveColor"),
|
||||||
|
*Property));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save if not dry run
|
||||||
|
bool bSaved = false;
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
bSaved = MCPUtils::SaveMaterialPackage(MaterialObj);
|
||||||
|
}
|
||||||
|
|
||||||
|
UE_LOG(LogTemp, Display, TEXT("BlueprintMCP: %sSet material property '%s' on '%s': '%s' -> '%s'"),
|
||||||
|
DryRun ? TEXT("[DRY RUN] ") : TEXT(""),
|
||||||
|
*Property, *Material, *OldValue, *NewValue);
|
||||||
|
|
||||||
|
Result->SetStringField(TEXT("material"), MaterialObj->GetName());
|
||||||
|
Result->SetStringField(TEXT("oldValue"), OldValue);
|
||||||
|
Result->SetStringField(TEXT("newValue"), NewValue);
|
||||||
|
Result->SetBoolField(TEXT("dryRun"), DryRun);
|
||||||
|
if (!DryRun)
|
||||||
|
{
|
||||||
|
Result->SetBoolField(TEXT("saved"), bSaved);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user