Remove the deprecated crap from UE Wingman

This commit is contained in:
2026-03-23 17:48:00 -04:00
parent 2bc06f3b02
commit 928abb5916
39 changed files with 0 additions and 5491 deletions

View File

@@ -1,106 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingPackageMaker.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/AnimInstance.h"
#include "Animation/Skeleton.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "AnimBlueprint_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_AnimBlueprint_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new Animation Blueprint (e.g. '/Game/AnimBP/ABP_Character')"))
FString AssetPath;
UPROPERTY(meta=(Description="Skeleton asset package path"))
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() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Resolve skeleton
WingFetcher SkeletonFetcher;
USkeleton* SkeletonObj = SkeletonFetcher.Asset(Skeleton).Cast<USkeleton>();
if (!SkeletonObj) return;
// Resolve parent class (default: UAnimInstance)
UClass* ParentClassObj = UAnimInstance::StaticClass();
if (!ParentClass.IsEmpty() && !ParentClass.Equals(TEXT("AnimInstance"), ESearchCase::IgnoreCase))
{
UClass* Found = nullptr;
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->IsChildOf(UAnimInstance::StaticClass()) && WingUtils::Identifies(ParentClass, *It))
{
Found = *It;
break;
}
}
if (!Found)
{
UWingServer::Printf(TEXT("ERROR: Parent class '%s' not found (must derive from AnimInstance)\n"), *ParentClass);
return;
}
ParentClassObj = Found;
}
// Create the package and Animation Blueprint
if (!Maker.Make()) return;
UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>(
FKismetEditorUtilities::CreateBlueprint(
ParentClassObj,
Maker.Package(),
FName(*Maker.Name()),
BPTYPE_Normal,
UAnimBlueprint::StaticClass(),
UAnimBlueprintGeneratedClass::StaticClass()
));
if (!NewAnimBP)
{
UWingServer::Print(TEXT("ERROR: FKismetEditorUtilities::CreateBlueprint returned null\n"));
return;
}
// Set target skeleton
NewAnimBP->TargetSkeleton = SkeletonObj;
// Compile
FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
UWingServer::Printf(TEXT("Created: %s\n"), *AssetPath);
UWingServer::Printf(TEXT("ParentClass: %s\n"), *WingUtils::FormatName(ParentClassObj));
TArray<UEdGraph*> Graphs = WingUtils::AllGraphs(NewAnimBP);
for (UEdGraph* Graph : Graphs)
{
UWingServer::Printf(TEXT("Graph: %s\n"), *WingUtils::FormatName(Graph));
}
}
};

View File

@@ -1,65 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Animation/AnimBlueprint.h"
#include "AnimGraphNode_Base.h"
#include "AnimBlueprint_ListSyncGroups.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_AnimBlueprint_ListSyncGroups : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to an Animation Blueprint, e.g. /Game/Foo/ABP_Character"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("List all sync group names used in an Animation Blueprint.");
}
virtual void Handle() override
{
WingFetcher F;
UAnimBlueprint* AnimBP = F.Walk(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Walk all anim nodes to collect sync group names
TSet<FString> SyncGroupNames;
for (UAnimGraphNode_Base* AnimNode : WingUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
{
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());
}
}
}
}
if (SyncGroupNames.Num() == 0)
{
UWingServer::Print(TEXT("No sync groups found.\n"));
return;
}
for (const FString& Group : SyncGroupNames)
{
UWingServer::Printf(TEXT("%s\n"), *Group);
}
}
};

View File

@@ -1,131 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "AnimBlueprint_SetBlendSpaceSamples.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FBlendSpaceSampleEntry
{
GENERATED_BODY()
UPROPERTY()
FString AnimationAsset;
UPROPERTY()
float X = 0.0f;
UPROPERTY()
float Y = 0.0f;
};
UCLASS()
class UWing_AnimBlueprint_SetBlendSpaceSamples : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blend Space 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"))
FWingJsonArray 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() override
{
// Load the blend space
WingFetcher F;
UBlendSpace* BS = F.Asset(BlendSpace).Cast<UBlendSpace>();
if (!BS) return;
// Set axis parameters
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 (AxisXMin != 0.0f) MutableParamX.Min = AxisXMin;
if (AxisXMax != 0.0f) MutableParamX.Max = AxisXMax;
if (!AxisYName.IsEmpty()) MutableParamY.DisplayName = AxisYName;
if (AxisYMin != 0.0f) MutableParamY.Min = AxisYMin;
if (AxisYMax != 0.0f) 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 (!WingJson::PopulateFromJson(FBlendSpaceSampleEntry::StaticStruct(), &Entry, SampleVal)) return;
UAnimSequence* AnimSeq = nullptr;
if (!Entry.AnimationAsset.IsEmpty())
{
WingFetcher F2;
AnimSeq = F2.Asset(Entry.AnimationAsset).Cast<UAnimSequence>();
if (!AnimSeq) return;
}
FVector SampleValue(Entry.X, Entry.Y, 0.0f);
if (AnimSeq)
{
BS->AddSample(AnimSeq, SampleValue);
}
else
{
BS->AddSample(SampleValue);
}
SamplesSet++;
}
BS->ValidateSampleData();
UWingServer::Printf(TEXT("Set %d samples on %s\n"), SamplesSet, *WingUtils::FormatName(BS));
}
};

View File

@@ -1,62 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingPackageMaker.h"
#include "Animation/Skeleton.h"
#include "Animation/BlendSpace.h"
#include "BlendSpace_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlendSpace_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new Blend Space (e.g. '/Game/BlendSpaces/BS_Locomotion')"))
FString AssetPath;
UPROPERTY(meta=(Description="Skeleton asset package path"))
FString Skeleton;
virtual FString GetDescription() const override
{
return TEXT("Create a new 2D Blend Space asset with a specified skeleton.");
}
virtual void Handle() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Resolve skeleton.
WingFetcher SkeletonFetcher;
USkeleton* SkeletonObj = SkeletonFetcher.Asset(Skeleton).Cast<USkeleton>();
if (!SkeletonObj) return;
// Create the package and Blend Space.
if (!Maker.Make()) return;
UBlendSpace* NewBS = NewObject<UBlendSpace>(Maker.Package(), FName(*Maker.Name()), RF_Public | RF_Standalone);
if (!NewBS)
{
UWingServer::Print(TEXT("ERROR: Failed to create Blend Space object\n"));
return;
}
// Set skeleton.
NewBS->SetSkeleton(SkeletonObj);
NewBS->MarkPackageDirty();
UWingServer::Printf(TEXT("Created %s\n"), *NewBS->GetPathName());
UWingServer::Printf(TEXT("Skeleton: %s\n"), *SkeletonObj->GetPathName());
}
};

View File

@@ -1,67 +0,0 @@
#include "BlueprintExportSubsystem.h"
#include "BlueprintExporter.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "UObject/Package.h"
#include "Exporters/Exporter.h"
#include "UnrealExporter.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
void UBlueprintExportSubsystem::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
OnAssetSavedHandle = UPackage::PackageSavedWithContextEvent.AddUObject(
this, &UBlueprintExportSubsystem::OnAssetSaved);
}
void UBlueprintExportSubsystem::Deinitialize()
{
UPackage::PackageSavedWithContextEvent.Remove(OnAssetSavedHandle);
Super::Deinitialize();
}
void UBlueprintExportSubsystem::OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context)
{
if (!Package) return;
FString PkDir = FPaths::ProjectDir() / TEXT("Saved") / TEXT("BlueprintExports") / FPaths::GetBaseFilename(PackageFilename);
IFileManager::Get().DeleteDirectory(*PkDir, false, true);
// Export the whole package in both formats for comparison.
{
FStringOutputDevice Archive;
const FExportObjectInnerContext InnerContext;
UExporter::ExportToOutputDevice(&InnerContext, Package, nullptr, Archive, TEXT("copy"), 0);
FFileHelper::SaveStringToFile(Archive, *(PkDir / TEXT("COPY_DUMP.txt")));
}
{
FStringOutputDevice Archive;
const FExportObjectInnerContext InnerContext;
UExporter::ExportToOutputDevice(&InnerContext, Package, nullptr, Archive, TEXT("t3d"), 0);
FFileHelper::SaveStringToFile(Archive, *(PkDir / TEXT("T3D_DUMP.txt")));
}
TArray<UObject*> AllObjects;
GetObjectsWithPackage(Package, AllObjects);
for (UObject *Obj : AllObjects)
{
if (UBlueprint *BP = Cast<UBlueprint>(Obj))
{
FString BPDir = PkDir / BP->GetName();
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
MCPGraphExport Exporter(Graph);
FString FilePath = BPDir / Graph->GetName() + TEXT(".txt");
FString DetailsPath = BPDir / TEXT("DETAILS") / Graph->GetName() + TEXT(".txt");
FFileHelper::SaveStringToFile(Exporter.GetOutput(), *FilePath);
FFileHelper::SaveStringToFile(Exporter.GetDetails(), *DetailsPath);
UE_LOG(LogTemp, Warning, TEXT("Blueprint export: %s"), *FilePath);
}
}
}
}

View File

@@ -1,23 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "EditorSubsystem.h"
#include "UObject/ObjectSaveContext.h"
#include "BlueprintExportSubsystem.generated.h"
/**
* Editor subsystem that exports blueprint text files whenever an asset is saved.
*/
UCLASS()
class UBlueprintExportSubsystem : public UEditorSubsystem
{
GENERATED_BODY()
public:
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override;
private:
void OnAssetSaved(const FString& PackageFilename, UPackage* Package, FObjectPostSaveContext Context);
FDelegateHandle OnAssetSavedHandle;
};

View File

@@ -1,450 +0,0 @@
#include "MCPUtils.h"
#include "Dom/JsonValue.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.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_MacroInstance.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
// Animation Blueprint support
#include "Animation/AnimBlueprint.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_AssetPlayerBase.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_BlendSpacePlayer.h"
#include "AnimGraphNode_Base.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimStateConduitNode.h"
#include "AnimStateEntryNode.h"
#include "AnimationStateMachineGraph.h"
// Material support
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.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/MaterialGraphNode.h"
TSharedRef<FJsonObject> MCPUtils::SerializeBlueprint(UBlueprint* BP)
{
TSharedRef<FJsonObject> J = MakeShared<FJsonObject>();
J->SetStringField(TEXT("name"), BP->GetName());
J->SetStringField(TEXT("path"), BP->GetPackage()->GetName());
J->SetStringField(TEXT("parentClass"), BP->ParentClass ? BP->ParentClass->GetName() : TEXT("None"));
J->SetStringField(TEXT("blueprintType"),
StaticEnum<EBlueprintType>()->GetNameStringByValue((int64)BP->BlueprintType));
// Animation Blueprint detection
if (UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP))
{
J->SetBoolField(TEXT("isAnimBlueprint"), true);
if (AnimBP->TargetSkeleton)
{
J->SetStringField(TEXT("targetSkeleton"), AnimBP->TargetSkeleton->GetName());
J->SetStringField(TEXT("targetSkeletonPath"), AnimBP->TargetSkeleton->GetPathName());
}
}
// Variables
TArray<TSharedPtr<FJsonValue>> Vars;
for (const FBPVariableDescription& V : BP->NewVariables)
{
TSharedRef<FJsonObject> VJ = MakeShared<FJsonObject>();
VJ->SetStringField(TEXT("name"), V.VarName.ToString());
VJ->SetStringField(TEXT("type"), V.VarType.PinCategory.ToString());
if (V.VarType.PinSubCategoryObject.IsValid())
VJ->SetStringField(TEXT("subtype"), V.VarType.PinSubCategoryObject->GetName());
VJ->SetBoolField(TEXT("isArray"), V.VarType.IsArray());
VJ->SetBoolField(TEXT("isSet"), V.VarType.IsSet());
VJ->SetBoolField(TEXT("isMap"), V.VarType.IsMap());
VJ->SetStringField(TEXT("category"), V.Category.ToString());
VJ->SetStringField(TEXT("defaultValue"), V.DefaultValue);
Vars.Add(MakeShared<FJsonValueObject>(VJ));
}
J->SetArrayField(TEXT("variables"), Vars);
// Interfaces
TArray<TSharedPtr<FJsonValue>> Ifaces;
for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces)
{
if (I.Interface)
Ifaces.Add(MakeShared<FJsonValueString>(I.Interface->GetName()));
}
J->SetArrayField(TEXT("interfaces"), Ifaces);
return J;
}
TSharedPtr<FJsonObject> MCPUtils::SerializeNode(UEdGraphNode* Node)
{
TSharedRef<FJsonObject> NJ = MakeShared<FJsonObject>();
NJ->SetStringField(TEXT("id"), Node->NodeGuid.ToString());
NJ->SetStringField(TEXT("class"), Node->GetClass()->GetName());
NJ->SetStringField(TEXT("title"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
if (!Node->NodeComment.IsEmpty())
NJ->SetStringField(TEXT("comment"), Node->NodeComment);
NJ->SetNumberField(TEXT("posX"), Node->NodePosX);
NJ->SetNumberField(TEXT("posY"), Node->NodePosY);
// Material graph node — extract UMaterialExpression data
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("MaterialExpression"));
if (MatNode->MaterialExpression)
{
TSharedPtr<FJsonObject> ExprJson = SerializeMaterialExpression(MatNode->MaterialExpression);
if (ExprJson.IsValid())
{
NJ->SetObjectField(TEXT("expression"), ExprJson);
}
}
}
// Animation Blueprint node types
else if (auto* SMNode = Cast<UAnimGraphNode_StateMachine>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateMachine"));
if (SMNode->EditorStateMachineGraph)
{
NJ->SetStringField(TEXT("stateMachineName"), SMNode->EditorStateMachineGraph->GetName());
int32 StateCount = 0, TransitionCount = 0;
for (UEdGraphNode* SubNode : SMNode->EditorStateMachineGraph->Nodes)
{
if (Cast<UAnimStateNode>(SubNode)) StateCount++;
else if (Cast<UAnimStateTransitionNode>(SubNode)) TransitionCount++;
}
NJ->SetNumberField(TEXT("stateCount"), StateCount);
NJ->SetNumberField(TEXT("transitionCount"), TransitionCount);
}
}
else if (auto* SeqPlayer = Cast<UAnimGraphNode_SequencePlayer>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimSequencePlayer"));
if (UAnimationAsset* Asset = SeqPlayer->GetAnimationAsset())
{
NJ->SetStringField(TEXT("animationAsset"), Asset->GetName());
NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName());
}
}
else if (auto* BSPlayer = Cast<UAnimGraphNode_BlendSpacePlayer>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimBlendSpacePlayer"));
if (UAnimationAsset* Asset = BSPlayer->GetAnimationAsset())
{
NJ->SetStringField(TEXT("blendSpaceAsset"), Asset->GetName());
NJ->SetStringField(TEXT("blendSpaceAssetPath"), Asset->GetPathName());
}
}
else if (auto* AssetPlayer = Cast<UAnimGraphNode_AssetPlayerBase>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimAssetPlayer"));
if (UAnimationAsset* Asset = AssetPlayer->GetAnimationAsset())
{
NJ->SetStringField(TEXT("animationAsset"), Asset->GetName());
NJ->SetStringField(TEXT("animationAssetPath"), Asset->GetPathName());
}
}
else if (Cast<UAnimGraphNode_Base>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimNode"));
}
else if (auto* StateNode = Cast<UAnimStateNode>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimState"));
NJ->SetStringField(TEXT("stateName"), StateNode->GetStateName());
NJ->SetBoolField(TEXT("bAlwaysResetOnEntry"), StateNode->bAlwaysResetOnEntry);
}
else if (auto* TransNode = Cast<UAnimStateTransitionNode>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimTransition"));
if (UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState()))
{
NJ->SetStringField(TEXT("fromState"), FromState->GetStateName());
}
if (UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState()))
{
NJ->SetStringField(TEXT("toState"), ToState->GetStateName());
}
NJ->SetNumberField(TEXT("crossfadeDuration"), TransNode->CrossfadeDuration);
NJ->SetNumberField(TEXT("blendMode"), (int32)TransNode->BlendMode);
NJ->SetNumberField(TEXT("priorityOrder"), TransNode->PriorityOrder);
NJ->SetNumberField(TEXT("logicType"), (int32)TransNode->LogicType.GetValue());
NJ->SetBoolField(TEXT("bBidirectional"), TransNode->Bidirectional);
}
else if (Cast<UAnimStateConduitNode>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimConduit"));
}
else if (Cast<UAnimStateEntryNode>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("AnimStateEntry"));
}
// K2Node specifics — check CallParentFunction before CallFunction (inheritance)
else if (auto* CPF = Cast<UK2Node_CallParentFunction>(Node))
{
NJ->SetStringField(TEXT("functionName"), CPF->FunctionReference.GetMemberName().ToString());
if (CPF->FunctionReference.GetMemberParentClass())
NJ->SetStringField(TEXT("targetClass"), CPF->FunctionReference.GetMemberParentClass()->GetName());
NJ->SetStringField(TEXT("nodeType"), TEXT("CallParentFunction"));
}
else if (auto* CF = Cast<UK2Node_CallFunction>(Node))
{
NJ->SetStringField(TEXT("functionName"), CF->FunctionReference.GetMemberName().ToString());
if (CF->FunctionReference.GetMemberParentClass())
NJ->SetStringField(TEXT("targetClass"), CF->FunctionReference.GetMemberParentClass()->GetName());
}
else if (auto* FE = Cast<UK2Node_FunctionEntry>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("FunctionEntry"));
// Serialize UserDefinedPins (parameter names and types)
TArray<TSharedPtr<FJsonValue>> ParamArr;
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
TSharedRef<FJsonObject> ParamJ = MakeShared<FJsonObject>();
ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
FString ParamType = PinInfo->PinType.PinCategory.ToString();
ParamJ->SetStringField(TEXT("type"), ParamType);
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName());
else if (ParamType == TEXT("None") || ParamType.IsEmpty())
ParamJ->SetBoolField(TEXT("typeUnknown"), true);
ParamArr.Add(MakeShared<FJsonValueObject>(ParamJ));
}
NJ->SetArrayField(TEXT("parameters"), ParamArr);
}
else if (auto* Ev = Cast<UK2Node_Event>(Node))
{
NJ->SetStringField(TEXT("eventName"), Ev->EventReference.GetMemberName().ToString());
NJ->SetStringField(TEXT("nodeType"), Ev->bOverrideFunction ? TEXT("OverrideEvent") : TEXT("Event"));
}
else if (auto* CE = Cast<UK2Node_CustomEvent>(Node))
{
NJ->SetStringField(TEXT("eventName"), CE->CustomFunctionName.ToString());
NJ->SetStringField(TEXT("nodeType"), TEXT("CustomEvent"));
// Serialize UserDefinedPins (parameter names and types)
TArray<TSharedPtr<FJsonValue>> ParamArr;
for (const TSharedPtr<FUserPinInfo>& PinInfo : CE->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
TSharedRef<FJsonObject> ParamJ = MakeShared<FJsonObject>();
ParamJ->SetStringField(TEXT("name"), PinInfo->PinName.ToString());
FString ParamType = PinInfo->PinType.PinCategory.ToString();
ParamJ->SetStringField(TEXT("type"), ParamType);
if (PinInfo->PinType.PinSubCategoryObject.IsValid())
ParamJ->SetStringField(TEXT("subtype"), PinInfo->PinType.PinSubCategoryObject->GetName());
else if (ParamType == TEXT("None") || ParamType.IsEmpty())
ParamJ->SetBoolField(TEXT("typeUnknown"), true);
ParamArr.Add(MakeShared<FJsonValueObject>(ParamJ));
}
NJ->SetArrayField(TEXT("parameters"), ParamArr);
}
else if (auto* VG = Cast<UK2Node_VariableGet>(Node))
{
NJ->SetStringField(TEXT("variableName"), VG->GetVarName().ToString());
NJ->SetStringField(TEXT("nodeType"), TEXT("VariableGet"));
}
else if (auto* VS = Cast<UK2Node_VariableSet>(Node))
{
NJ->SetStringField(TEXT("variableName"), VS->GetVarName().ToString());
NJ->SetStringField(TEXT("nodeType"), TEXT("VariableSet"));
}
else if (auto* MI = Cast<UK2Node_MacroInstance>(Node))
{
if (MI->GetMacroGraph())
NJ->SetStringField(TEXT("macroName"), MI->GetMacroGraph()->GetName());
NJ->SetStringField(TEXT("nodeType"), TEXT("MacroInstance"));
}
else if (auto* DC = Cast<UK2Node_DynamicCast>(Node))
{
if (DC->TargetType)
NJ->SetStringField(TEXT("castTarget"), DC->TargetType->GetName());
NJ->SetStringField(TEXT("nodeType"), TEXT("DynamicCast"));
}
else if (Cast<UK2Node_IfThenElse>(Node))
{
NJ->SetStringField(TEXT("nodeType"), TEXT("Branch"));
}
// Pins
TArray<TSharedPtr<FJsonValue>> Pins;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden) continue;
TSharedPtr<FJsonObject> PJ = SerializePin(Pin);
if (PJ.IsValid())
Pins.Add(MakeShared<FJsonValueObject>(PJ.ToSharedRef()));
}
NJ->SetArrayField(TEXT("pins"), Pins);
return NJ;
}
TSharedPtr<FJsonObject> MCPUtils::SerializePin(UEdGraphPin* Pin)
{
TSharedRef<FJsonObject> PJ = MakeShared<FJsonObject>();
PJ->SetStringField(TEXT("name"), Pin->PinName.ToString());
PJ->SetStringField(TEXT("direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
PJ->SetStringField(TEXT("type"), Pin->PinType.PinCategory.ToString());
if (Pin->PinType.PinSubCategoryObject.IsValid())
PJ->SetStringField(TEXT("subtype"), Pin->PinType.PinSubCategoryObject->GetName());
if (!Pin->DefaultValue.IsEmpty())
PJ->SetStringField(TEXT("defaultValue"), Pin->DefaultValue);
if (Pin->LinkedTo.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> Conns;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
TSharedRef<FJsonObject> CJ = MakeShared<FJsonObject>();
CJ->SetStringField(TEXT("nodeId"), Linked->GetOwningNode()->NodeGuid.ToString());
CJ->SetStringField(TEXT("pinName"), Linked->PinName.ToString());
Conns.Add(MakeShared<FJsonValueObject>(CJ));
}
PJ->SetArrayField(TEXT("connections"), Conns);
}
return PJ;
}
TSharedPtr<FJsonObject> MCPUtils::SerializeMaterialExpression(UMaterialExpression* Expression)
{
if (!Expression) return nullptr;
TSharedRef<FJsonObject> EJ = MakeShared<FJsonObject>();
EJ->SetStringField(TEXT("class"), Expression->GetClass()->GetName());
EJ->SetStringField(TEXT("name"), Expression->GetName());
EJ->SetStringField(TEXT("description"), Expression->GetDescription());
EJ->SetNumberField(TEXT("posX"), Expression->MaterialExpressionEditorX);
EJ->SetNumberField(TEXT("posY"), Expression->MaterialExpressionEditorY);
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("ScalarParameter"));
EJ->SetStringField(TEXT("parameterName"), SP->ParameterName.ToString());
EJ->SetNumberField(TEXT("defaultValue"), SP->DefaultValue);
EJ->SetStringField(TEXT("group"), SP->Group.ToString());
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("VectorParameter"));
EJ->SetStringField(TEXT("parameterName"), VP->ParameterName.ToString());
TSharedRef<FJsonObject> DefVal = MakeShared<FJsonObject>();
DefVal->SetNumberField(TEXT("r"), VP->DefaultValue.R);
DefVal->SetNumberField(TEXT("g"), VP->DefaultValue.G);
DefVal->SetNumberField(TEXT("b"), VP->DefaultValue.B);
DefVal->SetNumberField(TEXT("a"), VP->DefaultValue.A);
EJ->SetObjectField(TEXT("defaultValue"), DefVal);
EJ->SetStringField(TEXT("group"), VP->Group.ToString());
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSampleParameter2D"));
EJ->SetStringField(TEXT("parameterName"), TP->ParameterName.ToString());
if (TP->Texture)
EJ->SetStringField(TEXT("texture"), TP->Texture->GetPathName());
EJ->SetStringField(TEXT("group"), TP->Group.ToString());
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("StaticSwitchParameter"));
EJ->SetStringField(TEXT("parameterName"), SSP->ParameterName.ToString());
EJ->SetBoolField(TEXT("defaultValue"), SSP->DefaultValue);
EJ->SetStringField(TEXT("group"), SSP->Group.ToString());
}
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("Constant"));
EJ->SetNumberField(TEXT("value"), SC->R);
}
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("Constant3Vector"));
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
Val->SetNumberField(TEXT("r"), C3->Constant.R);
Val->SetNumberField(TEXT("g"), C3->Constant.G);
Val->SetNumberField(TEXT("b"), C3->Constant.B);
EJ->SetObjectField(TEXT("value"), Val);
}
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("Constant4Vector"));
TSharedRef<FJsonObject> Val = MakeShared<FJsonObject>();
Val->SetNumberField(TEXT("r"), C4->Constant.R);
Val->SetNumberField(TEXT("g"), C4->Constant.G);
Val->SetNumberField(TEXT("b"), C4->Constant.B);
Val->SetNumberField(TEXT("a"), C4->Constant.A);
EJ->SetObjectField(TEXT("value"), Val);
}
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("TextureSample"));
if (TS->Texture)
EJ->SetStringField(TEXT("texture"), TS->Texture->GetPathName());
}
else if (auto* TC = Cast<UMaterialExpressionTextureCoordinate>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("TextureCoordinate"));
EJ->SetNumberField(TEXT("coordinateIndex"), TC->CoordinateIndex);
EJ->SetNumberField(TEXT("uTiling"), TC->UTiling);
EJ->SetNumberField(TEXT("vTiling"), TC->VTiling);
}
else if (auto* CM = Cast<UMaterialExpressionComponentMask>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("ComponentMask"));
EJ->SetBoolField(TEXT("r"), CM->R != 0);
EJ->SetBoolField(TEXT("g"), CM->G != 0);
EJ->SetBoolField(TEXT("b"), CM->B != 0);
EJ->SetBoolField(TEXT("a"), CM->A != 0);
}
else if (auto* Custom = Cast<UMaterialExpressionCustom>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("Custom"));
EJ->SetStringField(TEXT("code"), Custom->Code);
EJ->SetStringField(TEXT("outputType"), StaticEnum<ECustomMaterialOutputType>()->GetNameStringByValue((int64)Custom->OutputType));
}
else if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionInput"));
EJ->SetStringField(TEXT("inputName"), FI->InputName.ToString());
}
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("FunctionOutput"));
EJ->SetStringField(TEXT("outputName"), FO->OutputName.ToString());
}
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expression))
{
EJ->SetStringField(TEXT("expressionType"), TEXT("MaterialFunctionCall"));
if (MFC->MaterialFunction)
EJ->SetStringField(TEXT("functionName"), MFC->MaterialFunction->GetName());
}
else
{
EJ->SetStringField(TEXT("expressionType"), Expression->GetClass()->GetName());
}
return EJ;
}

View File

@@ -1,242 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Blueprint_Diff.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_Diff : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="First blueprint package path"))
FString BlueprintA;
UPROPERTY(meta=(Description="Second blueprint package path"))
FString BlueprintB;
UPROPERTY(meta=(Optional, Description="Filter to a specific graph name"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Structural diff between two different Blueprints. Compares nodes, "
"connections, and variables across graphs. Use for comparing variants, "
"finding divergence after copy-paste, or auditing consistency.");
}
virtual void Handle() override
{
// Load both blueprints
WingFetcher FA;
UBlueprint* BPA = FA.Asset(BlueprintA).Cast<UBlueprint>();
if (!BPA) return;
WingFetcher FB;
UBlueprint* BPB = FB.Asset(BlueprintB).Cast<UBlueprint>();
if (!BPB) return;
// Gather graphs, optionally filtering by name
auto GatherGraphs = [this](UBlueprint* BP) -> TArray<UEdGraph*>
{
TArray<UEdGraph*> Graphs;
for (UEdGraph* G : BP->UbergraphPages)
{
if (!G) continue;
if (!Graph.IsEmpty() && !WingUtils::Identifies(Graph, G)) continue;
Graphs.Add(G);
}
for (UEdGraph* G : BP->FunctionGraphs)
{
if (!G) continue;
if (!Graph.IsEmpty() && !WingUtils::Identifies(Graph, G)) continue;
Graphs.Add(G);
}
return Graphs;
};
TArray<UEdGraph*> GraphsA = GatherGraphs(BPA);
TArray<UEdGraph*> GraphsB = GatherGraphs(BPB);
// Build graph name maps
TMap<FString, UEdGraph*> GraphMapA, GraphMapB;
for (UEdGraph* G : GraphsA) GraphMapA.Add(WingUtils::FormatName(G), G);
for (UEdGraph* G : GraphsB) GraphMapB.Add(WingUtils::FormatName(G), G);
// Find all unique graph names
TSet<FString> AllGraphNames;
for (auto& Pair : GraphMapA) AllGraphNames.Add(Pair.Key);
for (auto& Pair : GraphMapB) AllGraphNames.Add(Pair.Key);
int32 TotalDiffs = 0;
for (const FString& GraphName : AllGraphNames)
{
UEdGraph** pGA = GraphMapA.Find(GraphName);
UEdGraph** pGB = GraphMapB.Find(GraphName);
if (!pGA)
{
UWingServer::Printf(TEXT("Graph %s: only in B (%d nodes)\n"), *GraphName, (*pGB)->Nodes.Num());
TotalDiffs++;
continue;
}
if (!pGB)
{
UWingServer::Printf(TEXT("Graph %s: only in A (%d nodes)\n"), *GraphName, (*pGA)->Nodes.Num());
TotalDiffs++;
continue;
}
// Both exist -- compare nodes
UEdGraph* GA = *pGA;
UEdGraph* GB = *pGB;
// Build node title maps for matching
TMap<FString, TArray<UEdGraphNode*>> NodesA, NodesB;
for (UEdGraphNode* N : GA->Nodes)
{
if (!N) continue;
NodesA.FindOrAdd(WingUtils::FormatName(N)).Add(N);
}
for (UEdGraphNode* N : GB->Nodes)
{
if (!N) continue;
NodesB.FindOrAdd(WingUtils::FormatName(N)).Add(N);
}
// Nodes only in A
TArray<FString> OnlyInA;
for (auto& Pair : NodesA)
{
int32 CountA = Pair.Value.Num();
int32 CountB = 0;
if (auto* pArr = NodesB.Find(Pair.Key)) CountB = pArr->Num();
if (CountA > CountB)
OnlyInA.Add(FString::Printf(TEXT(" %s (x%d)"), *Pair.Key, CountA - CountB));
}
// Nodes only in B
TArray<FString> OnlyInB;
for (auto& Pair : NodesB)
{
int32 CountB = Pair.Value.Num();
int32 CountA = 0;
if (auto* pArr = NodesA.Find(Pair.Key)) CountA = pArr->Num();
if (CountB > CountA)
OnlyInB.Add(FString::Printf(TEXT(" %s (x%d)"), *Pair.Key, CountB - CountA));
}
// Connection diff
auto MakeConnKey = [](UEdGraphPin* SrcPin, UEdGraphPin* TgtPin) -> FString
{
return FString::Printf(TEXT("%s|%s|%s|%s"),
*WingUtils::FormatName(SrcPin->GetOwningNode()), *WingUtils::FormatName(SrcPin),
*WingUtils::FormatName(TgtPin->GetOwningNode()), *WingUtils::FormatName(TgtPin));
};
auto GatherConnections = [&MakeConnKey](UEdGraph* G) -> TSet<FString>
{
TSet<FString> Conns;
for (UEdGraphNode* N : G->Nodes)
{
if (!N) continue;
for (UEdGraphPin* Pin : N->Pins)
{
if (!Pin || Pin->Direction != EGPD_Output) continue;
for (UEdGraphPin* Linked : Pin->LinkedTo)
{
if (!Linked || !Linked->GetOwningNode()) continue;
Conns.Add(MakeConnKey(Pin, Linked));
}
}
}
return Conns;
};
TSet<FString> ConnectionsA = GatherConnections(GA);
TSet<FString> ConnectionsB = GatherConnections(GB);
TArray<FString> ConnsOnlyInA, ConnsOnlyInB;
for (const FString& Key : ConnectionsA)
if (!ConnectionsB.Contains(Key))
ConnsOnlyInA.Add(FString::Printf(TEXT(" %s"), *Key));
for (const FString& Key : ConnectionsB)
if (!ConnectionsA.Contains(Key))
ConnsOnlyInB.Add(FString::Printf(TEXT(" %s"), *Key));
bool bIdentical = OnlyInA.IsEmpty() && OnlyInB.IsEmpty() && ConnsOnlyInA.IsEmpty() && ConnsOnlyInB.IsEmpty();
if (bIdentical)
{
UWingServer::Printf(TEXT("Graph %s: identical (%d nodes)\n"), *GraphName, GA->Nodes.Num());
continue;
}
TotalDiffs++;
UWingServer::Printf(TEXT("Graph %s: different (A=%d nodes, B=%d nodes)\n"), *GraphName, GA->Nodes.Num(), GB->Nodes.Num());
if (!OnlyInA.IsEmpty())
{
UWingServer::Print(TEXT(" Nodes only in A:\n"));
for (const FString& Line : OnlyInA) UWingServer::Printf(TEXT(" %s\n"), *Line);
}
if (!OnlyInB.IsEmpty())
{
UWingServer::Print(TEXT(" Nodes only in B:\n"));
for (const FString& Line : OnlyInB) UWingServer::Printf(TEXT(" %s\n"), *Line);
}
if (!ConnsOnlyInA.IsEmpty())
{
UWingServer::Print(TEXT(" Connections only in A:\n"));
for (const FString& Line : ConnsOnlyInA) UWingServer::Printf(TEXT(" %s\n"), *Line);
}
if (!ConnsOnlyInB.IsEmpty())
{
UWingServer::Print(TEXT(" Connections only in B:\n"));
for (const FString& Line : ConnsOnlyInB) UWingServer::Printf(TEXT(" %s\n"), *Line);
}
}
// Compare variables
TSet<FString> VarNamesA, VarNamesB;
for (const FBPVariableDescription& V : BPA->NewVariables) VarNamesA.Add(WingUtils::FormatName(V));
for (const FBPVariableDescription& V : BPB->NewVariables) VarNamesB.Add(WingUtils::FormatName(V));
TArray<FString> VarsOnlyInA, VarsOnlyInB;
for (const FString& Name : VarNamesA)
if (!VarNamesB.Contains(Name))
VarsOnlyInA.Add(Name);
for (const FString& Name : VarNamesB)
if (!VarNamesA.Contains(Name))
VarsOnlyInB.Add(Name);
if (!VarsOnlyInA.IsEmpty())
{
UWingServer::Print(TEXT("Variables only in A:\n"));
for (const FString& Name : VarsOnlyInA) UWingServer::Printf(TEXT(" %s\n"), *Name);
TotalDiffs += VarsOnlyInA.Num();
}
if (!VarsOnlyInB.IsEmpty())
{
UWingServer::Print(TEXT("Variables only in B:\n"));
for (const FString& Name : VarsOnlyInB) UWingServer::Printf(TEXT(" %s\n"), *Name);
TotalDiffs += VarsOnlyInB.Num();
}
UWingServer::Printf(TEXT("Total differences: %d\n"), TotalDiffs);
}
};

View File

@@ -1,79 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPServer.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "MCPTypes.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "Blueprint_Search.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCP_Blueprint_Search : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for blueprint name or path"))
FString Query;
UPROPERTY(meta=(Optional, Description="Filter by parent class name (exact match, case-insensitive)"))
FString ParentClass;
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() override
{
MCPAssets<UObject> Assets;
Assets.Scan<UBlueprint>().Substring(Query).Limit(500);
if (!Assets.Info()) return;
UClass *Parent = nullptr;
if (!ParentClass.IsEmpty())
{
Parent = UMCPTypes::TextToOneObjectType(ParentClass);
if (!Parent) return;
}
int32 Count = 0;
for (const FAssetData& Asset : Assets.AllData())
{
// Extract parent class name from asset tags
FString ParentClassName;
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;
}
}
UMCPServer::Printf(TEXT("%30s %s\n"), *ParentClassName, *Asset.PackageName.ToString());
Count++;
}
if (Count == 0)
{
UMCPServer::Print(TEXT("No blueprint assets found.\n"));
}
}
};

View File

@@ -1,127 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPServer.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/World.h"
#include "Engine/Level.h"
#include "Engine/LevelScriptBlueprint.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 "Blueprint_SearchContents.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCP_Blueprint_SearchContents : 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() override
{
int32 Limit = (MaxResults > 0) ? FMath::Clamp(MaxResults, 1, 200) : 50;
int32 Count = 0;
// Search one blueprint's nodes for the query string.
auto SearchBlueprint = [&](UBlueprint* BP, bool bIsLevelBP)
{
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
if (Count >= Limit) return;
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) continue;
Count++;
UMCPServer::Printf(TEXT("blueprint: %s\n"), *MCPUtils::FormatName(BP));
UMCPServer::Printf(TEXT(" graph: %s\n"), *MCPUtils::FormatName(Node->GetGraph()));
UMCPServer::Printf(TEXT(" node: %s\n"), *MCPUtils::FormatName(Node));
UMCPServer::Printf(TEXT(" class: %s\n"), *MCPUtils::FormatName(Node->GetClass()));
if (!FuncName.IsEmpty()) UMCPServer::Printf(TEXT(" function: %s\n"), *FuncName);
if (!EventName.IsEmpty()) UMCPServer::Printf(TEXT(" event: %s\n"), *EventName);
if (!VarName.IsEmpty()) UMCPServer::Printf(TEXT(" variable: %s\n"), *VarName);
if (bIsLevelBP) UMCPServer::Print(TEXT(" level-blueprint: true\n"));
UMCPServer::Print(TEXT("\n"));
}
};
// Search regular blueprints
MCPAssets<UBlueprint> AllBlueprints;
if (!Path.IsEmpty()) AllBlueprints.Substring(Path);
AllBlueprints.Load();
for (UBlueprint* BP : AllBlueprints.Objects())
{
if (Count >= Limit) break;
SearchBlueprint(BP, false);
}
// Search level blueprints
MCPAssets<UWorld> AllWorlds;
if (!Path.IsEmpty()) AllWorlds.Substring(Path);
AllWorlds.Load();
for (UWorld* World : AllWorlds.Objects())
{
if (Count >= Limit) break;
if (!World || !World->PersistentLevel) continue;
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(false);
if (!LevelBP) continue;
SearchBlueprint(LevelBP, true);
}
UMCPServer::Printf(TEXT("Results: %d\n"), Count);
if (Count >= Limit) UMCPServer::Printf(TEXT("(limit %d reached, use MaxResults to increase)\n"), Limit);
}
};

View File

@@ -1,112 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "UObject/UObjectIterator.h"
#include "Class_Search.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleListClasses — discover available UClasses
// ============================================================
UCLASS()
class UWing_Class_Search : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for class names"))
FString Query;
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 names, parent class, package, and flags.");
}
virtual void Handle() override
{
Limit = FMath::Clamp(Limit, 1, 500);
UClass* ParentClassObj = nullptr;
if (!ParentClass.IsEmpty())
{
for (TObjectIterator<UClass> It; It; ++It)
{
if (WingUtils::Identifies(ParentClass, *It))
{
ParentClassObj = *It;
break;
}
}
if (!ParentClassObj)
{
UWingServer::Printf(TEXT("Error: Parent class '%s' not found\n"), *ParentClass);
return;
}
}
TArray<UClass*> Matches;
int32 TotalMatched = 0;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (!Class) continue;
if (Class->HasAnyClassFlags(CLASS_Deprecated | CLASS_NewerVersionExists)) continue;
if (ParentClassObj && !Class->IsChildOf(ParentClassObj)) continue;
FString ClassName = WingUtils::FormatName(Class);
if (!Query.IsEmpty() && !ClassName.Contains(Query, ESearchCase::IgnoreCase)) continue;
TotalMatched++;
if (Matches.Num() < Limit)
{
Matches.Add(Class);
}
}
UWingServer::Printf(TEXT("Found %d classes"), TotalMatched);
if (TotalMatched > Limit)
{
UWingServer::Printf(TEXT(" (showing %d)"), Limit);
}
UWingServer::Print(TEXT("\n"));
for (UClass* Class : Matches)
{
UWingServer::Printf(TEXT(" %s"), *WingUtils::FormatName(Class));
// Flags
TStringBuilder<64> Flags;
if (Class->HasAnyClassFlags(CLASS_Abstract)) Flags.Append(TEXT(" Abstract"));
if (Class->HasAnyClassFlags(CLASS_Interface)) Flags.Append(TEXT(" Interface"));
if (Class->HasAnyClassFlags(CLASS_MinimalAPI)) Flags.Append(TEXT(" MinimalAPI"));
if (Class->ClassGeneratedBy) Flags.Append(TEXT(" Blueprint"));
if (Flags.Len() > 0)
{
UWingServer::Printf(TEXT(" [%s]"), Flags.ToString() + 1); // skip leading space
}
if (Class->GetSuperClass())
{
UWingServer::Printf(TEXT(" : %s"), *WingUtils::FormatName(Class->GetSuperClass()));
}
UWingServer::Print(TEXT("\n"));
}
}
};

View File

@@ -1,76 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "Class_ShowProperties.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Class_ShowProperties : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Class name to list properties for"))
FString ClassName;
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
FString Query;
virtual FString GetDescription() const override
{
return TEXT("List properties on a UClass, including type, owning class, and property flags.");
}
virtual void Handle() override
{
UClass* FoundClass = UWingTypes::TextToOneObjectType(ClassName);
if (!FoundClass) return;
UWingServer::Printf(TEXT("Properties of %s:\n"), *WingUtils::FormatName(FoundClass));
int32 Count = 0;
for (TFieldIterator<FProperty> PropIt(FoundClass); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
FString PropName = Prop->GetName();
if (!Query.IsEmpty() && !PropName.Contains(Query, ESearchCase::IgnoreCase))
continue;
// Build compact flags string
TStringBuilder<256> Flags;
if (Prop->HasAnyPropertyFlags(CPF_BlueprintVisible)) Flags.Append(TEXT(" BlueprintVisible"));
if (Prop->HasAnyPropertyFlags(CPF_BlueprintReadOnly)) Flags.Append(TEXT(" BlueprintReadOnly"));
if (Prop->HasAnyPropertyFlags(CPF_Edit)) Flags.Append(TEXT(" EditAnywhere"));
if (Prop->HasAnyPropertyFlags(CPF_EditConst)) Flags.Append(TEXT(" VisibleOnly"));
if (Prop->HasAnyPropertyFlags(CPF_Config)) Flags.Append(TEXT(" Config"));
if (Prop->HasAnyPropertyFlags(CPF_SaveGame)) Flags.Append(TEXT(" SaveGame"));
if (Prop->HasAnyPropertyFlags(CPF_Transient)) Flags.Append(TEXT(" Transient"));
if (Prop->HasAnyPropertyFlags(CPF_RepNotify)) Flags.Append(TEXT(" RepNotify"));
UClass* OwnerClass = Prop->GetOwnerClass();
UWingServer::Printf(TEXT(" %s %s"), *UWingTypes::TypeToText(Prop), *PropName);
if (OwnerClass && OwnerClass != FoundClass)
UWingServer::Printf(TEXT(" [%s]"), *WingUtils::FormatName(OwnerClass));
if (Flags.Len() > 0)
UWingServer::Printf(TEXT(" (%s)"), Flags.ToString() + 1); // skip leading space
UWingServer::Print(TEXT("\n"));
Count++;
}
if (Count == 0)
{
UWingServer::Print(TEXT("No properties found.\n"));
}
}
};

View File

@@ -1,67 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "Engine/UserDefinedEnum.h"
#include "Kismet2/EnumEditorUtils.h"
#include "Factories/EnumFactory.h"
#include "WingPackageMaker.h"
#include "Enum_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Enum_Create : public UObject, public IWingHandler
{
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"))
FWingJsonArray Values;
virtual FString GetDescription() const override
{
return TEXT("Create a new UserDefinedEnum asset with the specified values.");
}
virtual void Handle() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
TArray<FString> EnumValues;
for (const TSharedPtr<FJsonValue>& Val : Values.Array)
{
FString Str = Val->AsString();
if (!Str.IsEmpty()) EnumValues.Add(Str);
}
if (EnumValues.Num() == 0)
{
UWingServer::Print(TEXT("ERROR: Values must be a non-empty array of strings\n"));
return;
}
// Create the enum using AssetTools.
UUserDefinedEnum* NewEnum = Maker.CreateAsset<UUserDefinedEnum, UEnumFactory>();
if (!NewEnum) return;
// Add enum values — UUserDefinedEnum starts with a MAX value.
// We need to add entries before MAX.
for (int32 i = 0; i < EnumValues.Num(); ++i)
{
FEnumEditorUtils::AddNewEnumeratorForUserDefinedEnum(NewEnum);
int32 NewIndex = NewEnum->NumEnums() - 2;
FEnumEditorUtils::SetEnumeratorDisplayName(NewEnum, NewIndex, FText::FromString(EnumValues[i]));
}
UWingServer::Printf(TEXT("Created %s with %d values\n"), *NewEnum->GetPathName(), EnumValues.Num());
}
};

View File

@@ -1,49 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "Materials/MaterialFunction.h"
#include "Factories/MaterialFunctionFactoryNew.h"
#include "WingPackageMaker.h"
#include "MaterialFunction_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_MaterialFunction_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new material function (e.g. '/Game/Materials/MF_MyFunc')"))
FString AssetPath;
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() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Create via IAssetTools + factory.
UMaterialFunction* MF = Maker.CreateAsset<UMaterialFunction, UMaterialFunctionFactoryNew>();
if (!MF) return;
// Set optional description.
if (!Description.IsEmpty())
MF->Description = Description;
UWingServer::Printf(TEXT("Created %s\n"), *MF->GetPathName());
}
};

View File

@@ -1,95 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialInterface.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Material_ReparentInstance.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UMCP_Material_ReparentInstance : 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, FStringBuilderBase& Result) override
{
// Load the Material Instance
MCPAssets<UMaterialInstanceConstant> Assets;
if (!Assets.Exact(MaterialInstance).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInstanceConstant* MI = Assets.Object();
// Load new parent (Material or MaterialInstance)
MCPAssets<UMaterialInterface> ParentAssets;
ParentAssets.NoScans();
ParentAssets.Scan<UMaterial>();
ParentAssets.Scan<UMaterialInstanceConstant>();
if (!ParentAssets.Exact(NewParent).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInterface* NewParentObj = ParentAssets.Object();
// Prevent circular parenting
UMaterialInterface* Check = NewParentObj;
while (Check)
{
if (Check == MI)
{
Result.Appendf(TEXT("ERROR: Reparenting to '%s' would create a circular parent chain.\n"),
*NameOf(NewParentObj));
return;
}
UMaterialInstanceConstant* CheckMI = Cast<UMaterialInstanceConstant>(Check);
if (!CheckMI) break;
Check = CheckMI->Parent;
}
FString OldParentName = MI->Parent ? NameOf(MI->Parent) : TEXT("None");
if (DryRun)
{
Result.Appendf(TEXT("[DRY RUN] Would reparent %s: %s -> %s\n"),
*MCPUtils::FormatName(MI), *OldParentName, *NameOf(NewParentObj));
return;
}
MCPUtils::PreEdit({MI});
MI->Parent = NewParentObj;
MCPUtils::PostEdit({MI});
MCPUtils::SaveGenericPackage(MI);
Result.Appendf(TEXT("Reparented %s: %s -> %s\n"),
*MCPUtils::FormatName(MI), *OldParentName, *NameOf(NewParentObj));
}
private:
FString NameOf(UMaterialInterface* Obj)
{
if (UMaterial* M = Cast<UMaterial>(Obj))
return MCPUtils::FormatName(M);
if (UMaterialInstance* MI = Cast<UMaterialInstance>(Obj))
return MCPUtils::FormatName(MI);
return Obj->GetPathName();
}
};

View File

@@ -1,26 +0,0 @@
// This code was extracted from git commit 0e79b02^ (the commit before it was
// replaced with GetGraphContextActions). Original file was:
// Plugins/BlueprintMCP/Source/BlueprintMCP/Private/MCPUtils.cpp
//
// It uses FBlueprintActionDatabase to search for spawnable node types,
// which is the correct approach for Blueprint (K2) graphs.
#include "BlueprintActionDatabase.h"
#include "BlueprintNodeSpawner.h"
FString MCPUtils::NodeSpawnerFullName(UBlueprintNodeSpawner* Spawner)
{
const FBlueprintActionUiSpec& UiSpec = Spawner->PrimeDefaultUiSpec();
FString Category = UiSpec.Category.ToString();
FString MenuName = UiSpec.MenuName.ToString();
if (Category.IsEmpty())
{
return MenuName;
}
return Category + TEXT("|") + MenuName;
}
TArray<UBlueprintNodeSpawner*> MCPUtils::SearchNodeSpawners(const FString& Query, int32 MaxResults, bool ExactMatch, UEdGraph* GraphFilter)
{
return Result;
}

View File

@@ -1,867 +0,0 @@
#include "WingUtils.h"
#include "WingJson.h"
#include "WingTypes.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "Engine/Blueprint.h"
#include "Engine/MemberReference.h"
#include "Engine/World.h"
#include "Components/ActorComponent.h"
#include "Engine/SCS_Node.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_EditablePinBase.h"
#include "EdGraph/EdGraphSchema.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "UObject/SavePackage.h"
#include "UObject/UObjectIterator.h"
#include "UObject/UnrealType.h"
#include "Misc/Paths.h"
#include "Misc/PackageName.h"
// Animation Blueprint support
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
// Material support
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialInstanceConstant.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "IMaterialEditor.h"
#include "Subsystems/AssetEditorSubsystem.h"
// Mesh, animation, texture support
#include "Engine/StaticMesh.h"
#include "Engine/SkeletalMesh.h"
#include "Animation/AnimSequence.h"
#include "Animation/BlendSpace.h"
#include "Engine/Texture.h"
// SEH support (Windows only) — defined in BlueprintWingServer.cpp
#if PLATFORM_WINDOWS
extern int32 TryCompileBlueprintSEH(UBlueprint* BP, EBlueprintCompileOptions Opts);
extern int32 TrySavePackageSEH(
UPackage* Package, UObject* Asset, const TCHAR* Filename,
FSavePackageArgs* SaveArgs, ESavePackageResult* OutResult);
#endif
// ============================================================
// Name sanitization
//
// Our parsers reserve certain punctuation marks for parsing
// types, paths, and the like. For example: Array<Int>.
// We therefore cannot allow those specific punctuation marks
// in names. We replace them with similar-looking unicode
// characters.
// ============================================================
void WingUtils::SanitizeNameInPlace(FString &Name)
{
int32 Dst = 0;
for (int32 Src = 0; Src < Name.Len(); Src++)
{
TCHAR c = Name[Src];
if (c < 0x20 || c == 0x7F) continue;
if (c == ' ') c=L'·';
if (c == '<') c=L'';
if (c == '>') c=L'';
if (c == ',') c=L'·';
Name[Dst++] = c;
}
if (Dst == 0) Name[Dst++] = L'·';
Name.LeftInline(Dst);
}
FString WingUtils::SanitizeName(const FString &Name)
{
FString Result = Name;
SanitizeNameInPlace(Result);
return Result;
}
FString WingUtils::SanitizeName(FName Name)
{
FString Result = Name.ToString();
SanitizeNameInPlace(Result);
return Result;
}
// ============================================================
// Name Lookup
// ============================================================
FString WingUtils::FormatName(const UWorld *World)
{
return World->GetPathName();
}
FString WingUtils::FormatName(const UBlueprint *BP)
{
return UWingTypes::TypeToTextOrDie(BP);
}
FString WingUtils::FormatName(const UActorComponent *C)
{
return SanitizeName(C->GetName());
}
FString WingUtils::FormatName(const USCS_Node *Node)
{
return SanitizeName(Node->GetVariableName());
}
FString WingUtils::FormatName(const UEdGraph *Graph)
{
return SanitizeName(Graph->GetName());
}
FString WingUtils::FormatName(const UEdGraphNode* Node)
{
return SanitizeName(Node->GetName());
}
FString WingUtils::FormatName(const UEdGraphPin *Pin)
{
return SanitizeName(Pin->GetName());
}
FString WingUtils::FormatName(const FMemberReference &Ref)
{
return SanitizeName(Ref.GetMemberName());
}
FString WingUtils::FormatName(const FBPVariableDescription &Var)
{
return SanitizeName(Var.VarName);
}
FString WingUtils::FormatName(const UStruct *Struct)
{
if (Cast<UScriptStruct>(Struct) || Cast<UClass>(Struct))
return UWingTypes::TypeToTextOrDie(Struct);
return SanitizeName(Struct->GetName());
}
FString WingUtils::FormatName(const UClass *Class)
{
return UWingTypes::TypeToTextOrDie(Class);
}
FString WingUtils::FormatName(const UMaterial *Material)
{
return Material->GetPathName();
}
FString WingUtils::FormatName(const UMaterialInstance *MaterialInstance)
{
return MaterialInstance->GetPathName();
}
FString WingUtils::FormatName(const UMaterialFunction *MaterialFunction)
{
return MaterialFunction->GetPathName();
}
FString WingUtils::FormatName(const UMaterialExpression *Expression)
{
return SanitizeName(Expression->GetName());
}
FString WingUtils::FormatName(const UStaticMesh *Mesh)
{
return Mesh->GetPathName();
}
FString WingUtils::FormatName(const USkeletalMesh *Mesh)
{
return Mesh->GetPathName();
}
FString WingUtils::FormatName(const UAnimSequence *Anim)
{
return Anim->GetPathName();
}
FString WingUtils::FormatName(const UBlendSpace *BlendSpace)
{
return BlendSpace->GetPathName();
}
FString WingUtils::FormatName(const UTexture *Texture)
{
return Texture->GetPathName();
}
FString WingUtils::FormatName(const UScriptStruct *Struct)
{
return UWingTypes::TypeToTextOrDie(Struct);
}
FString WingUtils::FormatName(const UEnum *Enum)
{
return UWingTypes::TypeToTextOrDie(Enum);
}
FString WingUtils::FormatName(const FProperty *Prop)
{
return SanitizeName(Prop->GetName());
}
FString WingUtils::FormatName(const FUserPinInfo &Pin)
{
return SanitizeName(Pin.PinName);
}
// ============================================================
// Formatting other things
// ============================================================
FString WingUtils::FormatNodeTitle(const UEdGraphNode *Node)
{
FString Title = Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString();
int32 NewlineIdx;
if (Title.FindChar(TEXT('\n'), NewlineIdx))
Title.LeftInline(NewlineIdx);
return Title;
}
// ============================================================
// JSON helpers
// ============================================================
// ============================================================
// Text formatting
// ============================================================
FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix)
{
FString Clean = Text;
Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n"));
TArray<FString> Words;
Clean.ParseIntoArrayWS(Words);
TStringBuilder<1024> Result;
int32 Col = 0;
for (const FString& Word : Words)
{
if (Col > 0 && Col + 1 + Word.Len() > ColLimit)
{
Result.Append(TEXT("\n"));
Col = 0;
}
if (Col == 0)
{
Result.Append(Prefix);
Col = Prefix.Len();
}
else
{
Result.Append(TEXT(" "));
Col += 1;
}
Result.Append(Word);
Col += Word.Len();
}
return Result.ToString();
}
// ============================================================
// Enum helpers
// ============================================================
FString WingUtils::EnumToString(UEnum* Enum, int64 Value, const FString& Prefix)
{
FString Full = Enum->GetNameStringByValue(Value);
if (!Prefix.IsEmpty() && Full.StartsWith(Prefix))
return Full.Mid(Prefix.Len());
return Full;
}
bool WingUtils::StringToEnum(UEnum* Enum, const FString& Str, int64& OutValue, const FString& Prefix)
{
OutValue = Enum->GetValueByNameString(Prefix + Str);
if (OutValue == INDEX_NONE)
{
UWingServer::Printf(TEXT("ERROR: Invalid value '%s' for %s\n"), *Str, *Enum->GetName());
return false;
}
return true;
}
// ============================================================
// Common Error Reporting
// ============================================================
bool WingUtils::CheckExactlyOneNamed(int Count, const FString &Kind, const FString &Name)
{
if (Count == 0)
{
UWingServer::Printf(TEXT("Could not find a %s named %s.\n"), *Kind, *Name);
return false;
}
if (Count > 1)
{
UWingServer::Printf(TEXT("More than one %s named %s\n"), *Kind, *Name);
return false;
}
return true;
}
bool WingUtils::CheckExactlyOneNamed(int Count, UClass *Class, const FString &Name)
{
return CheckExactlyOneNamed(Count, Class->GetName(), Name);
}
bool WingUtils::CheckExactlyNoneNamed(int Count, const FString &Kind, const FString &Name)
{
if (Count > 0)
{
UWingServer::Printf(TEXT("A %s named %s already exists."), *Kind, *Name);
return false;
}
return true;
}
bool WingUtils::CheckExactlyNoneNamed(int Count, UClass *Class, const FString &Name)
{
return CheckExactlyNoneNamed(Count, Class->GetName(), Name);
}
// ============================================================
// Blueprint helpers
// ============================================================
TArray<UEdGraph*> WingUtils::AllGraphs(UBlueprint* BP)
{
TArray<UEdGraph*> Graphs;
BP->GetAllGraphs(Graphs);
return Graphs;
}
TArray<UEdGraphNode*> WingUtils::AllNodes(UBlueprint* BP)
{
TArray<UEdGraphNode*> Nodes;
for (UEdGraph* Graph : AllGraphs(BP))
Nodes.Append(Graph->Nodes);
return Nodes;
}
bool WingUtils::SaveBlueprintPackage(UBlueprint* BP)
{
UPackage* Package = BP->GetPackage();
// 1. Build absolute package filename — use .umap for map packages, .uasset otherwise
FString PackageExtension = Package->ContainsMap()
? FPackageName::GetMapPackageExtension()
: FPackageName::GetAssetPackageExtension();
FString PackageFilename = FPackageName::LongPackageNameToFilename(
Package->GetName(), PackageExtension);
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
UE_LOG(LogTemp, Display, TEXT("UEWingman: Save target: %s"), *PackageFilename);
// 2. Phase 1: Try explicit compilation (same flags as UCompileAllBlueprintsCommandlet)
bool bCompiled = false;
{
EBlueprintCompileOptions CompileOpts =
EBlueprintCompileOptions::SkipSave |
EBlueprintCompileOptions::BatchCompile |
EBlueprintCompileOptions::SkipGarbageCollection |
EBlueprintCompileOptions::SkipFiBSearchMetaUpdate;
UE_LOG(LogTemp, Display, TEXT("UEWingman: Phase 1: Attempting explicit compilation..."));
#if PLATFORM_WINDOWS
int32 CompileResult = TryCompileBlueprintSEH(BP, CompileOpts);
if (CompileResult == 0)
{
bCompiled = (BP->Status == BS_UpToDate);
UE_LOG(LogTemp, Display, TEXT("UEWingman: Compilation %s (status=%d)"),
bCompiled ? TEXT("succeeded") : TEXT("completed with warnings"), (int32)BP->Status);
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UEWingman: Compilation crashed (SEH), proceeding uncompiled"));
}
#else
FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr);
bCompiled = (BP->Status == BS_UpToDate);
#endif
}
// 3. Phase 2: Set guards for save
uint8 OldRegen = BP->bIsRegeneratingOnLoad;
BP->bIsRegeneratingOnLoad = true;
EBlueprintStatus OldStatus = (EBlueprintStatus)(uint8)BP->Status;
if (!bCompiled)
{
// Tell PreSave the BP is up-to-date so it doesn't try to compile
BP->Status = BS_UpToDate;
}
// 4. Clear read-only attribute if present (source control or LFS may set this)
if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename))
{
UE_LOG(LogTemp, Display, TEXT("UEWingman: Clearing read-only attribute on %s"), *PackageFilename);
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false);
}
// 5. Phase 3: Save with SAVE_NoError + SEH protection
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.SaveFlags = SAVE_NoError;
// For level blueprints (map packages), the base object should be the UWorld, not the BP
bool bIsMapPackage = Package->ContainsMap();
UObject* BaseObject = BP;
if (bIsMapPackage)
{
// Find the UWorld in this package — it's the actual asset for .umap files
UWorld* World = FindObject<UWorld>(Package, *Package->GetName().Mid(Package->GetName().Find(TEXT("/"), ESearchCase::IgnoreCase, ESearchDir::FromEnd) + 1));
if (!World)
{
// Fallback: iterate the package to find any UWorld
ForEachObjectWithPackage(Package, [&World](UObject* Obj) {
if (UWorld* W = Cast<UWorld>(Obj))
{
World = W;
return false; // stop
}
return true; // continue
});
}
if (World)
{
BaseObject = World;
UE_LOG(LogTemp, Display, TEXT("UEWingman: Map package detected — saving UWorld '%s'"), *World->GetName());
}
else
{
UE_LOG(LogTemp, Warning, TEXT("UEWingman: Map package detected but no UWorld found — saving with BP as base"));
}
}
ESavePackageResult SaveResult = ESavePackageResult::Error;
UE_LOG(LogTemp, Display, TEXT("UEWingman: Phase 3: Calling UPackage::Save (compiled=%s, isMap=%s)..."),
bCompiled ? TEXT("yes") : TEXT("no"), bIsMapPackage ? TEXT("yes") : TEXT("no"));
#if PLATFORM_WINDOWS
int32 SEHCode = TrySavePackageSEH(Package, BaseObject, *PackageFilename, &SaveArgs, &SaveResult);
if (SEHCode != 0)
{
UE_LOG(LogTemp, Error, TEXT("UEWingman: UPackage::Save CRASHED (SEH exception caught)"));
}
#else
FSavePackageResultStruct Result = UPackage::Save(Package, BaseObject, *PackageFilename, SaveArgs);
SaveResult = Result.Result;
#endif
// 6. Restore guards
BP->bIsRegeneratingOnLoad = OldRegen;
if (!bCompiled)
{
BP->Status = (TEnumAsByte<EBlueprintStatus>)OldStatus;
}
bool bSuccess = (SaveResult == ESavePackageResult::Success);
UE_LOG(LogTemp, Display, TEXT("UEWingman: SaveBlueprintPackage — %s for '%s' (compiled=%s, result=%d)"),
bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"),
*BP->GetName(), bCompiled ? TEXT("yes") : TEXT("no"), (int32)SaveResult);
return bSuccess;
}
// ============================================================
// Material helpers
// ============================================================
void WingUtils::EnsureMaterialGraph(UMaterial* Material)
{
if (!Material) return;
if (!Material->MaterialGraph)
{
// In commandlet/headless mode the MaterialGraph is not auto-created.
// Replicate what the Material Editor does on open (MaterialEditor.cpp:619).
Material->MaterialGraph = CastChecked<UMaterialGraph>(
FBlueprintEditorUtils::CreateNewGraph(
Material, NAME_None,
UMaterialGraph::StaticClass(),
UMaterialGraphSchema::StaticClass()));
Material->MaterialGraph->Material = Material;
Material->MaterialGraph->RebuildGraph();
}
}
UMaterial* WingUtils::ReplaceMaterialWithTransientCopy(UMaterial* Material)
{
if (!Material) return nullptr;
// Already a preview material — nothing to do.
if (Material->GetOutermost() == GetTransientPackage())
return Material;
// If the material editor has a transient preview copy open, get it
// via the editor API. This follows the same pattern as Epic's
// MaterialEditingLibrary (FindMaterialEditorForAsset).
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
IAssetEditorInstance* EditorInstance = Sub ? Sub->FindEditorForAsset(Material, false) : nullptr;
if (EditorInstance)
{
// This is a weird hack. We know that the IAssetEditorInstance for a material
// is always going to be an FMaterialEditor, which conforms to IMaterialEditor.
// If that weren't the case, this unsafe code would crash hard. However,
// lots of places in unreal use this same unsafe pattern.
IMaterialEditor* MatEditor = static_cast<IMaterialEditor*>(EditorInstance);
UMaterialInterface* Edited = MatEditor->GetMaterialInterface();
if (UMaterial* EditedMat = Cast<UMaterial>(Edited))
return EditedMat;
}
return Material;
}
bool WingUtils::SaveGenericPackage(UObject* Asset)
{
if (!Asset) return false;
UPackage* Package = Asset->GetPackage();
UE_LOG(LogTemp, Display, TEXT("UEWingman: SaveGenericPackage — begin for '%s'"), *Asset->GetName());
FString PackageFilename = FPackageName::LongPackageNameToFilename(
Package->GetName(), FPackageName::GetAssetPackageExtension());
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
if (FPlatformFileManager::Get().GetPlatformFile().IsReadOnly(*PackageFilename))
{
FPlatformFileManager::Get().GetPlatformFile().SetReadOnly(*PackageFilename, false);
}
FSavePackageArgs SaveArgs;
SaveArgs.TopLevelFlags = RF_Public | RF_Standalone;
SaveArgs.SaveFlags = SAVE_NoError;
ESavePackageResult SaveResult = ESavePackageResult::Error;
#if PLATFORM_WINDOWS
int32 SEHCode = TrySavePackageSEH(Package, Asset, *PackageFilename, &SaveArgs, &SaveResult);
if (SEHCode != 0)
{
UE_LOG(LogTemp, Error, TEXT("UEWingman: SaveGenericPackage CRASHED (SEH exception)"));
}
#else
FSavePackageResultStruct Result = UPackage::Save(Package, Asset, *PackageFilename, SaveArgs);
SaveResult = Result.Result;
#endif
bool bSuccess = (SaveResult == ESavePackageResult::Success);
UE_LOG(LogTemp, Display, TEXT("UEWingman: SaveGenericPackage — %s for '%s'"),
bSuccess ? TEXT("SUCCEEDED") : TEXT("FAILED"), *Asset->GetName());
return bSuccess;
}
// ============================================================
// Anim blueprint helpers
// ============================================================
UAnimationStateMachineGraph* WingUtils::FindStateMachineGraph(UBlueprint* BP, const FString& GraphName)
{
TArray<UEdGraph*> AllGraphs;
BP->GetAllGraphs(AllGraphs);
for (UEdGraph* Graph : AllGraphs)
{
if (UAnimationStateMachineGraph* SMGraph = Cast<UAnimationStateMachineGraph>(Graph))
{
if (SMGraph->GetName() == GraphName)
{
return SMGraph;
}
}
}
return nullptr;
}
UAnimStateNode* WingUtils::FindStateByName(UAnimationStateMachineGraph* SMGraph, const FString& StateName)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateNode* StateNode = Cast<UAnimStateNode>(Node))
{
if (StateNode->GetStateName() == StateName)
{
return StateNode;
}
}
}
UWingServer::Printf(TEXT("ERROR: State '%s' not found in graph '%s'\n"), *StateName, *SMGraph->GetName());
return nullptr;
}
UAnimStateTransitionNode* WingUtils::FindTransition(UAnimationStateMachineGraph* SMGraph,
const FString& FromStateName, const FString& ToStateName)
{
for (UEdGraphNode* Node : SMGraph->Nodes)
{
if (UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node))
{
UAnimStateNode* FromState = Cast<UAnimStateNode>(TransNode->GetPreviousState());
UAnimStateNode* ToState = Cast<UAnimStateNode>(TransNode->GetNextState());
if (FromState && ToState &&
(FromState->GetStateName() == FromStateName) &&
(ToState->GetStateName() == ToStateName))
{
return TransNode;
}
}
}
return nullptr;
}
// ============================================================
// Graph actions (node spawning)
// ============================================================
FString WingUtils::ActionFullName(const TSharedPtr<FEdGraphSchemaAction>& Action)
{
FString Category = Action->GetCategory().ToString();
FString MenuName = Action->GetMenuDescription().ToString();
if (Category.IsEmpty())
return MenuName;
return Category + TEXT("|") + MenuName;
}
TArray<TSharedPtr<FEdGraphSchemaAction>> WingUtils::SearchGraphActions(UEdGraph* Graph, const FString& Query, int32 MaxResults, bool ExactMatch)
{
FString QueryLower = Query.ToLower();
TArray<TSharedPtr<FEdGraphSchemaAction>> Result;
FGraphContextMenuBuilder ContextMenuBuilder(Graph);
Graph->GetSchema()->GetGraphContextActions(ContextMenuBuilder);
for (int32 i = 0; i < ContextMenuBuilder.GetNumActions(); i++)
{
TSharedPtr<FEdGraphSchemaAction> Action = ContextMenuBuilder.GetSchemaAction(i);
if (!Action.IsValid()) continue;
FString FullName = ActionFullName(Action);
if (FullName.IsEmpty()) continue;
if (ExactMatch)
{
if (FullName.ToLower() != QueryLower)
continue;
}
else
{
FString Keywords = Action->GetKeywords().ToString();
if (!FullName.ToLower().Contains(QueryLower) && !Keywords.ToLower().Contains(QueryLower))
continue;
}
Result.Add(Action);
if ((MaxResults > 0) && (Result.Num() >= MaxResults))
break;
}
return Result;
}
// ============================================================
// PopulateFromJson — fill a USTRUCT from a JSON object
// ============================================================
// ============================================================
// CollectHandlerClasses — find all concrete IWingHandler classes
// ============================================================
TArray<UClass*> WingUtils::CollectHandlerClasses()
{
TArray<UClass*> Result;
for (TObjectIterator<UClass> It; It; ++It)
{
UClass* Class = *It;
if (Class->HasAnyClassFlags(CLASS_Abstract)) continue;
if (!Class->ImplementsInterface(UWingHandler::StaticClass())) continue;
Result.Add(Class);
}
Result.Sort([](UClass& A, UClass& B) { return GetHandlerName(&A) < GetHandlerName(&B); });
return Result;
}
// ============================================================
// GetHandlerName — derive tool name from handler class name
// ============================================================
FString WingUtils::GetHandlerName(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
// Strip "Wing_" prefix
Name.RemoveFromStart(TEXT("Wing_"));
return Name;
}
// ============================================================
// GetHandlerGroup — derive group name from handler class name
// ============================================================
FString WingUtils::GetHandlerGroup(UClass* HandlerClass)
{
FString Name = HandlerClass->GetName();
// Strip "Wing_" prefix
Name.RemoveFromStart(TEXT("Wing_"));
// Everything before the underscore is the group
int32 UnderscoreIdx;
if (Name.FindChar(TEXT('_'), UnderscoreIdx))
return Name.Left(UnderscoreIdx);
return Name;
}
// ============================================================
// GetTemplate
// ============================================================
// ============================================================
// FindPropertyByName
// ============================================================
FProperty* WingUtils::FindPropertyByName(UObject* Obj, const FString& Name)
{
if (!Obj)
{
UWingServer::Print(TEXT("ERROR: Object is null\n"));
return nullptr;
}
FProperty* Found = nullptr;
for (TFieldIterator<FProperty> PropIt(Obj->GetClass()); PropIt; ++PropIt)
{
if (!Identifies(Name, *PropIt)) continue;
if (Found)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous property '%s' on %s\n"), *Name, *FormatName(Obj->GetClass()));
return nullptr;
}
Found = *PropIt;
}
if (!Found)
UWingServer::Printf(TEXT("ERROR: Property '%s' not found on %s\n"), *Name, *FormatName(Obj->GetClass()));
return Found;
}
// ============================================================
// GetPropertyValueText
// ============================================================
FString WingUtils::GetPropertyValueText(UObject* Container, FProperty* Prop)
{
FString Result;
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, Container, PPF_None);
return Result;
}
// ============================================================
// SetPropertyValueText
// ============================================================
bool WingUtils::SetPropertyValueText(UObject* Container, FProperty* Prop, const FString& Value)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Container, PPF_None);
if (!ImportResult)
{
UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
bool WingUtils::SetPropertyValueText(void* Container, FProperty* Prop, const FString& Value, UObject* Owner)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
const TCHAR* ImportResult = Prop->ImportText_Direct(*Value, ValuePtr, Owner, PPF_None);
if (!ImportResult)
{
UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
// ============================================================
// SearchProperties
// ============================================================
TArray<FProperty*> WingUtils::SearchProperties(UObject* Obj, const FString& Query, EPropertyFlags Flags, bool bLocal)
{
TArray<FProperty*> Result;
if (!Obj) return Result;
UClass* ObjClass = Obj->GetClass();
for (TFieldIterator<FProperty> PropIt(ObjClass); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
if (!Prop) continue;
if (Flags != 0 && !Prop->HasAnyPropertyFlags(Flags)) continue;
if (bLocal && Prop->GetOwnerStruct() != ObjClass) continue;
if (!Query.IsEmpty() && !FormatName(Prop).Contains(Query, ESearchCase::IgnoreCase))
continue;
Result.Add(Prop);
}
return Result;
}
// ============================================================
// FormatCommandHelp — verbose description of one handler command
// ============================================================
void WingUtils::FormatCommandHelp(UClass* HandlerClass)
{
const IWingHandler* Handler = Cast<IWingHandler>(HandlerClass->GetDefaultObject());
if (!Handler) return;
FString ToolName = GetHandlerName(HandlerClass);
UWingServer::Print(TEXT("\n"));
UWingServer::Print(WrapText(Handler->GetDescription(), 80, TEXT("// ")));
UWingServer::Print(TEXT("\n"));
// Command signature line
UWingServer::Print(ToolName);
UWingServer::Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) UWingServer::Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) UWingServer::Print(TEXT("?"));
UWingServer::Print(PropIt->GetName());
}
UWingServer::Print(TEXT(")\n"));
// parameter details
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
FString Name = Prop->GetName();
FString Type = UWingTypes::TypeToText(Prop);
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
UWingServer::Printf(TEXT(" %s %s%s"),
*Type, *Name, bOptional ? TEXT(" (optional)") : TEXT(""));
if (!Desc.IsEmpty())
UWingServer::Printf(TEXT(" — %s"), *Desc);
UWingServer::Print(TEXT("\n"));
}
}

View File

@@ -1,116 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimStateNode.h"
#include "AnimationStateMachineGraph.h"
#include "StateMachine_AddState.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_StateMachine_AddState : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint 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 package path 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() override
{
// Resolve the anim blueprint
WingFetcher F;
UAnimBlueprint* AnimBP = F.Walk(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Find the state machine graph
UAnimationStateMachineGraph* SMGraph = WingUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
UWingServer::Printf(TEXT("ERROR: State machine graph '%s' not found in %s\n"), *Graph, *WingUtils::FormatName(AnimBP));
return;
}
// Check for duplicate state name
if (WingUtils::FindStateByName(SMGraph, StateName))
{
UWingServer::Printf(TEXT("ERROR: State '%s' already exists in %s\n"), *StateName, *WingUtils::FormatName(SMGraph));
return;
}
// 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())
{
WingFetcher F2;
UAnimSequence* AnimSeq = F2.Asset(AnimationAsset).Cast<UAnimSequence>();
if (!AnimSeq) return;
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
FKismetEditorUtilities::CompileBlueprint(AnimBP);
UWingServer::Printf(TEXT("Created state '%s' in %s\n"), *StateName, *WingUtils::FormatName(SMGraph));
UWingServer::Printf(TEXT(" node: %s\n"), *WingUtils::FormatName(NewState));
}
};

View File

@@ -1,98 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "StateMachine_AddTransition.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_StateMachine_AddTransition : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint 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() override
{
WingFetcher F;
UAnimBlueprint* AnimBP = F.Asset(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
UAnimationStateMachineGraph* SMGraph = WingUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
UWingServer::Printf(TEXT("ERROR: State machine graph '%s' not found in '%s'\n"), *Graph, *WingUtils::FormatName(AnimBP));
return;
}
UAnimStateNode* FromStateNode = WingUtils::FindStateByName(SMGraph, FromState);
if (!FromStateNode) return;
UAnimStateNode* ToStateNode = WingUtils::FindStateByName(SMGraph, ToState);
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
TransNode->CrossfadeDuration = CrossfadeDuration;
TransNode->PriorityOrder = Priority;
TransNode->Bidirectional = BBidirectional;
// Compile
FKismetEditorUtilities::CompileBlueprint(AnimBP);
UWingServer::Printf(TEXT("Created transition %s -> %s: %s\n"),
*FromState, *ToState, *WingUtils::FormatName(TransNode));
}
};

View File

@@ -1,81 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "StateMachine_RemoveState.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_StateMachine_RemoveState : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the state machine graph, e.g. /Game/MyAnimBP,graph:StateMachine"))
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() override
{
// Fetch the state machine graph via WingFetcher
WingFetcher F;
F.Walk(Graph);
if (!F.Ok()) return;
UAnimationStateMachineGraph* SMGraph = F.Cast<UAnimationStateMachineGraph>();
if (!SMGraph) return;
// Find the owning AnimBlueprint for compile/save
UBlueprint* BP = Cast<UBlueprint>(SMGraph->GetOuter()->GetOuter());
if (!BP)
{
UWingServer::Print(TEXT("ERROR: Could not find owning blueprint.\n"));
return;
}
// Find the state node
UAnimStateNode* StateNode = WingUtils::FindStateByName(SMGraph, StateName);
if (!StateNode) return;
// Collect and remove transitions connected to this state
int32 RemovedTransitions = 0;
for (UEdGraphNode* Node : TArray<UEdGraphNode*>(SMGraph->Nodes))
{
UAnimStateTransitionNode* TransNode = Cast<UAnimStateTransitionNode>(Node);
if (!TransNode) continue;
if (TransNode->GetPreviousState() != StateNode && TransNode->GetNextState() != StateNode) continue;
TransNode->BreakAllNodeLinks();
SMGraph->RemoveNode(TransNode);
RemovedTransitions++;
}
// Remove the state
StateNode->BreakAllNodeLinks();
SMGraph->RemoveNode(StateNode);
// Compile
FKismetEditorUtilities::CompileBlueprint(BP);
UWingServer::Printf(TEXT("Removed state %s and %d transition(s).\n"),
*WingUtils::FormatName(StateNode), RemovedTransitions);
}
};

View File

@@ -1,108 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimSequence.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimStateNode.h"
#include "AnimationStateMachineGraph.h"
#include "StateMachine_SetAnimation.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_StateMachine_SetAnimation : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint 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 package path 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() override
{
// Resolve the anim blueprint
WingFetcher F;
UAnimBlueprint* AnimBP = F.Walk(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Find the state machine graph
UAnimationStateMachineGraph* SMGraph = WingUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
UWingServer::Printf(TEXT("ERROR: State machine graph '%s' not found in %s\n"), *Graph, *WingUtils::FormatName(AnimBP));
return;
}
// Find the target state
UAnimStateNode* StateNode = WingUtils::FindStateByName(SMGraph, StateName);
if (!StateNode) return;
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph)
{
UWingServer::Printf(TEXT("ERROR: State '%s' has no bound graph\n"), *StateName);
return;
}
// Find the animation asset
WingFetcher F2;
UAnimSequence* AnimSeq = F2.Asset(AnimationAsset).Cast<UAnimSequence>();
if (!AnimSeq) return;
// 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
FKismetEditorUtilities::CompileBlueprint(AnimBP);
if (bCreatedNew)
UWingServer::Printf(TEXT("Created sequence player in state '%s', assigned %s\n"), *StateName, *WingUtils::FormatName(AnimSeq));
else
UWingServer::Printf(TEXT("Updated sequence player in state '%s' to %s\n"), *StateName, *WingUtils::FormatName(AnimSeq));
}
};

View File

@@ -1,217 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/BlendSpace.h"
#include "AnimGraphNode_BlendSpacePlayer.h"
#include "EdGraphSchema_K2.h"
#include "AnimStateNode.h"
#include "AnimationStateMachineGraph.h"
#include "K2Node_VariableGet.h"
#include "StateMachine_SetBlendSpace.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_StateMachine_SetBlendSpace : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint 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 package 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() override
{
// Load the anim blueprint
WingFetcher F;
UAnimBlueprint* AnimBP = F.Asset(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Find the state machine graph and state
UAnimationStateMachineGraph* SMGraph = WingUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph) { UWingServer::Printf(TEXT("ERROR: State machine graph '%s' not found\n"), *Graph); return; }
UAnimStateNode* StateNode = WingUtils::FindStateByName(SMGraph, StateName);
if (!StateNode) return;
UEdGraph* InnerGraph = StateNode->GetBoundGraph();
if (!InnerGraph) { UWingServer::Printf(TEXT("ERROR: State '%s' has no bound graph\n"), *StateName); return; }
// Load the blend space asset
WingFetcher F2;
UBlendSpace* BlendSpaceAsset = F2.Asset(BlendSpace).Cast<UBlendSpace>();
if (!BlendSpaceAsset) return;
// 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
ConnectToOutputPose(BSNode, InnerGraph);
// Wire X and Y variables if provided
WireVariable(AnimBP, InnerGraph, BSNode, XVariable, TEXT("X"));
WireVariable(AnimBP, InnerGraph, BSNode, YVariable, TEXT("Y"));
// Compile
FKismetEditorUtilities::CompileBlueprint(AnimBP);
UWingServer::Printf(TEXT("BlendSpacePlayer %s placed in state %s\n"),
*WingUtils::FormatName(BSNode), *StateName);
}
private:
void ConnectToOutputPose(UAnimGraphNode_BlendSpacePlayer* BSNode, UEdGraph* InnerGraph)
{
// Find the result node (AnimGraphNode_Root or AnimGraphNode_StateResult)
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) return;
// Find the pose output pin on BlendSpacePlayer and input 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) return;
ResultInputPin->BreakAllPinLinks();
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
if (Schema)
Schema->TryCreateConnection(BSOutputPin, ResultInputPin);
}
void WireVariable(UAnimBlueprint* AnimBP, UEdGraph* InnerGraph,
UAnimGraphNode_BlendSpacePlayer* BSNode, const FString& VarName,
const TCHAR* PinName)
{
if (VarName.IsEmpty()) return;
// 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)
{
if (UClass* GenClass = AnimBP->SkeletonGeneratedClass)
{
if (GenClass->FindPropertyByName(VarFName))
bVarFound = true;
}
}
if (!bVarFound)
{
UWingServer::Printf(TEXT("WARNING: Variable '%s' not found, skipping %s wire\n"), *VarName, PinName);
return;
}
// 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;
}
}
UEdGraphPin* TargetPin = BSNode->FindPin(FName(PinName));
if (VarOutPin && TargetPin)
{
const UEdGraphSchema* Schema = InnerGraph->GetSchema();
if (Schema)
Schema->TryCreateConnection(VarOutPin, TargetPin);
}
}
};

View File

@@ -1,91 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Animation/AnimBlueprint.h"
#include "AnimStateTransitionNode.h"
#include "AnimationStateMachineGraph.h"
#include "StateMachine_SetTransitionRule.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_StateMachine_SetTransitionRule : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint 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() override
{
WingFetcher F;
UAnimBlueprint* AnimBP = F.Asset(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
UAnimationStateMachineGraph* SMGraph = WingUtils::FindStateMachineGraph(AnimBP, Graph);
if (!SMGraph)
{
UWingServer::Printf(TEXT("ERROR: State machine graph '%s' not found in '%s'\n"), *Graph, *WingUtils::FormatName(AnimBP));
return;
}
UAnimStateTransitionNode* TransNode = WingUtils::FindTransition(SMGraph, FromState, ToState);
if (!TransNode)
{
UWingServer::Printf(TEXT("ERROR: Transition from '%s' to '%s' not found in graph '%s'\n"),
*FromState, *ToState, *Graph);
return;
}
// Apply properties
TransNode->CrossfadeDuration = CrossfadeDuration;
TransNode->BlendMode = (EAlphaBlendOption)BlendMode;
TransNode->PriorityOrder = PriorityOrder;
TransNode->LogicType = (ETransitionLogicType::Type)LogicType;
TransNode->Bidirectional = BBidirectional;
// Compile
FKismetEditorUtilities::CompileBlueprint(AnimBP);
UWingServer::Printf(TEXT("Updated transition %s -> %s: %s\n"),
*FromState, *ToState, *WingUtils::FormatName(TransNode));
}
};

View File

@@ -1,95 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingTypes.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "StructUtils/UserDefinedStruct.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "Factories/StructureFactory.h"
#include "WingPackageMaker.h"
#include "Struct_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FStructPropertyEntry
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
FString Type;
};
UCLASS()
class UWing_Struct_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset 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"))
FWingJsonArray Properties;
virtual FString GetDescription() const override
{
return TEXT("Create a new UserDefinedStruct asset with optional initial properties.");
}
virtual void Handle() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Create the struct using the AssetTools factory.
UUserDefinedStruct* NewStruct = Maker.CreateAsset<UUserDefinedStruct, UStructureFactory>();
if (!NewStruct) return;
// Add properties if specified.
int32 PropsAdded = 0;
for (const TSharedPtr<FJsonValue>& PropVal : Properties.Array)
{
FStructPropertyEntry Entry;
if (!WingJson::PopulateFromJson(FStructPropertyEntry::StaticStruct(), &Entry, PropVal)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FEdGraphPinType PinType;
if (!UWingTypes::TextToType(Entry.Type, PinType))
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);
if (!FStructureEditorUtils::AddVariable(NewStruct, PinType))
continue;
// Find the new variable by diffing GUID sets.
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(NewStruct))
{
if (!ExistingGuids.Contains(Var.VarGuid))
{
FStructureEditorUtils::RenameVariable(NewStruct, Var.VarGuid, Entry.Name);
break;
}
}
PropsAdded++;
}
UWingServer::Printf(TEXT("Created %s\n"), *NewStruct->GetPathName());
if (PropsAdded > 0)
UWingServer::Printf(TEXT("Properties added: %d\n"), PropsAdded);
}
};

View File

@@ -1,169 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UObject/UObjectIterator.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;
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, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'\n"));
return;
}
if (!Material.IsEmpty() && !MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction', not both\n"));
return;
}
// Resolve the expression class
UClass* ExprClass = ResolveExpressionClass(Result);
if (!ExprClass) return;
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
UObject* Owner = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = Assets.Object();
Owner = MatFunc;
}
else
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = Assets.Object();
Owner = MaterialObj;
}
// Ensure the MaterialGraph exists (commandlet mode doesn't auto-create it)
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
// Create the expression
UMaterialExpression* NewExpr = NewObject<UMaterialExpression>(Owner, ExprClass);
if (!NewExpr)
{
Result.Append(TEXT("ERROR: Failed to create material expression object\n"));
return;
}
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
{
MatFunc->GetExpressionCollection().AddExpression(NewExpr);
MatFunc->PreEditChange(nullptr);
MatFunc->PostEditChange();
MatFunc->MarkPackageDirty();
}
// Save
bool bSaved = MaterialObj
? MCPUtils::SaveMaterialPackage(MaterialObj)
: MCPUtils::SaveGenericPackage(MatFunc);
// Output
Result.Appendf(TEXT("Added %s\n"), *MCPUtils::FormatName(NewExpr));
// Find the graph node GUID (only for materials with a material graph)
if (MaterialObj && MaterialObj->MaterialGraph)
{
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node);
if (MatNode && MatNode->MaterialExpression == NewExpr)
{
Result.Appendf(TEXT("NodeId: %s\n"), *Node->NodeGuid.ToString());
break;
}
}
}
if (!bSaved)
Result.Append(TEXT("WARNING: Failed to save package\n"));
}
private:
UClass* ResolveExpressionClass(FStringBuilderBase& Result)
{
// Convenience aliases
static TMap<FString, FString> Aliases = {
{TEXT("Lerp"), TEXT("LinearInterpolate")},
};
FString LookupName = ExpressionClass;
if (const FString* Alias = Aliases.Find(ExpressionClass))
LookupName = *Alias;
FString FullClassName = FString::Printf(TEXT("MaterialExpression%s"), *LookupName);
for (TObjectIterator<UClass> It; It; ++It)
{
if (It->GetName() == FullClassName && It->IsChildOf(UMaterialExpression::StaticClass()))
{
if (It->HasAnyClassFlags(CLASS_Abstract))
{
Result.Appendf(TEXT("ERROR: Expression class '%s' is abstract\n"), *ExpressionClass);
return nullptr;
}
return *It;
}
}
Result.Appendf(TEXT("ERROR: Unknown expression class '%s'. Use the UMaterialExpression subclass name without the 'MaterialExpression' prefix (e.g. 'Constant', 'ScalarParameter', 'Add', 'Multiply', 'Lerp')\n"),
*ExpressionClass);
return nullptr;
}
};

View File

@@ -1,73 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPTypes.h"
#include "MCPUtils.h"
#include "StructUtils/UserDefinedStruct.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "UMCPHandler_AddStructField.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_AddStructField : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Package path of the struct asset"))
FString Struct;
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, FStringBuilderBase& Result) override
{
// Find the struct
MCPFetcher F(Result);
UUserDefinedStruct* S = F.Walk(Struct).Cast<UUserDefinedStruct>();
if (!S) return;
// Resolve type
FEdGraphPinType PinType;
if (!UMCPTypes::TextToType(Type, PinType, Result))
return;
// Snapshot existing GUIDs so we can find the newly added one
TSet<FGuid> ExistingGuids;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
ExistingGuids.Add(Var.VarGuid);
if (!FStructureEditorUtils::AddVariable(S, PinType))
{
Result.Append(TEXT("ERROR: Failed to add field to struct.\n"));
return;
}
// Find the new variable by diffing GUID sets and rename it
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
{
if (!ExistingGuids.Contains(Var.VarGuid))
{
FStructureEditorUtils::RenameVariable(S, Var.VarGuid, Name);
break;
}
}
MCPUtils::SaveGenericPackage(S);
Result.Appendf(TEXT("Added field: %s %s\n"), *Type, *Name);
}
};

View File

@@ -1,229 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.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_BlueprintSearchTypeUsage.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ============================================================
// HandleSearchByType — find all usages of a type across blueprints
// ============================================================
UCLASS(meta=(Group="Blueprint"))
class UMCPHandler_BlueprintSearchTypeUsage : 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 Query;
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, FStringBuilderBase& Result) override
{
FString DecodedTypeName = MCPUtils::UrlDecode(TypeName);
FString FilterStr = Query.IsEmpty() ? FString() : MCPUtils::UrlDecode(Query);
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);
};
int32 ResultCount = 0;
// Helper: get subtype name from a pin type
auto GetSubtype = [](const FEdGraphPinType& PinType) -> FString
{
if (PinType.PinSubCategoryObject.IsValid())
return PinType.PinSubCategoryObject->GetName();
return FString();
};
// Helper: check if a pin type matches
auto PinTypeMatches = [&](const FEdGraphPinType& PinType) -> bool
{
return MatchesType(GetSubtype(PinType)) || MatchesType(PinType.PinCategory.ToString());
};
// Lambda that searches a single Blueprint for type usages
auto SearchOneBlueprint = [&](UBlueprint* BP, bool bIsLevel)
{
FString BPName = MCPUtils::FormatName(BP);
// Check variables
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (ResultCount >= EffectiveMaxResults) return;
if (!PinTypeMatches(Var.VarType)) continue;
Result.Appendf(TEXT("variable %s in %s: %s %s\n"),
*MCPUtils::FormatName(Var), *BPName,
*Var.VarType.PinCategory.ToString(), *GetSubtype(Var.VarType));
ResultCount++;
}
// Check graphs for function/event params, struct nodes, and pin connections
for (UEdGraphNode* Node : MCPUtils::AllNodes(BP))
{
if (ResultCount >= EffectiveMaxResults) return;
// Check FunctionEntry parameters
if (auto* FuncEntry = Cast<UK2Node_FunctionEntry>(Node))
{
for (const TSharedPtr<FUserPinInfo>& PinInfo : FuncEntry->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (!PinTypeMatches(PinInfo->PinType)) continue;
Result.Appendf(TEXT("func-param %s.%s in %s: %s %s\n"),
*MCPUtils::FormatName(Node->GetGraph()), *PinInfo->PinName.ToString(),
*BPName,
*PinInfo->PinType.PinCategory.ToString(), *GetSubtype(PinInfo->PinType));
ResultCount++;
}
}
else if (auto* CustomEvent = Cast<UK2Node_CustomEvent>(Node))
{
for (const TSharedPtr<FUserPinInfo>& PinInfo : CustomEvent->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (!PinTypeMatches(PinInfo->PinType)) continue;
Result.Appendf(TEXT("event-param %s.%s in %s: %s %s\n"),
*MCPUtils::FormatName(Node), *PinInfo->PinName.ToString(),
*BPName,
*PinInfo->PinType.PinCategory.ToString(), *GetSubtype(PinInfo->PinType));
ResultCount++;
}
}
// Check Break/Make struct nodes
else if (auto* BreakNode = Cast<UK2Node_BreakStruct>(Node))
{
if (BreakNode->StructType && MatchesType(BreakNode->StructType->GetName()))
{
Result.Appendf(TEXT("break-struct %s in %s graph %s\n"),
*BreakNode->StructType->GetName(), *BPName,
*MCPUtils::FormatName(Node->GetGraph()));
ResultCount++;
}
}
else if (auto* MakeNode = Cast<UK2Node_MakeStruct>(Node))
{
if (MakeNode->StructType && MatchesType(MakeNode->StructType->GetName()))
{
Result.Appendf(TEXT("make-struct %s in %s graph %s\n"),
*MakeNode->StructType->GetName(), *BPName,
*MCPUtils::FormatName(Node->GetGraph()));
ResultCount++;
}
}
// Check pin connections carrying the type
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin || Pin->bHidden || ResultCount >= EffectiveMaxResults) continue;
if (Pin->LinkedTo.Num() == 0) continue;
if (!PinTypeMatches(Pin->PinType)) continue;
Result.Appendf(TEXT("pin %s.%s in %s graph %s: %s %s (%d connections)\n"),
*MCPUtils::FormatName(Node), *MCPUtils::FormatName(Pin),
*BPName, *MCPUtils::FormatName(Node->GetGraph()),
*Pin->PinType.PinCategory.ToString(), *GetSubtype(Pin->PinType),
Pin->LinkedTo.Num());
ResultCount++;
}
}
};
MCPAssets<UBlueprint> AllBlueprints;
AllBlueprints.Info();
MCPAssets<UWorld> AllWorlds;
AllWorlds.Info();
// Search regular blueprints
for (const FAssetData& Asset : AllBlueprints.AllData())
{
if (ResultCount >= EffectiveMaxResults) break;
FString AssetPath = Asset.PackageName.ToString();
FString AssetName = Asset.AssetName.ToString();
if (!FilterStr.IsEmpty() && !AssetName.Contains(FilterStr, ESearchCase::IgnoreCase) &&
!AssetPath.Contains(FilterStr, ESearchCase::IgnoreCase))
{
continue;
}
UBlueprint* BP = Cast<UBlueprint>(const_cast<FAssetData&>(Asset).GetAsset());
if (!BP) continue;
SearchOneBlueprint(BP, false);
}
// Search level blueprints from maps
for (const FAssetData& MapAsset : AllWorlds.AllData())
{
if (ResultCount >= 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(LevelBP, true);
}
Result.Appendf(TEXT("\n%d results\n"), ResultCount);
}
};

View File

@@ -1,188 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "MaterialGraph/MaterialGraph.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "UMCPHandler_ConnectMaterialExpressionPins.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectMaterialPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourceNode;
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetNode;
UPROPERTY()
FString TargetPin;
};
UCLASS(meta=(Group="Unclassified"))
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="Array of {sourceNode, sourcePin, targetNode, targetPin} objects"))
FMCPJsonArray Connections;
virtual FString GetDescription() const override
{
return TEXT("Connect pins between nodes in a material or material function graph. Supports batching.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'.\n"));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = Assets.Object();
}
else
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = Assets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
Result.Appendf(TEXT("ERROR: %s has no material graph.\n"),
MaterialObj ? *MCPUtils::FormatName(MaterialObj) : *MCPUtils::FormatName(MatFunc));
return;
}
int32 SuccessCount = 0;
const UEdGraphSchema* Schema = Graph->GetSchema();
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
FConnectMaterialPinsEntry Entry;
if (!MCPUtils::PopulateFromJson(FConnectMaterialPinsEntry::StaticStruct(), &Entry, ConnVal, MCPErrorCallback(Result)))
continue;
// Find source node
UEdGraphNode* SrcNode = FindNodeByName(Graph, Entry.SourceNode);
if (!SrcNode)
{
Result.Appendf(TEXT("ERROR: Source node '%s' not found.\n"), *Entry.SourceNode);
continue;
}
// Find target node
UEdGraphNode* TgtNode = FindNodeByName(Graph, Entry.TargetNode);
if (!TgtNode)
{
Result.Appendf(TEXT("ERROR: Target node '%s' not found.\n"), *Entry.TargetNode);
continue;
}
// Find source pin
UEdGraphPin* SrcPin = FindPinByName(SrcNode, Entry.SourcePin);
if (!SrcPin)
{
Result.Appendf(TEXT("ERROR: Pin '%s' not found on %s. Available:"),
*Entry.SourcePin, *MCPUtils::FormatName(SrcNode));
for (UEdGraphPin* P : SrcNode->Pins)
if (P) Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(P));
Result.Append(TEXT("\n"));
continue;
}
// Find target pin
UEdGraphPin* TgtPin = FindPinByName(TgtNode, Entry.TargetPin);
if (!TgtPin)
{
Result.Appendf(TEXT("ERROR: Pin '%s' not found on %s. Available:"),
*Entry.TargetPin, *MCPUtils::FormatName(TgtNode));
for (UEdGraphPin* P : TgtNode->Pins)
if (P) Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(P));
Result.Append(TEXT("\n"));
continue;
}
// Check compatibility
const FPinConnectionResponse Response = Schema->CanCreateConnection(SrcPin, TgtPin);
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
{
Result.Appendf(TEXT("ERROR: Cannot connect %s.%s -> %s.%s: %s\n"),
*MCPUtils::FormatName(SrcNode), *MCPUtils::FormatName(SrcPin),
*MCPUtils::FormatName(TgtNode), *MCPUtils::FormatName(TgtPin),
*Response.Message.ToString());
continue;
}
Schema->TryCreateConnection(SrcPin, TgtPin);
SuccessCount++;
}
if (SuccessCount > 0)
{
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("Connected %d/%d. Saved: %s\n"),
SuccessCount, Connections.Array.Num(), bSaved ? TEXT("yes") : TEXT("no"));
}
else
{
Result.Appendf(TEXT("0/%d connections succeeded.\n"), Connections.Array.Num());
}
}
private:
UEdGraphNode* FindNodeByName(UEdGraph* Graph, const FString& Name)
{
for (UEdGraphNode* Node : Graph->Nodes)
if (Node && MCPUtils::Identifies(Name, Node))
return Node;
return nullptr;
}
UEdGraphPin* FindPinByName(UEdGraphNode* Node, const FString& Name)
{
for (UEdGraphPin* Pin : Node->Pins)
if (Pin && MCPUtils::Identifies(Name, Pin))
return Pin;
return nullptr;
}
};

View File

@@ -1,126 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UMCPHandler_DeleteMaterialExpression.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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="Expression name (use FormatName from DumpMaterial output)"))
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, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
MCPErrorCallback(Result).SetError(TEXT("Specify 'material' or 'materialFunction'."));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
MCPErrorCallback(Result).SetError(TEXT("Asset has no material graph."));
return;
}
// Find node by name
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(GraphNode);
if (!MatNode || !MatNode->MaterialExpression) continue;
if (MCPUtils::Identifies(Node, MatNode->MaterialExpression))
{
TargetMatNode = MatNode;
break;
}
}
if (!TargetMatNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(TEXT("Expression '%s' not found."), *Node));
return;
}
FString ExprName = MCPUtils::FormatName(TargetMatNode->MaterialExpression);
if (DryRun)
{
Result.Appendf(TEXT("DryRun: would delete %s\n"), *ExprName);
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);
Result.Appendf(TEXT("Deleted %s"), *ExprName);
if (!bSaved) Result.Append(TEXT(" (save failed)"));
Result.Append(TEXT("\n"));
}
};

View File

@@ -1,188 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
#include "Materials/MaterialExpressionConstant4Vector.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphNode_Root.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "UMCPHandler_DescribeMaterialInEnglish.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterial* MaterialObj = Assets.Object();
// Ensure material graph is built
MCPUtils::EnsureMaterialGraph(MaterialObj);
if (!MaterialObj->MaterialGraph)
{
MCPErrorCallback(Result).SetError(TEXT("Could not build MaterialGraph for this material"));
return;
}
// Find root node
UMaterialGraphNode_Root* RootNode = nullptr;
for (UEdGraphNode* Node : MaterialObj->MaterialGraph->Nodes)
{
RootNode = Cast<UMaterialGraphNode_Root>(Node);
if (RootNode) break;
}
if (!RootNode)
{
MCPErrorCallback(Result).SetError(TEXT("Could not find root node in material graph"));
return;
}
// 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 (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;
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(SourceNode);
if (!MatNode)
{
NodeDesc = MCPUtils::FormatName(SourceNode);
Sources.Add(NodeDesc);
continue;
}
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 ? MCPUtils::FormatName(TP->Texture) : 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 ? MCPUtils::FormatName(TS->Texture) : TEXT("None");
NodeDesc = FString::Printf(TEXT("TextureSample(%s)"), *TexName);
}
else if (auto* MFC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
{
FString FuncName = MFC->MaterialFunction ? MFC->MaterialFunction->GetPathName() : TEXT("None");
NodeDesc = FString::Printf(TEXT("FunctionCall(%s)"), *FuncName);
}
else
{
NodeDesc = MCPUtils::FormatName(Expr);
}
// Recurse into input pins
TArray<FString> InputDescs;
for (UEdGraphPin* InputPin : SourceNode->Pins)
{
if (!InputPin || InputPin->Direction != EGPD_Input || InputPin->LinkedTo.Num() == 0) continue;
InputDescs.Add(TracePin(InputPin, Depth + 1));
}
if (InputDescs.Num() > 0)
{
NodeDesc += TEXT(" <- (") + FString::Join(InputDescs, TEXT(", ")) + TEXT(")");
}
Sources.Add(NodeDesc);
}
if (Sources.Num() == 1)
return Sources[0];
return TEXT("(") + FString::Join(Sources, TEXT(", ")) + TEXT(")");
};
// Trace each connected root input and output plain text
Result.Appendf(TEXT("Material: %s\n\n"), *MCPUtils::FormatName(MaterialObj));
for (UEdGraphPin* Pin : RootNode->Pins)
{
if (!Pin || Pin->Direction != EGPD_Input) continue;
if (Pin->LinkedTo.Num() == 0) continue;
FString PinName = MCPUtils::FormatName(Pin);
FString Description = TracePin(Pin, 0);
Result.Appendf(TEXT("%s <- %s\n"), *PinName, *Description);
}
}
};

View File

@@ -1,98 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "UMCPHandler_DisconnectMaterialExpressionPin.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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 name (use FormatName-style identifier)"))
FString Node;
UPROPERTY(meta=(Description="Pin name to disconnect"))
FString PinName;
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, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'.\n"));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = Assets.Object();
}
else
{
MCPAssets<UMaterial> Assets;
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = Assets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
Result.Appendf(TEXT("ERROR: %s has no material graph.\n"),
MaterialObj ? *MCPUtils::FormatName(MaterialObj) : *MCPUtils::FormatName(MatFunc));
return;
}
// Find node and pin via MCPFetcher
MCPFetcher F(Result, Graph);
UEdGraphPin* Pin = F.Node(Node).Pin(PinName).Cast<UEdGraphPin>();
if (!Pin) return;
int32 BrokenCount = Pin->LinkedTo.Num();
Pin->BreakAllPinLinks();
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
Asset->PostEditChange();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("Disconnected %d link(s) from %s on %s.\n"),
BrokenCount, *MCPUtils::FormatName(Pin), *MCPUtils::FormatName(Pin->GetOwningNode()));
if (!bSaved)
Result.Append(TEXT("WARNING: Failed to save package.\n"));
}
};

View File

@@ -1,228 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Engine/Texture.h"
#include "UMCPHandler_DumpMaterial.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
MCPAssets<UMaterialInterface> Assets;
Assets.NoScans();
Assets.Scan<UMaterial>();
Assets.Scan<UMaterialInstanceConstant>();
if (!Assets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
UMaterialInterface* LoadedObj = Assets.Object();
if (UMaterial* Mat = Cast<UMaterial>(LoadedObj))
{
EmitMaterial(Mat, Result);
return;
}
if (UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(LoadedObj))
{
EmitMaterialInstance(MI, Result);
return;
}
Result.Appendf(TEXT("ERROR: Loaded object is %s, expected Material or MaterialInstance.\n"),
*LoadedObj->GetClass()->GetName());
}
private:
void EmitMaterial(UMaterial* Mat, FStringBuilderBase& Result)
{
Result.Appendf(TEXT("Material: %s\n"), *MCPUtils::FormatName(Mat));
Result.Appendf(TEXT("Domain: %s\n"), *MCPUtils::EnumToString(Mat->MaterialDomain, TEXT("MD_")));
Result.Appendf(TEXT("BlendMode: %s\n"), *MCPUtils::EnumToString(Mat->BlendMode, TEXT("BLEND_")));
Result.Appendf(TEXT("TwoSided: %s\n"), Mat->IsTwoSided() ? TEXT("true") : TEXT("false"));
// Shading models
FMaterialShadingModelField SMField = Mat->GetShadingModels();
const UEnum* SMEnum = StaticEnum<EMaterialShadingModel>();
TArray<FString> SMNames;
for (int32 i = 0; i < SMEnum->NumEnums() - 1; ++i)
{
EMaterialShadingModel SM = (EMaterialShadingModel)SMEnum->GetValueByIndex(i);
if (SMField.HasShadingModel(SM))
SMNames.Add(SMEnum->GetNameStringByIndex(i));
}
Result.Appendf(TEXT("ShadingModels: %s\n"), *FString::Join(SMNames, TEXT(", ")));
// Parameters
auto Expressions = Mat->GetExpressions();
bool bHasParams = false;
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" Scalar \"%s\" = %g"), *SP->ParameterName.ToString(), SP->DefaultValue);
if (!SP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SP->Group.ToString());
Result.Append(TEXT("\n"));
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" Vector \"%s\" = (%.3f, %.3f, %.3f, %.3f)"),
*VP->ParameterName.ToString(),
VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
if (!VP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *VP->Group.ToString());
Result.Append(TEXT("\n"));
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" Texture \"%s\" = %s"),
*TP->ParameterName.ToString(),
TP->Texture ? *MCPUtils::FormatName(TP->Texture) : TEXT("None"));
if (!TP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *TP->Group.ToString());
Result.Append(TEXT("\n"));
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
if (!bHasParams) { Result.Append(TEXT("\nParameters:\n")); bHasParams = true; }
Result.Appendf(TEXT(" StaticSwitch \"%s\" = %s"),
*SSP->ParameterName.ToString(),
SSP->DefaultValue ? TEXT("true") : TEXT("false"));
if (!SSP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SSP->Group.ToString());
Result.Append(TEXT("\n"));
}
}
// Referenced textures
auto RefTexObjs = Mat->GetReferencedTextures();
bool bHasTextures = false;
for (const TObjectPtr<UObject>& TexObj : RefTexObjs)
{
if (!TexObj) continue;
if (!bHasTextures) { Result.Append(TEXT("\nReferenced Textures:\n")); bHasTextures = true; }
if (UTexture* Tex = Cast<UTexture>(TexObj.Get()))
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Tex));
else
Result.Appendf(TEXT(" %s\n"), *TexObj->GetPathName());
}
// Usage flags — only print enabled ones
Result.Append(TEXT("\nUsage Flags:"));
bool bAnyUsage = false;
auto EmitFlag = [&](bool bSet, const TCHAR* Name) {
if (bSet) { Result.Appendf(TEXT(" %s"), Name); bAnyUsage = true; }
};
EmitFlag(Mat->bUsedWithSkeletalMesh, TEXT("SkeletalMesh"));
EmitFlag(Mat->bUsedWithMorphTargets, TEXT("MorphTargets"));
EmitFlag(Mat->bUsedWithNiagaraSprites, TEXT("NiagaraSprites"));
EmitFlag(Mat->bUsedWithParticleSprites, TEXT("ParticleSprites"));
EmitFlag(Mat->bUsedWithStaticLighting, TEXT("StaticLighting"));
if (!bAnyUsage) Result.Append(TEXT(" (none)"));
Result.Append(TEXT("\n"));
// Stats
Result.Appendf(TEXT("Expressions: %d\n"), Expressions.Num());
int32 TextureSampleCount = 0;
for (UMaterialExpression* Expr : Expressions)
if (Expr && Expr->IsA<UMaterialExpressionTextureSample>())
TextureSampleCount++;
Result.Appendf(TEXT("TextureSamples: %d\n"), TextureSampleCount);
if (Mat->MaterialGraph)
Result.Appendf(TEXT("GraphNodes: %d\n"), Mat->MaterialGraph->Nodes.Num());
// Additional settings — only print non-default values
if (Mat->OpacityMaskClipValue != 0.3333f)
Result.Appendf(TEXT("OpacityMaskClipValue: %g\n"), Mat->OpacityMaskClipValue);
if (Mat->DitheredLODTransition)
Result.Append(TEXT("DitheredLODTransition: true\n"));
if (Mat->bAllowNegativeEmissiveColor)
Result.Append(TEXT("AllowNegativeEmissiveColor: true\n"));
}
void EmitMaterialInstance(UMaterialInstanceConstant* MI, FStringBuilderBase& Result)
{
Result.Appendf(TEXT("MaterialInstance: %s\n"), *MCPUtils::FormatName(MI));
if (MI->Parent)
{
if (UMaterial* ParentMat = Cast<UMaterial>(MI->Parent))
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentMat));
else if (UMaterialInstance* ParentMI = Cast<UMaterialInstance>(MI->Parent))
Result.Appendf(TEXT("Parent: %s\n"), *MCPUtils::FormatName(ParentMI));
else
Result.Appendf(TEXT("Parent: %s\n"), *MI->Parent->GetPathName());
}
// Overridden parameters
bool bHasParams = false;
auto EnsureHeader = [&]() {
if (!bHasParams) { Result.Append(TEXT("\nOverridden Parameters:\n")); bHasParams = true; }
};
for (const FScalarParameterValue& P : MI->ScalarParameterValues)
{
EnsureHeader();
Result.Appendf(TEXT(" Scalar \"%s\" = %g\n"), *P.ParameterInfo.Name.ToString(), P.ParameterValue);
}
for (const FVectorParameterValue& P : MI->VectorParameterValues)
{
EnsureHeader();
Result.Appendf(TEXT(" Vector \"%s\" = (%.3f, %.3f, %.3f, %.3f)\n"),
*P.ParameterInfo.Name.ToString(),
P.ParameterValue.R, P.ParameterValue.G, P.ParameterValue.B, P.ParameterValue.A);
}
for (const FTextureParameterValue& P : MI->TextureParameterValues)
{
EnsureHeader();
if (P.ParameterValue)
{
Result.Appendf(TEXT(" Texture \"%s\" = %s\n"),
*P.ParameterInfo.Name.ToString(), *MCPUtils::FormatName(P.ParameterValue));
}
else
{
Result.Appendf(TEXT(" Texture \"%s\" = None\n"), *P.ParameterInfo.Name.ToString());
}
}
for (const FStaticSwitchParameter& P : MI->GetStaticParameters().StaticSwitchParameters)
{
EnsureHeader();
Result.Appendf(TEXT(" StaticSwitch \"%s\" = %s%s\n"),
*P.ParameterInfo.Name.ToString(),
P.Value ? TEXT("true") : TEXT("false"),
P.bOverride ? TEXT("") : TEXT(" (not overridden)"));
}
}
};

View File

@@ -1,147 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionFunctionInput.h"
#include "Materials/MaterialExpressionFunctionOutput.h"
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "Materials/MaterialExpressionConstant.h"
#include "Materials/MaterialExpressionConstant3Vector.h"
#include "Materials/MaterialExpressionConstant4Vector.h"
#include "Materials/MaterialExpressionTextureObjectParameter.h"
#include "Materials/MaterialExpressionTextureSample.h"
#include "Materials/MaterialExpressionTextureCoordinate.h"
#include "Materials/MaterialExpressionComponentMask.h"
#include "Materials/MaterialExpressionCustom.h"
#include "Engine/Texture.h"
#include "UMCPHandler_DumpMaterialFunction.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
MCPAssets<UMaterialFunction> Assets;
if (!Assets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
UMaterialFunction* MF = Assets.Object();
Result.Appendf(TEXT("MaterialFunction: %s\n"), *MCPUtils::FormatName(MF));
FString Desc = MF->GetDescription();
if (!Desc.IsEmpty())
Result.Appendf(TEXT("Description: %s\n"), *Desc);
auto Expressions = MF->GetExpressions();
Result.Appendf(TEXT("Expressions: %d\n"), Expressions.Num());
// Inputs and outputs
bool bHasInputs = false;
bool bHasOutputs = false;
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
if (auto* FI = Cast<UMaterialExpressionFunctionInput>(Expr))
{
if (!bHasInputs) { Result.Append(TEXT("\nInputs:\n")); bHasInputs = true; }
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Expr));
}
else if (auto* FO = Cast<UMaterialExpressionFunctionOutput>(Expr))
{
if (!bHasOutputs) { Result.Append(TEXT("\nOutputs:\n")); bHasOutputs = true; }
Result.Appendf(TEXT(" %s\n"), *MCPUtils::FormatName(Expr));
}
}
// All expressions
Result.Append(TEXT("\nExpression List:\n"));
for (UMaterialExpression* Expr : Expressions)
{
if (!Expr) continue;
Result.Appendf(TEXT(" %s"), *MCPUtils::FormatName(Expr));
EmitExpressionDetails(Expr, Result);
Result.Append(TEXT("\n"));
}
}
private:
void EmitExpressionDetails(UMaterialExpression* Expr, FStringBuilderBase& Result)
{
if (auto* SP = Cast<UMaterialExpressionScalarParameter>(Expr))
{
Result.Appendf(TEXT(" default=%g"), SP->DefaultValue);
if (!SP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SP->Group.ToString());
}
else if (auto* VP = Cast<UMaterialExpressionVectorParameter>(Expr))
{
Result.Appendf(TEXT(" default=(%.3f, %.3f, %.3f, %.3f)"),
VP->DefaultValue.R, VP->DefaultValue.G, VP->DefaultValue.B, VP->DefaultValue.A);
if (!VP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *VP->Group.ToString());
}
else if (auto* TP = Cast<UMaterialExpressionTextureSampleParameter2D>(Expr))
{
Result.Appendf(TEXT(" texture=%s"),
TP->Texture ? *MCPUtils::FormatName(TP->Texture) : TEXT("None"));
if (!TP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *TP->Group.ToString());
}
else if (auto* SSP = Cast<UMaterialExpressionStaticSwitchParameter>(Expr))
{
Result.Appendf(TEXT(" default=%s"), SSP->DefaultValue ? TEXT("true") : TEXT("false"));
if (!SSP->Group.IsNone()) Result.Appendf(TEXT(" [%s]"), *SSP->Group.ToString());
}
else if (auto* SC = Cast<UMaterialExpressionConstant>(Expr))
{
Result.Appendf(TEXT(" value=%g"), SC->R);
}
else if (auto* C3 = Cast<UMaterialExpressionConstant3Vector>(Expr))
{
Result.Appendf(TEXT(" value=(%.3f, %.3f, %.3f)"), C3->Constant.R, C3->Constant.G, C3->Constant.B);
}
else if (auto* C4 = Cast<UMaterialExpressionConstant4Vector>(Expr))
{
Result.Appendf(TEXT(" value=(%.3f, %.3f, %.3f, %.3f)"),
C4->Constant.R, C4->Constant.G, C4->Constant.B, C4->Constant.A);
}
else if (auto* FC = Cast<UMaterialExpressionMaterialFunctionCall>(Expr))
{
if (FC->MaterialFunction)
Result.Appendf(TEXT(" calls=%s"), *FC->MaterialFunction->GetPathName());
}
else if (auto* TS = Cast<UMaterialExpressionTextureSample>(Expr))
{
if (TS->Texture)
Result.Appendf(TEXT(" texture=%s"), *MCPUtils::FormatName(TS->Texture));
}
else if (auto* TC = Cast<UMaterialExpressionTextureCoordinate>(Expr))
{
Result.Appendf(TEXT(" index=%d tiling=(%.1f, %.1f)"), TC->CoordinateIndex, TC->UTiling, TC->VTiling);
}
else if (auto* Custom = Cast<UMaterialExpressionCustom>(Expr))
{
Result.Appendf(TEXT(" code_len=%d"), Custom->Code.Len());
}
}
};

View File

@@ -1,75 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "StructUtils/UserDefinedStruct.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "UMCPHandler_RemoveStructField.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
class UMCPHandler_RemoveStructField : public UObject, public IMCPHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Struct name or package path"))
FString Struct;
UPROPERTY(meta=(Description="Name of the field to remove"))
FString FieldName;
virtual FString GetDescription() const override
{
return TEXT("Remove a field from a UserDefinedStruct asset.");
}
virtual void Handle(const FJsonObject* Json, FStringBuilderBase& Result) override
{
MCPFetcher F(Result);
UUserDefinedStruct* S = F.Walk(Struct).Cast<UUserDefinedStruct>();
if (!S) return;
// Find the field GUID by name
FGuid TargetGuid;
bool bFound = false;
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
{
if (Var.FriendlyName.Equals(FieldName, ESearchCase::IgnoreCase) ||
Var.VarName.ToString().Equals(FieldName, ESearchCase::IgnoreCase))
{
TargetGuid = Var.VarGuid;
bFound = true;
break;
}
}
if (!bFound)
{
Result.Appendf(TEXT("ERROR: Field '%s' not found in %s.\nAvailable fields:\n"),
*FieldName, *MCPUtils::FormatName(S));
for (const FStructVariableDescription& Var : FStructureEditorUtils::GetVarDesc(S))
{
Result.Appendf(TEXT(" %s\n"), *Var.FriendlyName);
}
return;
}
if (!FStructureEditorUtils::RemoveVariable(S, TargetGuid))
{
Result.Appendf(TEXT("ERROR: Failed to remove field '%s'."), *FieldName);
return;
}
bool bSaved = MCPUtils::SaveGenericPackage(S);
Result.Appendf(TEXT("Removed field %s from %s.%s\n"),
*FieldName, *MCPUtils::FormatName(S),
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -1,106 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.h"
#include "Materials/MaterialExpressionTextureSampleParameter2D.h"
#include "Materials/MaterialExpressionStaticSwitchParameter.h"
#include "UMCPHandler_SearchWithinMaterials.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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, FStringBuilderBase& Result) override
{
FString DecodedQuery = MCPUtils::UrlDecode(Query);
MaxResults = FMath::Clamp(MaxResults, 1, 200);
int32 Count = 0;
MCPAssets<UMaterial> AllMaterials;
AllMaterials.Load();
for (UMaterial* MaterialObj : AllMaterials.Objects())
{
if (Count >= MaxResults) break;
if (!MaterialObj) continue;
FString MatName = MCPUtils::FormatName(MaterialObj);
// Check material name
bool bNameMatch = MatName.Contains(DecodedQuery, ESearchCase::IgnoreCase);
if (bNameMatch)
{
Result.Appendf(TEXT("material %s\n"), *MatName);
Count++;
}
// Search expressions
for (UMaterialExpression* Expr : MaterialObj->GetExpressions())
{
if (!Expr || Count >= MaxResults) continue;
FString ExprName = MCPUtils::FormatName(Expr);
FString ExprClass = Expr->GetClass()->GetName();
FString ExprDesc = Expr->GetDescription();
// 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) continue;
Result.Appendf(TEXT("expression %s in %s (%s)"), *ExprName, *MatName, *ExprClass);
if (!ParamName.IsEmpty())
Result.Appendf(TEXT(" param=%s"), *ParamName);
Result.Append(TEXT("\n"));
Count++;
}
}
if (Count == 0)
{
Result.Append(TEXT("No matches found.\n"));
}
else if (Count >= MaxResults)
{
Result.Appendf(TEXT("WARNING: Reached limit of %d results. Specify MaxResults to raise it.\n"), MaxResults);
}
}
};

View File

@@ -1,132 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialFunction.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UMCPHandler_SetMaterialExpressionPosition.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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="Expression name (use FormatName from DumpMaterial output)"))
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, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
MCPErrorCallback(Result).SetError(TEXT("Specify 'material' or 'materialFunction'."));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
MCPErrorCallback(Result).SetError(TEXT("Asset has no material graph."));
return;
}
// Find node by name
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(GraphNode);
if (!MatNode || !MatNode->MaterialExpression) continue;
if (MCPUtils::Identifies(Node, MatNode->MaterialExpression))
{
TargetMatNode = MatNode;
break;
}
}
if (!TargetMatNode)
{
MCPErrorCallback(Result).SetError(FString::Printf(TEXT("Expression '%s' not found."), *Node));
return;
}
if (DryRun)
{
Result.Appendf(TEXT("DryRun: would move %s to (%d, %d)\n"),
*MCPUtils::FormatName(TargetMatNode->MaterialExpression), PosX, PosY);
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);
Result.Appendf(TEXT("Moved %s to (%d, %d)"),
*MCPUtils::FormatName(TargetMatNode->MaterialExpression), PosX, PosY);
if (!bSaved) Result.Append(TEXT(" (save failed)"));
Result.Append(TEXT("\n"));
}
};

View File

@@ -1,282 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPHandler.h"
#include "MCPAssets.h"
#include "MCPFetcher.h"
#include "MCPUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialFunction.h"
#include "Materials/MaterialExpression.h"
#include "Materials/MaterialExpressionScalarParameter.h"
#include "Materials/MaterialExpressionVectorParameter.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 "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "UMCPHandler_SetMaterialExpressionProperty.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS(meta=(Group="Unclassified"))
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="Expression node name (from FormatName)"))
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, FStringBuilderBase& Result) override
{
if (Material.IsEmpty() && MaterialFunction.IsEmpty())
{
Result.Append(TEXT("ERROR: Specify 'material' or 'materialFunction'\n"));
return;
}
if (!Json->HasField(TEXT("value")))
{
Result.Append(TEXT("ERROR: Missing required field: value\n"));
return;
}
// Load material or material function
UMaterial* MaterialObj = nullptr;
UMaterialFunction* MatFunc = nullptr;
if (!MaterialFunction.IsEmpty())
{
MCPAssets<UMaterialFunction> MFAssets;
if (!MFAssets.Exact(MaterialFunction).Errors(Result).ENone().ETwo().Load()) return;
MatFunc = MFAssets.Object();
}
else
{
MCPAssets<UMaterial> MatAssets;
if (!MatAssets.Exact(Material).Errors(Result).ENone().ETwo().Load()) return;
MaterialObj = MatAssets.Object();
}
if (MaterialObj) MCPUtils::EnsureMaterialGraph(MaterialObj);
UEdGraph* Graph = MaterialObj ? (UEdGraph*)MaterialObj->MaterialGraph : (MatFunc ? MatFunc->MaterialGraph : nullptr);
if (!Graph)
{
Result.Appendf(TEXT("ERROR: No material graph found\n"));
return;
}
// Find the node by name using Identifies
UMaterialGraphNode* TargetMatNode = nullptr;
for (UEdGraphNode* GraphNode : Graph->Nodes)
{
if (!GraphNode) continue;
if (!MCPUtils::Identifies(Node, GraphNode)) continue;
TargetMatNode = Cast<UMaterialGraphNode>(GraphNode);
if (TargetMatNode) break;
}
if (!TargetMatNode)
{
Result.Appendf(TEXT("ERROR: Node '%s' not found in material graph\n"), *Node);
return;
}
UMaterialExpression* Expr = TargetMatNode->MaterialExpression;
if (!Expr)
{
Result.Appendf(TEXT("ERROR: Node '%s' has no material expression\n"), *MCPUtils::FormatName(TargetMatNode));
return;
}
UObject* Asset = MaterialObj ? (UObject*)MaterialObj : (UObject*)MatFunc;
Asset->PreEditChange(nullptr);
FString SetResult;
if (!ApplyValue(Expr, Json, Result, SetResult))
{
Asset->PostEditChange();
return;
}
Asset->PostEditChange();
Asset->MarkPackageDirty();
bool bSaved = MaterialObj ? MCPUtils::SaveMaterialPackage(MaterialObj) : MCPUtils::SaveGenericPackage(MatFunc);
Result.Appendf(TEXT("%s = %s"), *MCPUtils::FormatName(Expr), *SetResult);
if (!bSaved) Result.Append(TEXT(" (save failed)"));
Result.Append(TEXT("\n"));
}
private:
// Apply the value from JSON to the expression. Returns false on error (with message in Result).
// On success, fills SetResult with a human-readable summary of the new value.
bool ApplyValue(UMaterialExpression* Expr, const FJsonObject* Json, FStringBuilderBase& Result, FString& SetResult)
{
if (auto* E = Cast<UMaterialExpressionConstant>(Expr))
{
double Value = Json->GetNumberField(TEXT("value"));
E->R = (float)Value;
SetResult = FString::Printf(TEXT("%g"), Value);
return true;
}
if (auto* E = Cast<UMaterialExpressionConstant3Vector>(Expr))
{
FLinearColor C;
if (!ParseColorValue(Json, C, false, Result)) return false;
E->Constant = C;
SetResult = FString::Printf(TEXT("(%.3f, %.3f, %.3f)"), C.R, C.G, C.B);
return true;
}
if (auto* E = Cast<UMaterialExpressionConstant4Vector>(Expr))
{
FLinearColor C;
if (!ParseColorValue(Json, C, true, Result)) return false;
E->Constant = C;
SetResult = FString::Printf(TEXT("(%.3f, %.3f, %.3f, %.3f)"), C.R, C.G, C.B, C.A);
return true;
}
if (auto* E = Cast<UMaterialExpressionScalarParameter>(Expr))
{
double Value = Json->GetNumberField(TEXT("value"));
E->DefaultValue = (float)Value;
SetResult = FString::Printf(TEXT("%g"), Value);
ApplyParameterName(E, Json);
return true;
}
if (auto* E = Cast<UMaterialExpressionVectorParameter>(Expr))
{
FLinearColor C;
if (!ParseColorValue(Json, C, true, Result)) return false;
E->DefaultValue = C;
SetResult = FString::Printf(TEXT("(%.3f, %.3f, %.3f, %.3f)"), C.R, C.G, C.B, C.A);
ApplyParameterName(E, Json);
return true;
}
if (auto* E = Cast<UMaterialExpressionTextureCoordinate>(Expr))
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Append(TEXT("ERROR: TextureCoordinate requires value as {coordinateIndex, uTiling, vTiling}\n"));
return false;
}
double CoordIndex = 0, UTiling = 1, VTiling = 1;
(*ValueObj)->TryGetNumberField(TEXT("coordinateIndex"), CoordIndex);
(*ValueObj)->TryGetNumberField(TEXT("uTiling"), UTiling);
(*ValueObj)->TryGetNumberField(TEXT("vTiling"), VTiling);
E->CoordinateIndex = (int32)CoordIndex;
E->UTiling = (float)UTiling;
E->VTiling = (float)VTiling;
SetResult = FString::Printf(TEXT("index=%d uTiling=%g vTiling=%g"), (int32)CoordIndex, UTiling, VTiling);
return true;
}
if (auto* E = Cast<UMaterialExpressionCustom>(Expr))
{
FString Code;
if (Json->TryGetStringField(TEXT("code"), Code))
{
E->Code = Code;
}
else if (Json->HasField(TEXT("value")))
{
FString ValueStr = Json->GetStringField(TEXT("value"));
if (!ValueStr.IsEmpty()) E->Code = ValueStr;
}
SetResult = FString::Printf(TEXT("code: %d chars"), E->Code.Len());
FString OutputTypeStr;
if (Json->TryGetStringField(TEXT("outputType"), OutputTypeStr) && !OutputTypeStr.IsEmpty())
{
ECustomMaterialOutputType OutType;
if (MCPUtils::StringToEnum(OutputTypeStr, OutType, MCPErrorCallback(Result)))
E->OutputType = OutType;
}
return true;
}
if (auto* E = Cast<UMaterialExpressionComponentMask>(Expr))
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Append(TEXT("ERROR: ComponentMask requires value as {r, g, b, a} (booleans)\n"));
return false;
}
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);
E->R = bR ? 1 : 0;
E->G = bG ? 1 : 0;
E->B = bB ? 1 : 0;
E->A = bA ? 1 : 0;
SetResult = 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"));
return true;
}
Result.Appendf(TEXT("ERROR: Expression type '%s' does not support value setting. Supported: "
"Constant, Constant3Vector, Constant4Vector, ScalarParameter, VectorParameter, "
"TextureCoordinate, Custom, ComponentMask\n"),
*Expr->GetClass()->GetName());
return false;
}
// Parse {r, g, b[, a]} from the "value" JSON field.
bool ParseColorValue(const FJsonObject* Json, FLinearColor& OutColor, bool bHasAlpha, FStringBuilderBase& Result)
{
const TSharedPtr<FJsonObject>* ValueObj = nullptr;
if (!Json->TryGetObjectField(TEXT("value"), ValueObj) || !ValueObj || !(*ValueObj).IsValid())
{
Result.Appendf(TEXT("ERROR: requires value as {r, g, b%s}\n"), bHasAlpha ? TEXT(", a") : TEXT(""));
return false;
}
double R = 0, G = 0, B = 0, A = 1;
(*ValueObj)->TryGetNumberField(TEXT("r"), R);
(*ValueObj)->TryGetNumberField(TEXT("g"), G);
(*ValueObj)->TryGetNumberField(TEXT("b"), B);
if (bHasAlpha) (*ValueObj)->TryGetNumberField(TEXT("a"), A);
OutColor = FLinearColor((float)R, (float)G, (float)B, (float)A);
return true;
}
// If the JSON has a "parameterName" field, apply it to a parameter expression.
void ApplyParameterName(UMaterialExpressionParameter* Param, const FJsonObject* Json)
{
FString ParamName;
if (Json->TryGetStringField(TEXT("parameterName"), ParamName) && !ParamName.IsEmpty())
Param->ParameterName = FName(*ParamName);
}
};