UE Wingman renaming complete.

This commit is contained in:
2026-03-18 10:29:38 -04:00
parent c55c5d8953
commit a2f6a21d29
134 changed files with 36 additions and 36 deletions

View File

@@ -0,0 +1,67 @@
#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

@@ -0,0 +1,23 @@
#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

@@ -0,0 +1,450 @@
#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

@@ -0,0 +1,79 @@
#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

@@ -0,0 +1,127 @@
#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

@@ -0,0 +1,95 @@
#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

@@ -0,0 +1,169 @@
#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(meta=(Group="Unclassified"))
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

@@ -0,0 +1,73 @@
#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

@@ -0,0 +1,229 @@
#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

@@ -0,0 +1,188 @@
#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

@@ -0,0 +1,126 @@
#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

@@ -0,0 +1,188 @@
#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

@@ -0,0 +1,98 @@
#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

@@ -0,0 +1,228 @@
#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

@@ -0,0 +1,147 @@
#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

@@ -0,0 +1,75 @@
#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

@@ -0,0 +1,106 @@
#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

@@ -0,0 +1,132 @@
#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

@@ -0,0 +1,282 @@
#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);
}
};

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 BlueprintMCP Contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,72 @@
= History
This plugin was originally vibe-coded by Claude Code. It
was then posted to David Gundry's github account, here:
https://github.com/mirno-ehf/ue5-mcp
He released it under the MIT license.
How do I know it was vibe coded? Because he says so:
> For Humans:
>
> You're welcome here, but probably not in the way you'd
> expect. This project is built and maintained entirely by
> AI coding agents — Claude Code, Cursor, Copilot Workspace,
> and the like. We don't accept human-written code or
> human-opened issues.
Huh, interesting. I (Josh Yelon) downloaded the code, and
evaluated it. I was curious what vibe coding could do.
Here's what I found:
* The code, overall, had issues, but in the end, it wasn't
that bad. It did work, mostly - and that matters. I wanted
my own working MCP server, and starting from a place where
you have working code is a lot easier than starting from
a blank slate.
* Claude really doesn't build any mechanisms to enforce
consistency. For example: there were all the handlers, and
then in a different file, declarations of all the handlers,
then in another file registrations of all the handlers with
the webserver, then in another place a list of all the
handlers that require undo support, then in an entirely
other directory, a list of all the handlers and their
parameters. If any of these lists got out of sync, it would
break.
* Claude doesn't seem to care at all about extra layers upon
layers, or about dependencies. Rather than building an MCP
server in an unreal plugin, it built a *web* server in an
unreal plugin, then built an MCP-to-web translation program
in *javascript*. Requests to the server were getting
translated from json into URL parameters and then back into
json. Ouch. The javascript required installation of about
a thousand javascript libraries, some of which were not easy
to install. Why not just build an MCP server in the Unreal
plugin directly, skipping all the dependencies and
translation layers?
* Claude will repeat code over and over. I found endless
places where there were 10 copies of the same function, with
trivial variations, that could have all been merged into one
function with a parameter or two. When it does repeat code,
the repetitions are not generally consistent with each other:
one variant might have a bug, whereas another is fine.
* There were lots of little edge-case bugs throughout the
code. Claude is actually not bad about noticing edge cases,
but it misses some.
Despite all this, it was much easier to start from something
than to start from nothing.
So, I undertook a massive refactoring effort. I did use
Claude Code extensively - in fact, I'd say Claude did 70% of
the work. But I monitored every step, and constantly pushed
Claude hard to use better software engineering practices.
The result, I think, is a pretty clean MCP server.

View File

@@ -0,0 +1,108 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
bool bSaved = WingUtils::SaveBlueprintPackage(NewAnimBP);
UWingServer::Printf(TEXT("Created: %s\n"), *AssetPath);
UWingServer::Printf(TEXT("ParentClass: %s\n"), *WingUtils::FormatName(ParentClassObj));
UWingServer::Printf(TEXT("Saved: %s\n"), bSaved ? TEXT("true") : TEXT("false"));
TArray<UEdGraph*> Graphs = WingUtils::AllGraphs(NewAnimBP);
for (UEdGraph* Graph : Graphs)
{
UWingServer::Printf(TEXT("Graph: %s\n"), *WingUtils::FormatName(Graph));
}
}
};

View File

@@ -0,0 +1,64 @@
#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_ListSlotNames.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_AnimBlueprint_ListSlotNames : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Animation Blueprint package path"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("List all animation slot names used in an Animation Blueprint.");
}
virtual void Handle() override
{
WingFetcher F;
UAnimBlueprint* AnimBP = F.Asset(Blueprint).Cast<UAnimBlueprint>();
if (!AnimBP) return;
// Walk all anim nodes to collect slot names
TSet<FString> SlotNames;
for (UAnimGraphNode_Base* AnimNode : WingUtils::AllNodes<UAnimGraphNode_Base>(AnimBP))
{
for (TFieldIterator<FNameProperty> PropIt(AnimNode->GetClass()); PropIt; ++PropIt)
{
if (PropIt->GetName().Contains(TEXT("SlotName")) || PropIt->GetName().Contains(TEXT("Slot")))
{
FName SlotValue = PropIt->GetPropertyValue_InContainer(AnimNode);
if (!SlotValue.IsNone())
{
SlotNames.Add(SlotValue.ToString());
}
}
}
}
for (const FString& Slot : SlotNames)
{
UWingServer::Printf(TEXT("%s\n"), *Slot);
}
if (SlotNames.Num() == 0)
{
UWingServer::Print(TEXT("No animation slot names found.\n"));
}
}
};

View File

@@ -0,0 +1,65 @@
#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

@@ -0,0 +1,138 @@
#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();
// Save
bool bSaved = WingUtils::SaveGenericPackage(BS);
UWingServer::Printf(TEXT("Set %d samples on %s\n"), SamplesSet, *WingUtils::FormatName(BS));
if (!bSaved)
{
UWingServer::Print(TEXT("WARNING: package save failed\n"));
}
}
};

View File

@@ -0,0 +1,65 @@
#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();
bool bSaved = WingUtils::SaveGenericPackage(NewBS);
UWingServer::Printf(TEXT("Created %s\n"), *NewBS->GetPathName());
UWingServer::Printf(TEXT("Skeleton: %s\n"), *SkeletonObj->GetPathName());
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -0,0 +1,120 @@
#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 "EdGraphSchema_K2.h"
#include "K2Node_CustomEvent.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintGraph_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintGraph_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name for the new graph"))
FString Graph;
UPROPERTY(meta=(Description="Type of graph: function, macro, or customEvent"))
FString GraphType;
virtual FString GetDescription() const override
{
return TEXT("Create a new function, macro, or custom event graph in a Blueprint.");
}
virtual void Handle() override
{
if (GraphType != TEXT("function") && GraphType != TEXT("macro") && GraphType != TEXT("customEvent"))
{
UWingServer::Printf(TEXT("ERROR: Invalid GraphType '%s'. Valid values: function, macro, customEvent\n"), *GraphType);
return;
}
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Check graph name uniqueness
if (!WingUtils::AllGraphsNamed(BP, Graph).IsEmpty())
{
UWingServer::Printf(TEXT("ERROR: A graph named '%s' already exists in %s\n"), *Graph, *WingUtils::FormatName(BP));
return;
}
// For custom events, also check for existing custom events with the same name
if (GraphType == TEXT("customEvent"))
{
for (UK2Node_CustomEvent* CE : WingUtils::AllNodes<UK2Node_CustomEvent>(BP))
{
if (CE->CustomFunctionName == FName(*Graph))
{
UWingServer::Printf(TEXT("ERROR: A custom event named '%s' already exists in %s\n"), *Graph, *WingUtils::FormatName(BP));
return;
}
}
}
if (GraphType == TEXT("function"))
{
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph)
{
UWingServer::Print(TEXT("ERROR: Failed to create function graph\n"));
return;
}
FBlueprintEditorUtils::AddFunctionGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromObject=*/static_cast<UClass*>(nullptr));
UWingServer::Printf(TEXT("Created function graph: %s\n"), *WingUtils::FormatName(NewGraph));
}
else if (GraphType == TEXT("macro"))
{
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, FName(*Graph),
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!NewGraph)
{
UWingServer::Print(TEXT("ERROR: Failed to create macro graph\n"));
return;
}
FBlueprintEditorUtils::AddMacroGraph(BP, NewGraph, /*bIsUserCreated=*/true, /*SignatureFromClass=*/nullptr);
UWingServer::Printf(TEXT("Created macro graph: %s\n"), *WingUtils::FormatName(NewGraph));
}
else // customEvent
{
UEdGraph* EventGraph = nullptr;
if (BP->UbergraphPages.Num() > 0)
EventGraph = BP->UbergraphPages[0];
if (!EventGraph)
{
UWingServer::Print(TEXT("ERROR: Blueprint has no EventGraph to add a custom event to\n"));
return;
}
UK2Node_CustomEvent* NewEvent = NewObject<UK2Node_CustomEvent>(EventGraph);
NewEvent->CustomFunctionName = FName(*Graph);
NewEvent->bIsEditable = true;
EventGraph->AddNode(NewEvent, /*bFromUI=*/false, /*bSelectNewNode=*/false);
NewEvent->CreateNewGuid();
NewEvent->PostPlacedNewNode();
NewEvent->AllocateDefaultPins();
UWingServer::Printf(TEXT("Created custom event: %s\n"), *WingUtils::FormatName(NewEvent));
}
WingUtils::SaveBlueprintPackage(BP);
}
};

View File

@@ -0,0 +1,96 @@
#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 "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintGraph_Delete.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintGraph_Delete : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/Bar"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the graph to delete"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Delete a function or macro graph from a Blueprint. Cannot delete EventGraph pages.");
}
virtual void Handle() override
{
WingFetcher F;
F.Walk(Blueprint);
if (!F.Ok()) return;
UBlueprint* BP = F.Cast<UBlueprint>();
if (!BP) return;
// Search function graphs, then macro graphs
UEdGraph* TargetGraph = nullptr;
FString GraphType;
for (UEdGraph* G : BP->FunctionGraphs)
{
if (G && WingUtils::Identifies(Graph, G))
{
TargetGraph = G;
GraphType = TEXT("function");
break;
}
}
if (!TargetGraph)
{
for (UEdGraph* G : BP->MacroGraphs)
{
if (G && WingUtils::Identifies(Graph, G))
{
TargetGraph = G;
GraphType = TEXT("macro");
break;
}
}
}
// Check if it's an UbergraphPage (EventGraph) — disallow deletion
if (!TargetGraph)
{
for (UEdGraph* G : BP->UbergraphPages)
{
if (G && WingUtils::Identifies(Graph, G))
{
UWingServer::Printf(TEXT("ERROR: Cannot delete UbergraphPage '%s'. EventGraph pages cannot be deleted.\n"),
*WingUtils::FormatName(G));
return;
}
}
UWingServer::Printf(TEXT("ERROR: Graph '%s' not found in blueprint %s\n"),
*Graph, *WingUtils::FormatName(BP));
return;
}
// Remove the graph
FString GraphName = WingUtils::FormatName(TargetGraph);
FBlueprintEditorUtils::RemoveGraph(BP, TargetGraph, EGraphRemoveFlags::Default);
bool bSaved = WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Deleted %s graph %s\n"), *GraphType, *GraphName);
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Package save failed.\n"));
}
};

View File

@@ -0,0 +1,84 @@
#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 "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintGraph_Rename.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintGraph_Rename : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to the graph, e.g. /Game/Foo,graph:MyFunction"))
FString Graph;
UPROPERTY(meta=(Description="New name for the graph"))
FString NewName;
virtual FString GetDescription() const override
{
return TEXT("Rename a function or macro graph in a Blueprint. Cannot rename EventGraph pages.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
UBlueprint* BP = Cast<UBlueprint>(TargetGraph->GetOuter());
if (!BP)
{
UWingServer::Printf(TEXT("Error: Graph '%s' is not owned by a Blueprint.\n"), *Graph);
return;
}
// Check if it's an UbergraphPage -- disallow rename
if (BP->UbergraphPages.Contains(TargetGraph))
{
UWingServer::Printf(TEXT("Error: Cannot rename UbergraphPage '%s'. EventGraph pages cannot be renamed.\n"),
*WingUtils::FormatName(TargetGraph));
return;
}
// Verify it's a function or macro graph
bool bIsFunction = BP->FunctionGraphs.Contains(TargetGraph);
bool bIsMacro = BP->MacroGraphs.Contains(TargetGraph);
if (!bIsFunction && !bIsMacro)
{
UWingServer::Printf(TEXT("Error: Graph '%s' is not a function or macro graph.\n"),
*WingUtils::FormatName(TargetGraph));
return;
}
// Check for name collision
for (UEdGraph* Existing : WingUtils::AllGraphsNamed(BP, NewName))
{
if (Existing != TargetGraph)
{
UWingServer::Printf(TEXT("Error: A graph named '%s' already exists in '%s'.\n"),
*NewName, *WingUtils::FormatName(BP));
return;
}
}
FBlueprintEditorUtils::RenameGraph(TargetGraph, NewName);
WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Renamed to %s %s\n"),
bIsFunction ? TEXT("function") : TEXT("macro"),
*WingUtils::FormatName(TargetGraph));
}
};

View File

@@ -0,0 +1,135 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingServer.h"
#include "Engine/Blueprint.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Components/ActorComponent.h"
#include "Blueprint_AddComponent.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_AddComponent : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Component class name (e.g. StaticMeshComponent, SceneComponent)"))
FString ComponentClass;
UPROPERTY(meta=(Description="Component name for the new component"))
FString Component;
UPROPERTY(meta=(Optional, Description="Name of the parent component to attach to"))
FString ParentComponent;
virtual FString GetDescription() const override
{
return TEXT("Add a component to a Blueprint's SimpleConstructionScript. "
"Optionally attach it to an existing parent component.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
if (!BP) return;
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
UWingServer::Printf(TEXT("ERROR: Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)\n"),
*WingUtils::FormatName(BP));
return;
}
// Check for duplicate component names
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
for (USCS_Node* Existing : ExistingNodes)
{
if (Existing && Existing->ComponentTemplate &&
WingUtils::Identifies(Component, Existing->ComponentTemplate))
{
UWingServer::Printf(TEXT("ERROR: A component named '%s' already exists in Blueprint '%s'\n"),
*Component, *WingUtils::FormatName(BP));
return;
}
}
// Resolve the component class by name
UClass* ComponentClassObj = WingUtils::FindClassByName(ComponentClass);
if (!ComponentClassObj || !ComponentClassObj->IsChildOf(UActorComponent::StaticClass()))
{
UWingServer::Printf(TEXT("ERROR: Component class '%s' not found or is not a subclass of UActorComponent. "
"Common classes: StaticMeshComponent, SkeletalMeshComponent, AudioComponent, "
"SceneComponent, BoxCollisionComponent, SphereCollisionComponent, CapsuleComponent, "
"ArrowComponent, ChildActorComponent, SpotLightComponent, PointLightComponent, "
"WidgetComponent, BillboardComponent\n"),
*ComponentClass);
return;
}
// If parent component specified, find its SCS node
USCS_Node* ParentSCSNode = nullptr;
if (!ParentComponent.IsEmpty())
{
for (USCS_Node* Node : ExistingNodes)
{
if (Node && Node->ComponentTemplate &&
WingUtils::Identifies(ParentComponent, Node->ComponentTemplate))
{
ParentSCSNode = Node;
break;
}
}
if (!ParentSCSNode)
{
UWingServer::Printf(TEXT("ERROR: Parent component '%s' not found in Blueprint '%s'\n"),
*ParentComponent, *WingUtils::FormatName(BP));
return;
}
}
// Create the SCS node
USCS_Node* NewNode = SCS->CreateNode(ComponentClassObj, FName(*Component));
if (!NewNode)
{
UWingServer::Printf(TEXT("ERROR: Failed to create SCS node for component '%s' with class '%s'\n"),
*Component, *WingUtils::FormatName(ComponentClassObj));
return;
}
// Add to the hierarchy
if (ParentSCSNode)
{
ParentSCSNode->AddChildNode(NewNode);
}
else
{
SCS->AddNode(NewNode);
}
bool bSaved = WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Added component %s (%s)"),
*WingUtils::FormatName(NewNode->ComponentTemplate),
*WingUtils::FormatName(ComponentClassObj));
if (ParentSCSNode)
{
UWingServer::Printf(TEXT(" under %s"), *WingUtils::FormatName(ParentSCSNode->ComponentTemplate));
}
UWingServer::Printf(TEXT("\nSaved: %s\n"), bSaved ? TEXT("true") : TEXT("false"));
}
};

View File

@@ -0,0 +1,148 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingTypes.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Blueprint_AddEventDispatcher.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FDispatcherParamEntry
{
GENERATED_BODY()
UPROPERTY()
FString Name;
UPROPERTY()
FString Type;
};
UCLASS()
class UWing_Blueprint_AddEventDispatcher : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/MyBlueprint"))
FString Blueprint;
UPROPERTY(meta=(Description="Name for the new event dispatcher"))
FString DispatcherName;
UPROPERTY(meta=(Optional, Description="Array of parameter objects, each with 'name' and 'type' fields"))
FWingJsonArray Parameters;
virtual FString GetDescription() const override
{
return TEXT("Create a new multicast event dispatcher on a Blueprint, optionally with parameters.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
FName DispatcherFName(*DispatcherName);
// Check for name uniqueness against existing variables
for (const FBPVariableDescription& Var : BP->NewVariables)
{
if (Var.VarName == DispatcherFName)
{
UWingServer::Printf(TEXT("Error: A variable or dispatcher named '%s' already exists.\n"), *DispatcherName);
return;
}
}
// Check against existing graphs (functions, macros, etc.)
if (!WingUtils::AllGraphsNamed(BP, DispatcherName).IsEmpty())
{
UWingServer::Printf(TEXT("Error: A graph named '%s' already exists.\n"), *DispatcherName);
return;
}
// Add a member variable with PC_MCDelegate pin type
FEdGraphPinType DelegateType;
DelegateType.PinCategory = UEdGraphSchema_K2::PC_MCDelegate;
if (!FBlueprintEditorUtils::AddMemberVariable(BP, DispatcherFName, DelegateType))
{
UWingServer::Printf(TEXT("Error: Failed to add delegate variable for '%s'.\n"), *DispatcherName);
return;
}
// Create the signature graph
const UEdGraphSchema_K2* K2Schema = GetDefault<UEdGraphSchema_K2>();
UEdGraph* SigGraph = FBlueprintEditorUtils::CreateNewGraph(BP, DispatcherFName,
UEdGraph::StaticClass(), UEdGraphSchema_K2::StaticClass());
if (!SigGraph)
{
UWingServer::Print(TEXT("Error: Failed to create delegate signature graph.\n"));
return;
}
K2Schema->CreateDefaultNodesForGraph(*SigGraph);
K2Schema->CreateFunctionGraphTerminators(*SigGraph, static_cast<UClass*>(nullptr));
K2Schema->AddExtraFunctionFlags(SigGraph, FUNC_BlueprintCallable | FUNC_BlueprintEvent | FUNC_Public);
K2Schema->MarkFunctionEntryAsEditable(SigGraph, true);
BP->DelegateSignatureGraphs.Add(SigGraph);
// Add parameters if provided
int32 ParamCount = 0;
if (Parameters.Array.Num() > 0)
{
UK2Node_EditablePinBase* EntryNode = nullptr;
for (UK2Node_FunctionEntry* FE : WingUtils::AllNodes<UK2Node_FunctionEntry>(SigGraph))
{
EntryNode = FE;
break;
}
if (!EntryNode)
{
WingUtils::SaveBlueprintPackage(BP);
UWingServer::Print(TEXT("Error: Event dispatcher created but entry node not found — parameters could not be added.\n"));
return;
}
for (const TSharedPtr<FJsonValue>& ParamVal : Parameters.Array)
{
FDispatcherParamEntry Entry;
if (!WingJson::PopulateFromJson(FDispatcherParamEntry::StaticStruct(), &Entry, ParamVal)) return;
if (Entry.Name.IsEmpty() || Entry.Type.IsEmpty()) continue;
FEdGraphPinType PinType;
if (!UWingTypes::TextToType(Entry.Type, PinType))
return;
EntryNode->CreateUserDefinedPin(FName(*Entry.Name), PinType, EGPD_Output);
ParamCount++;
}
}
WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Created event dispatcher '%s'"), *DispatcherName);
if (ParamCount > 0)
UWingServer::Printf(TEXT(" with %d parameter(s)"), ParamCount);
UWingServer::Print(TEXT(".\n"));
}
};

View File

@@ -0,0 +1,105 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingServer.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "UObject/UObjectIterator.h"
#include "Blueprint_AddInterface.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_AddInterface : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Native UInterface class name or Blueprint Interface package path"))
FString InterfaceName;
virtual FString GetDescription() const override
{
return TEXT("Add a Blueprint Interface implementation to a Blueprint. "
"Creates stub function graphs for each interface function.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Resolve the interface class
UClass* InterfaceClass = FindInterfaceClass(InterfaceName);
if (!InterfaceClass) return;
// Check for duplicates
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (IfaceDesc.Interface == InterfaceClass)
{
UWingServer::Printf(TEXT("ERROR: Interface '%s' is already implemented by this Blueprint.\n"),
*WingUtils::FormatName(InterfaceClass));
return;
}
}
FTopLevelAssetPath InterfacePath = InterfaceClass->GetClassPathName();
bool bAdded = FBlueprintEditorUtils::ImplementNewInterface(BP, InterfacePath);
if (!bAdded)
{
UWingServer::Printf(TEXT("ERROR: ImplementNewInterface failed for '%s'.\n"),
*WingUtils::FormatName(InterfaceClass));
return;
}
// Collect stub function graph names from the newly added interface entry
UWingServer::Printf(TEXT("Added interface %s\n"), *WingUtils::FormatName(InterfaceClass));
UWingServer::Printf(TEXT("Function stubs:\n"));
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (IfaceDesc.Interface != InterfaceClass) continue;
for (const UEdGraph* Graph : IfaceDesc.Graphs)
{
if (Graph)
UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(Graph));
}
break;
}
}
private:
// Resolve an interface name to a UClass. Tries loaded UInterface classes
// first (for native interfaces), then falls back to loading a Blueprint
// Interface asset by package path.
static UClass* FindInterfaceClass(const FString& Name)
{
// Strategy 1: Search loaded UInterface classes by name
for (TObjectIterator<UClass> It; It; ++It)
{
if (!It->IsChildOf(UInterface::StaticClass())) continue;
if (WingUtils::Identifies(Name, *It))
return *It;
}
// Strategy 2: Try loading as a Blueprint Interface asset by package path
WingFetcher F;
UBlueprint* IfaceBP = F.Asset(Name).Cast<UBlueprint>();
if (IfaceBP && IfaceBP->GeneratedClass && IfaceBP->GeneratedClass->IsChildOf(UInterface::StaticClass()))
return IfaceBP->GeneratedClass;
UWingServer::Printf(TEXT("ERROR: Interface '%s' not found. Provide a native UInterface class name or Blueprint Interface package path.\n"),
*Name);
return nullptr;
}
};

View File

@@ -0,0 +1,54 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Blueprint_Compile.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_Compile : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Path of the blueprint."))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("Compile a blueprint. ");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint *BP = F.Walk(Blueprint).Cast<UBlueprint>();
EBlueprintCompileOptions CompileOpts =
EBlueprintCompileOptions::SkipSave |
EBlueprintCompileOptions::SkipGarbageCollection |
EBlueprintCompileOptions::SkipFiBSearchMetaUpdate;
FKismetEditorUtilities::CompileBlueprint(BP, CompileOpts, nullptr);
// Collect compiler messages from nodes
for (UEdGraphNode* Node : WingUtils::AllNodes(BP))
{
if (!Node->bHasCompilerMessage) continue;
UWingServer::Printf(TEXT("%s %s: %s\n"),
*WingUtils::FormatName(Node->GetGraph()),
*WingUtils::FormatName(Node),
*Node->ErrorMsg);
}
UWingServer::Printf(TEXT("Compilation Done.\n"));
}
};

View File

@@ -0,0 +1,242 @@
#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

@@ -0,0 +1,107 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "Animation/AnimBlueprint.h"
#include "Animation/Skeleton.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Blueprint_Dump.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_Dump : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("Dump a Blueprint's structure: variables, interfaces, components, "
"and graph names. Does not include graph contents (use DumpGraphs for that).");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Header
UWingServer::Printf(TEXT("Blueprint: %s\n"), *WingUtils::FormatName(BP));
UWingServer::Printf(TEXT("Parent: %s\n"), BP->ParentClass ? *WingUtils::FormatName(BP->ParentClass) : TEXT("None"));
UWingServer::Printf(TEXT("Type: %s\n"), *WingUtils::EnumToString(BP->BlueprintType));
// Animation Blueprint
if (UAnimBlueprint* AnimBP = Cast<UAnimBlueprint>(BP))
{
if (AnimBP->TargetSkeleton)
UWingServer::Printf(TEXT("TargetSkeleton: %s\n"), *AnimBP->TargetSkeleton->GetPathName());
}
// Interfaces
for (const FBPInterfaceDescription& I : BP->ImplementedInterfaces)
{
if (I.Interface)
UWingServer::Printf(TEXT("Interface: %s\n"), *WingUtils::FormatName(I.Interface));
}
// Variables
if (!BP->NewVariables.IsEmpty())
{
UWingServer::Print(TEXT("\nVariables:\n"));
for (const FBPVariableDescription& V : BP->NewVariables)
{
UWingServer::Printf(TEXT(" %s %s"),
*UWingTypes::TypeToText(V.VarType),
*WingUtils::FormatName(V));
if (!V.DefaultValue.IsEmpty())
UWingServer::Printf(TEXT(" = %s"), *V.DefaultValue);
if (!V.Category.IsEmpty() && V.Category.ToString() != TEXT("Default"))
UWingServer::Printf(TEXT(" [%s]"), *V.Category.ToString());
UWingServer::Print(TEXT("\n"));
}
}
// Components
if (USimpleConstructionScript* SCS = BP->SimpleConstructionScript)
{
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
if (!AllNodes.IsEmpty())
{
UWingServer::Print(TEXT("\nComponents:\n"));
for (USCS_Node* Node : AllNodes)
{
if (!Node || !Node->ComponentTemplate) continue;
UWingServer::Printf(TEXT(" %s (%s)"),
*WingUtils::FormatName(Node->ComponentTemplate),
*WingUtils::FormatName(Node->ComponentClass));
if (Node->ParentComponentOrVariableName != NAME_None)
UWingServer::Printf(TEXT(" parent=%s"), *Node->ParentComponentOrVariableName.ToString());
UWingServer::Print(TEXT("\n"));
}
}
}
// Graph names (without contents)
TArray<UEdGraph*> Graphs = WingUtils::AllGraphs(BP);
if (!Graphs.IsEmpty())
{
UWingServer::Print(TEXT("\nGraphs:\n"));
for (UEdGraph* Graph : Graphs)
UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(Graph));
}
}
};

View File

@@ -0,0 +1,96 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Blueprint_ListComponents.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_ListComponents : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Tangibles/TAN_Tree"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("List all components in a Blueprint's SimpleConstructionScript, "
"showing hierarchy and component classes.");
}
virtual void Handle() override
{
WingFetcher F;
F.Walk(Blueprint);
if (!F.Ok()) return;
UBlueprint* BP = F.Cast<UBlueprint>();
if (!BP) return;
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
UWingServer::Print(TEXT("ERROR: Not an Actor Blueprint (no SimpleConstructionScript)\n"));
return;
}
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
if (AllNodes.Num() == 0)
{
UWingServer::Print(TEXT("No components.\n"));
return;
}
UWingServer::Print(TEXT("WARNING: This only lists components added in this blueprint's SCS. "
"It does not include inherited components from C++ parent classes "
"(available via the CDO's OwnedComponents) or from parent blueprint SCS nodes.\n"));
// Emit components as a tree, starting from root nodes
for (USCS_Node* Root : RootNodes)
{
if (!Root) continue;
EmitNode(Root, 0, Root == RootNodes[0]);
}
}
private:
void EmitNode(USCS_Node* Node, int32 Depth, bool bIsSceneRoot)
{
// Indent to show hierarchy
for (int32 i = 0; i < Depth; i++)
UWingServer::Print(TEXT(" "));
FString ClassName = Node->ComponentClass
? WingUtils::FormatName(Node->ComponentClass)
: TEXT("None");
UWingServer::Printf(TEXT("%s %s"),
*ClassName,
*WingUtils::FormatName(Node->ComponentTemplate));
if (bIsSceneRoot && Depth == 0)
UWingServer::Print(TEXT(" [SceneRoot]"));
UWingServer::Print(TEXT("\n"));
for (USCS_Node* Child : Node->GetChildNodes())
{
if (!Child) continue;
EmitNode(Child, Depth + 1, false);
}
}
};

View File

@@ -0,0 +1,76 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Blueprint_ListEventDispatchers.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_ListEventDispatchers : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/MyBlueprint"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("List all event dispatchers on a Blueprint, including their parameter signatures.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
TSet<FName> DelegateNameSet;
FBlueprintEditorUtils::GetDelegateNameList(BP, DelegateNameSet);
for (const FName& DelegateName : DelegateNameSet)
{
UWingServer::Printf(TEXT("%s("), *DelegateName.ToString());
UEdGraph* SigGraph = FBlueprintEditorUtils::GetDelegateSignatureGraphByName(BP, DelegateName);
if (SigGraph)
{
bool bFirst = true;
for (UK2Node_FunctionEntry* FE : WingUtils::AllNodes<UK2Node_FunctionEntry>(SigGraph))
{
for (const TSharedPtr<FUserPinInfo>& PinInfo : FE->UserDefinedPins)
{
if (!PinInfo.IsValid()) continue;
if (!bFirst) UWingServer::Print(TEXT(", "));
bFirst = false;
UWingServer::Printf(TEXT("%s %s"),
*UWingTypes::TypeToText(PinInfo->PinType),
*PinInfo->PinName.ToString());
}
break; // only need the first entry node
}
}
UWingServer::Print(TEXT(")\n"));
}
if (DelegateNameSet.Num() == 0)
{
UWingServer::Print(TEXT("No event dispatchers found.\n"));
}
}
};

View File

@@ -0,0 +1,58 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "Blueprint_ListInterfaces.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_ListInterfaces : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a blueprint, e.g. /Game/Foo/MyBlueprint"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("List all Blueprint Interfaces implemented by a Blueprint, "
"including their function graphs.");
}
virtual void Handle() override
{
WingFetcher F;
F.Walk(Blueprint);
if (!F.Ok()) return;
UBlueprint* BP = F.Cast<UBlueprint>();
if (!BP) return;
bool bAny = false;
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (!IfaceDesc.Interface) continue;
bAny = true;
UWingServer::Printf(TEXT("Interface: %s\n"), *WingUtils::FormatName(IfaceDesc.Interface));
for (const UEdGraph* Graph : IfaceDesc.Graphs)
{
if (!Graph) continue;
UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(Graph));
}
}
if (!bAny)
{
UWingServer::Print(TEXT("No interfaces implemented.\n"));
}
}
};

View File

@@ -0,0 +1,87 @@
#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 "Kismet2/BlueprintEditorUtils.h"
#include "Blueprint_RefreshAllNodes.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_RefreshAllNodes : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint package path"))
FString Blueprint;
virtual FString GetDescription() const override
{
return TEXT("Refresh all nodes in a Blueprint, removing orphaned pins. "
"Reports compiler warnings and errors.");
}
virtual void Handle() override
{
// Load Blueprint
WingFetcher F;
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
if (!BP) return;
int32 GraphCount = WingUtils::AllGraphs(BP).Num();
int32 NodeCount = WingUtils::AllNodes(BP).Num();
// Refresh all nodes
FBlueprintEditorUtils::RefreshAllNodes(BP);
// Remove orphaned pins from all nodes
int32 OrphanedPinsRemoved = 0;
for (UEdGraphNode* Node : WingUtils::AllNodes(BP))
{
for (int32 i = Node->Pins.Num() - 1; i >= 0; --i)
{
UEdGraphPin* Pin = Node->Pins[i];
if (Pin && Pin->bOrphanedPin)
{
Pin->BreakAllPinLinks();
Node->Pins.RemoveAt(i);
OrphanedPinsRemoved++;
}
}
}
// Summary
UWingServer::Printf(TEXT("Refreshed %s: %d graphs, %d nodes"), *WingUtils::FormatName(BP), GraphCount, NodeCount);
if (OrphanedPinsRemoved > 0)
{
UWingServer::Printf(TEXT(", %d orphaned pins removed"), OrphanedPinsRemoved);
}
UWingServer::Print(TEXT("\n"));
// Collect compiler warnings and errors
if (BP->Status == BS_Error)
{
UWingServer::Print(TEXT("ERROR: Blueprint has compiler errors after refresh\n"));
}
for (UEdGraphNode* Node : WingUtils::AllNodes(BP))
{
if (!Node->bHasCompilerMessage) continue;
const TCHAR* Prefix = (Node->ErrorType == EMessageSeverity::Error) ? TEXT("ERROR") : TEXT("WARNING");
UWingServer::Printf(TEXT("%s: [%s] %s: %s\n"),
Prefix, *WingUtils::FormatName(Node->GetGraph()),
*WingUtils::FormatName(Node), *Node->ErrorMsg);
}
}
};

View File

@@ -0,0 +1,95 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Blueprint_RemoveComponent.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_RemoveComponent : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Component to remove"))
FString Component;
virtual FString GetDescription() const override
{
return TEXT("Remove a component from a Blueprint's SimpleConstructionScript.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
UWingServer::Print(TEXT("ERROR: Not an Actor Blueprint (no SimpleConstructionScript).\n"));
return;
}
// Find the node to remove using Identifies for consistent name matching
USCS_Node* NodeToRemove = nullptr;
const TArray<USCS_Node*>& AllNodes = SCS->GetAllNodes();
for (USCS_Node* Node : AllNodes)
{
if (Node && Node->ComponentTemplate &&
WingUtils::Identifies(Component, Node->ComponentTemplate))
{
NodeToRemove = Node;
break;
}
}
if (!NodeToRemove)
{
UWingServer::Printf(TEXT("ERROR: Component '%s' not found.\nAvailable components:\n"),
*Component);
for (USCS_Node* Node : AllNodes)
{
if (Node && Node->ComponentTemplate)
UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(Node->ComponentTemplate));
}
return;
}
// Prevent removing the root scene component if it has children
const TArray<USCS_Node*>& RootNodes = SCS->GetRootNodes();
if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0)
{
UWingServer::Printf(TEXT("ERROR: Cannot remove '%s' — it is a root component with %d child(ren). "
"Remove or re-parent the children first.\n"),
*WingUtils::FormatName(NodeToRemove->ComponentTemplate),
NodeToRemove->GetChildNodes().Num());
return;
}
FString RemovedName = WingUtils::FormatName(NodeToRemove->ComponentTemplate);
// Remove the node (promotes children to parent if it has any — but we've guarded root above)
SCS->RemoveNodeAndPromoteChildren(NodeToRemove);
bool bSaved = WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Removed component %s.%s\n"),
*RemovedName,
bSaved ? TEXT("") : TEXT(" WARNING: save failed."));
}
};

View File

@@ -0,0 +1,74 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingServer.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Blueprint_RemoveInterface.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_RemoveInterface : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Interface name to remove"))
FString InterfaceName;
UPROPERTY(meta=(Optional, Description="If true, keep the function graphs as regular functions"))
bool PreserveFunctions = false;
virtual FString GetDescription() const override
{
return TEXT("Remove a Blueprint Interface implementation from a Blueprint. "
"Optionally preserve the function graphs as regular functions.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Find the interface by name
UClass* FoundInterface = nullptr;
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (!IfaceDesc.Interface) continue;
if (WingUtils::Identifies(InterfaceName, IfaceDesc.Interface))
{
FoundInterface = IfaceDesc.Interface;
break;
}
}
if (!FoundInterface)
{
UWingServer::Printf(TEXT("ERROR: Interface '%s' not found. Implemented interfaces:\n"), *InterfaceName);
for (const FBPInterfaceDescription& IfaceDesc : BP->ImplementedInterfaces)
{
if (IfaceDesc.Interface)
UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(IfaceDesc.Interface));
}
return;
}
FTopLevelAssetPath InterfacePath = FoundInterface->GetClassPathName();
FBlueprintEditorUtils::RemoveInterface(BP, InterfacePath, PreserveFunctions);
UWingServer::Printf(TEXT("Removed interface %s\n"), *WingUtils::FormatName(FoundInterface));
if (PreserveFunctions)
UWingServer::Print(TEXT("Function graphs preserved as regular functions.\n"));
}
};

View File

@@ -0,0 +1,73 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingServer.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Blueprint_Reparent.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_Reparent : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint package path"))
FString Blueprint;
UPROPERTY(meta=(Description="New parent class: C++ class name or Blueprint package path"))
FString NewParentClass;
virtual FString GetDescription() const override
{
return TEXT("Change a Blueprint's parent class. Accepts C++ class names or Blueprint package paths.");
}
virtual void Handle() override
{
// Load Blueprint
WingFetcher F;
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
if (!BP) return;
FString OldParentName = BP->ParentClass ? WingUtils::FormatName(BP->ParentClass) : TEXT("None");
// Find the new parent class: try C++ classes first, then Blueprint package path
UClass* NewParentClassObj = WingUtils::FindClassByName(NewParentClass);
if (!NewParentClassObj)
{
WingFetcher F2;
UBlueprint* ParentBP = F2.Asset(NewParentClass).Cast<UBlueprint>();
if (ParentBP && ParentBP->GeneratedClass)
NewParentClassObj = ParentBP->GeneratedClass;
}
if (!NewParentClassObj)
{
UWingServer::Printf(TEXT("ERROR: Could not find class '%s'. Provide a C++ class name or Blueprint package path.\n"),
*NewParentClass);
return;
}
// Perform reparent
BP->ParentClass = NewParentClassObj;
FBlueprintEditorUtils::RefreshAllNodes(BP);
FKismetEditorUtilities::CompileBlueprint(BP);
bool bSaved = WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Reparented %s: %s -> %s\n"),
*WingUtils::FormatName(BP), *OldParentName, *WingUtils::FormatName(NewParentClassObj));
if (!bSaved)
UWingServer::Print(TEXT("Warning: save failed\n"));
}
};

View File

@@ -0,0 +1,112 @@
#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

@@ -0,0 +1,80 @@
#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 = WingUtils::FindClassByName(ClassName);
if (!FoundClass)
{
UWingServer::Printf(TEXT("ERROR: Class '%s' not found\n"), *ClassName);
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

@@ -0,0 +1,71 @@
#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]));
}
bool bSaved = WingUtils::SaveGenericPackage(NewEnum);
UWingServer::Printf(TEXT("Created %s with %d values\n"), *NewEnum->GetPathName(), EnumValues.Num());
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -0,0 +1,53 @@
#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;
bool bSaved = WingUtils::SaveGenericPackage(MF);
UWingServer::Printf(TEXT("Created %s\n"), *MF->GetPathName());
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -0,0 +1,117 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
WingUtils::SaveBlueprintPackage(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

@@ -0,0 +1,99 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
WingUtils::SaveBlueprintPackage(AnimBP);
UWingServer::Printf(TEXT("Created transition %s -> %s: %s\n"),
*FromState, *ToState, *WingUtils::FormatName(TransNode));
}
};

View File

@@ -0,0 +1,82 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(BP);
WingUtils::SaveBlueprintPackage(BP);
UWingServer::Printf(TEXT("Removed state %s and %d transition(s).\n"),
*WingUtils::FormatName(StateNode), RemovedTransitions);
}
};

View File

@@ -0,0 +1,109 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
WingUtils::SaveBlueprintPackage(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

@@ -0,0 +1,220 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
bool bSaved = WingUtils::SaveBlueprintPackage(AnimBP);
UWingServer::Printf(TEXT("BlendSpacePlayer %s placed in state %s\n"),
*WingUtils::FormatName(BSNode), *StateName);
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Failed to save package\n"));
}
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

@@ -0,0 +1,92 @@
#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 and save
FKismetEditorUtilities::CompileBlueprint(AnimBP);
WingUtils::SaveBlueprintPackage(AnimBP);
UWingServer::Printf(TEXT("Updated transition %s -> %s: %s\n"),
*FromState, *ToState, *WingUtils::FormatName(TransNode));
}
};

View File

@@ -0,0 +1,99 @@
#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++;
}
bool bSaved = WingUtils::SaveGenericPackage(NewStruct);
UWingServer::Printf(TEXT("Created %s\n"), *NewStruct->GetPathName());
if (PropsAdded > 0)
UWingServer::Printf(TEXT("Properties added: %d\n"), PropsAdded);
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "Misc/Paths.h"
#include "Misc/PackageName.h"
#include "HAL/FileManager.h"
#include "Asset_Backup.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Asset_Backup : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset to back up"))
FString Asset;
virtual FString GetDescription() const override
{
return TEXT("Copy an asset's .uasset file to a .uasset.bak backup.");
}
virtual void Handle() override
{
FString Filename = FPaths::ConvertRelativePathToFull(
FPackageName::LongPackageNameToFilename(Asset, FPackageName::GetAssetPackageExtension()));
if (!IFileManager::Get().FileExists(*Filename))
{
UWingServer::Printf(TEXT("ERROR: Asset file not found: %s\n"), *Filename);
return;
}
FString BackupFilename = Filename + TEXT(".bak");
uint32 CopyResult = IFileManager::Get().Copy(*BackupFilename, *Filename, true);
if (CopyResult != COPY_OK)
{
UWingServer::Printf(TEXT("ERROR: Failed to copy %s to %s\n"), *Filename, *BackupFilename);
return;
}
UWingServer::Printf(TEXT("Backed up to %s\n"), *BackupFilename);
}
};

View File

@@ -0,0 +1,127 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "Misc/PackageName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "HAL/FileManager.h"
#include "UObject/LinkerLoad.h"
#include "UObject/Package.h"
#include "Asset_Delete.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Asset_Delete : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset to delete"))
FString Asset;
UPROPERTY(meta=(Optional, Description="If true, skip reference check and force delete"))
bool Force = false;
virtual FString GetDescription() const override
{
return TEXT("Delete a .uasset after verifying no references. "
"Use force=true to skip the reference check.");
}
virtual void Handle() override
{
// Verify the asset file exists on disk
FString PackageFilename = FPackageName::LongPackageNameToFilename(
Asset, FPackageName::GetAssetPackageExtension());
PackageFilename = FPaths::ConvertRelativePathToFull(PackageFilename);
if (!IFileManager::Get().FileExists(*PackageFilename))
{
UWingServer::Printf(TEXT("ERROR: Asset file not found on disk: %s\n"), *PackageFilename);
return;
}
// Check references
IAssetRegistry& Registry = *IAssetRegistry::Get();
TArray<FName> Referencers;
Registry.GetReferencers(FName(*Asset), Referencers);
// Filter out self-references
Referencers.RemoveAll([this](const FName& Ref) {
return Ref.ToString() == Asset;
});
if (Referencers.Num() > 0 && !Force)
{
UWingServer::Printf(TEXT("ERROR: Asset is still referenced by %d package(s):\n"), Referencers.Num());
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
UPackage* RefPackage = FindPackage(nullptr, *RefStr);
UWingServer::Printf(TEXT(" %s%s\n"), *RefStr,
RefPackage ? TEXT(" (loaded)") : TEXT(" (on-disk only)"));
}
UWingServer::Print(TEXT("Use force=true to skip the reference check.\n"));
return;
}
// Force delete: unload the package from memory first
if (Force && Referencers.Num() > 0)
{
UWingServer::Printf(TEXT("WARNING: Force-deleting despite %d referencer(s).\n"), Referencers.Num());
}
// Mark the package, and all the objects in it, as NOT
// GC Roots. Also, make them undiscoverable.
UPackage* Package = FindPackage(nullptr, *Asset);
if (Package)
{
// Collect all objects in this package
TArray<UObject*> ObjectsInPackage;
GetObjectsWithPackage(Package, ObjectsInPackage);
// Clear flags and remove from root to allow GC
for (UObject* Obj : ObjectsInPackage)
{
if (Obj)
{
Obj->ClearFlags(RF_Standalone | RF_Public);
Obj->RemoveFromRoot();
}
}
Package->ClearFlags(RF_Standalone | RF_Public);
Package->RemoveFromRoot();
}
// The loader that loaded the package might still
// have a file lock on it. Unlock the file.
ResetLoaders(Package);
// Delete the file on disk
bool bDeleted = IFileManager::Get().Delete(*PackageFilename, false, true);
if (!bDeleted)
{
UWingServer::Printf(TEXT("ERROR: Failed to delete file from disk: %s\n"), *PackageFilename);
return;
}
// Trigger an asset registry rescan so it notices the deletion
FString PackageDir;
int32 LastSlash;
if (Asset.FindLastChar(TEXT('/'), LastSlash))
{
PackageDir = Asset.Left(LastSlash);
Registry.ScanPathsSynchronous({PackageDir}, true);
}
UWingServer::Printf(TEXT("Deleted %s\n"), *Asset);
}
};

View File

@@ -0,0 +1,69 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Asset_FindReferences.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Asset_FindReferences : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset to find references for"))
FString Asset;
virtual FString GetDescription() const override
{
return TEXT("Find all assets that reference a given asset.");
}
virtual void Handle() override
{
IAssetRegistry& Registry = *IAssetRegistry::Get();
// Verify the asset exists
FAssetData AssetData = Registry.GetAssetByObjectPath(FSoftObjectPath(Asset));
if (!AssetData.IsValid())
{
UWingServer::Printf(TEXT("ERROR: Asset not found: %s\n"), *Asset);
return;
}
TArray<FName> Referencers;
Registry.GetReferencers(FName(*Asset), Referencers);
if (Referencers.Num() == 0)
{
UWingServer::Print(TEXT("No referencers found.\n"));
return;
}
// Classify referencers by looking up their asset class
for (const FName& Ref : Referencers)
{
FString RefStr = Ref.ToString();
TArray<FAssetData> RefAssets;
Registry.GetAssetsByPackageName(Ref, RefAssets);
if (RefAssets.Num() > 0)
{
UWingServer::Printf(TEXT("%s %s\n"),
*WingUtils::FormatName(RefAssets[0].GetClass()),
*RefStr);
}
else
{
UWingServer::Printf(TEXT("Unknown %s\n"), *RefStr);
}
}
}
};

View File

@@ -0,0 +1,71 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Asset_Rename.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Asset_Rename : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset to rename"))
FString Asset;
UPROPERTY(meta=(Description="New package path or just a new name"))
FString NewPath;
virtual FString GetDescription() const override
{
return TEXT("Rename or move an asset with reference fixup.");
}
virtual void Handle() override
{
// Load the asset
WingFetcher F;
UObject* AssetObj = F.Asset(Asset).GetObj();
if (!AssetObj) return;
// Parse new path into package path and asset name
FString NewPackagePath = FPackageName::GetLongPackagePath(NewPath);
FString NewAssetName = FPackageName::GetShortName(NewPath);
if (NewPackagePath.IsEmpty())
{
// No slash — just a new name, keep the same directory
NewPackagePath = FPackageName::GetLongPackagePath(Asset);
NewAssetName = NewPath;
if (NewPackagePath.IsEmpty())
{
UWingServer::Printf(TEXT("ERROR: Cannot determine directory from Asset '%s'\n"), *Asset);
return;
}
}
// Perform the rename with reference fixup
FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools");
IAssetTools& AssetTools = AssetToolsModule.Get();
TArray<FAssetRenameData> RenameData;
RenameData.Add(FAssetRenameData(AssetObj, NewPackagePath, NewAssetName));
if (!AssetTools.RenameAssets(RenameData))
{
UWingServer::Print(TEXT("ERROR: Rename failed. The target path may be invalid or a conflicting asset may exist.\n"));
return;
}
UWingServer::Printf(TEXT("Renamed to %s/%s\n"), *NewPackagePath, *NewAssetName);
}
};

View File

@@ -0,0 +1,75 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "Misc/PackageName.h"
#include "FileHelpers.h"
#include "HAL/FileManager.h"
#include "UObject/LinkerLoad.h"
#include "Asset_Restore.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Asset_Restore : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset to restore"))
FString Asset;
virtual FString GetDescription() const override
{
return TEXT("Restore a .uasset file from its .uasset.bak backup, reloading it in the editor.");
}
virtual void Handle() override
{
FString Filename = FPaths::ConvertRelativePathToFull(
FPackageName::LongPackageNameToFilename(Asset, FPackageName::GetAssetPackageExtension()));
FString BackupFilename = Filename + TEXT(".bak");
if (!IFileManager::Get().FileExists(*BackupFilename))
{
UWingServer::Printf(TEXT("ERROR: Backup file not found: %s\n"), *BackupFilename);
return;
}
// Release file handles if the package is loaded
UPackage* Package = FindPackage(nullptr, *Asset);
if (Package)
{
ResetLoaders(Package);
}
// Copy backup over the original
uint32 CopyResult = IFileManager::Get().Copy(*Filename, *BackupFilename, true);
if (CopyResult != COPY_OK)
{
UWingServer::Printf(TEXT("ERROR: Failed to copy backup over %s\n"), *Asset);
return;
}
// Reload the package if it was loaded
if (Package)
{
bool bReloaded = false;
FText ErrorMessage;
UEditorLoadingAndSavingUtils::ReloadPackages({Package}, bReloaded, ErrorMessage, EReloadPackagesInteractionMode::AssumePositive);
if (!bReloaded)
{
UWingServer::Printf(TEXT("WARNING: Restored %s but reload failed: %s\n"),
*Asset, *ErrorMessage.ToString());
return;
}
}
UWingServer::Printf(TEXT("Restored %s from backup\n"), *Asset);
}
};

View File

@@ -0,0 +1,96 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "Asset_Search.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Asset_Search : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring to match against asset package paths"))
FString Query;
UPROPERTY(meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type;
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50)"))
int32 Limit = 50;
virtual FString GetDescription() const override
{
return TEXT("Search for assets by name and/or type. At least one of Query or Type must be specified.");
}
virtual void Handle() override
{
if (Query.IsEmpty() && Type.IsEmpty())
{
UWingServer::Print(TEXT("ERROR: At least one of Query or Type must be specified\n"));
return;
}
// Build the asset registry filter
FARFilter Filter;
Filter.bRecursiveClasses = true;
Filter.bRecursivePaths = true;
Filter.PackagePaths.Add(FName(TEXT("/Game")));
if (!Type.IsEmpty())
{
UClass* TypeClass = WingUtils::FindClassByName(Type);
if (!TypeClass)
{
UWingServer::Printf(TEXT("ERROR: Unknown asset type '%s'\n"), *Type);
return;
}
Filter.ClassPaths.Add(TypeClass->GetClassPathName());
}
// Query the asset registry
IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
TArray<FAssetData> Candidates;
AR.GetAssets(Filter, Candidates);
// Filter by query substring and collect results
TArray<FAssetData> Results;
for (const FAssetData& Data : Candidates)
{
if (Results.Num() >= Limit) break;
if (!Query.IsEmpty())
{
if (!Data.AssetName.ToString().Contains(Query, ESearchCase::IgnoreCase) &&
!Data.PackageName.ToString().Contains(Query, ESearchCase::IgnoreCase))
continue;
}
Results.Add(Data);
}
for (const FAssetData& Data : Results)
{
UWingServer::Printf(TEXT("%s %s\n"),
*WingUtils::FormatName(Data.GetClass()),
*Data.PackageName.ToString());
}
if (Results.Num() == 0)
{
UWingServer::Print(TEXT("No assets found.\n"));
}
else if (Results.Num() >= Limit)
{
UWingServer::Printf(TEXT("WARNING: You reached the limit of %d, to raise it, specify the Limit parameter.\n"), Limit);
}
}
};

View File

@@ -0,0 +1,79 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingProperty.h"
#include "WingBlueprintVar.h"
#include "WingUtils.h"
#include "WingTypes.h"
#include "Engine/Blueprint.h"
#include "EdGraphSchema_K2.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintVariable_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintVariable_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the new variable"))
FString Name;
UPROPERTY(meta=(Optional, Description="Variable configuration: VarType, Category, DefaultValue, InstanceEditable, BlueprintReadOnly, ExposeOnSpawn, Private, ExposeToCinematics, etc."))
FWingJsonObject Config;
virtual FString GetDescription() const override
{
return TEXT("Add a new member variable to a Blueprint. Pass Config to set type, category, flags, etc.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
// Check for duplicate variable name
FName VarFName(*Name);
if (FBlueprintEditorUtils::FindNewVariableIndex(BP, VarFName) != INDEX_NONE)
{
UWingServer::Printf(TEXT("ERROR: Variable '%s' already exists in %s\n"), *Name, *WingUtils::FormatName(BP));
return;
}
// Add the variable with a default type
FEdGraphPinType DefaultType;
DefaultType.PinCategory = UEdGraphSchema_K2::PC_Int;
if (!FBlueprintEditorUtils::AddMemberVariable(BP, VarFName, DefaultType))
{
UWingServer::Printf(TEXT("ERROR: Failed to add variable '%s' to %s\n"), *Name, *WingUtils::FormatName(BP));
return;
}
// Find the newly created variable description
FBlueprintVar Editor(BP, Name);
if (Editor.NotFound()) return;
// Apply config if provided
if (Config.Json && Config.Json->Values.Num() > 0)
{
if (!Editor.ApplyJson(Config.Json.Get()))
return;
}
UWingServer::Printf(TEXT("Created variable %s (%s) in %s\n"),
*Name, *UWingTypes::TypeToText(Editor.Desc->VarType), *WingUtils::FormatName(BP));
}
};

View File

@@ -0,0 +1,49 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingBlueprintVar.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintVariable_Delete.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintVariable_Delete : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the variable to delete"))
FString Variable;
virtual FString GetDescription() const override
{
return TEXT("Remove a member variable from a Blueprint.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
FBlueprintVar Editor(BP, Variable);
if (Editor.NotFound()) return;
FBlueprintEditorUtils::RemoveMemberVariable(BP, Editor.Desc->VarName);
UWingServer::Printf(TEXT("Removed variable %s from %s\n"),
*Variable, *WingUtils::FormatName(BP));
}
};

View File

@@ -0,0 +1,47 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingBlueprintVar.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintVariable_Dump.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintVariable_Dump : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the variable to inspect"))
FString Variable;
virtual FString GetDescription() const override
{
return TEXT("Show all editable properties of a Blueprint variable.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
FBlueprintVar Editor(BP, Variable);
if (Editor.NotFound()) return;
UWingServer::Printf(TEXT("Variable %s in %s:\n"), *Variable, *WingUtils::FormatName(BP));
Editor.Dump();
}
};

View File

@@ -0,0 +1,62 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingProperty.h"
#include "WingBlueprintVar.h"
#include "WingUtils.h"
#include "WingTypes.h"
#include "Engine/Blueprint.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "BlueprintVariable_Modify.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_BlueprintVariable_Modify : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Blueprint name or package path"))
FString Blueprint;
UPROPERTY(meta=(Description="Name of the variable to modify"))
FString Variable;
UPROPERTY(meta=(Description="Properties to change: VarType, Category, DefaultValue, InstanceEditable, BlueprintReadOnly, ExposeOnSpawn, Private, ExposeToCinematics, etc."))
FWingJsonObject Properties;
virtual FString GetDescription() const override
{
return TEXT("Modify properties of an existing Blueprint variable.");
}
virtual void Handle() override
{
WingFetcher F;
UBlueprint* BP = F.Walk(Blueprint).Cast<UBlueprint>();
if (!BP) return;
FBlueprintVar Editor(BP, Variable);
if (Editor.NotFound()) return;
if (!Properties.Json || Properties.Json->Values.Num() == 0)
{
UWingServer::Print(TEXT("ERROR: No properties specified\n"));
return;
}
if (!Editor.ApplyJson(Properties.Json.Get()))
return;
UWingServer::Printf(TEXT("Modified variable %s (%s) in %s\n"),
*Variable, *UWingTypes::TypeToText(Editor.Desc->VarType), *WingUtils::FormatName(BP));
}
};

View File

@@ -0,0 +1,94 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingTypes.h"
#include "WingPackageMaker.h"
#include "Engine/Blueprint.h"
#include "Kismet/BlueprintFunctionLibrary.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Blueprint_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Blueprint_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new Blueprint"))
FString AssetPath;
UPROPERTY(meta=(Description="Parent class, expressed as a type"))
FString ParentClass;
UPROPERTY(meta=(Optional, Description="Normal, Interface, FunctionLibrary, or MacroLibrary"))
TEnumAsByte<EBlueprintType> BlueprintType = BPTYPE_Normal;
virtual FString GetDescription() const override
{
return TEXT("Create a new Blueprint asset with a specified parent class and type.");
}
virtual void Handle() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Resolve parent class based on blueprint type
UClass* ParentClassObj = nullptr;
switch (BlueprintType)
{
case BPTYPE_Normal:
ParentClassObj = UWingTypes::TextToOneObjectType(ParentClass);
if (!ParentClassObj) return;
break;
case BPTYPE_MacroLibrary:
ParentClassObj = UWingTypes::TextToOneObjectType(ParentClass);
if (!ParentClassObj) return;
break;
case BPTYPE_Interface:
ParentClassObj = UInterface::StaticClass();
break;
case BPTYPE_FunctionLibrary:
ParentClassObj = UBlueprintFunctionLibrary::StaticClass();
break;
default:
UWingServer::Print(TEXT("ERROR: BlueprintType must be Normal, Interface, FunctionLibrary, or MacroLibrary\n"));
return;
}
// Create the package and Blueprint
if (!Maker.Make()) return;
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
ParentClassObj,
Maker.Package(),
FName(*Maker.Name()),
BlueprintType,
UBlueprint::StaticClass(),
UBlueprintGeneratedClass::StaticClass()
);
if (!NewBP)
{
UWingServer::Print(TEXT("ERROR: FKismetEditorUtilities::CreateBlueprint returned null\n"));
return;
}
// Compile and save
FKismetEditorUtilities::CompileBlueprint(NewBP);
bool bSaved = WingUtils::SaveBlueprintPackage(NewBP);
// Report result
UWingServer::Printf(TEXT("Created: %s\n"), *WingUtils::FormatName(NewBP));
if (!bSaved) UWingServer::Print(TEXT("Warning: save failed\n"));
}
};

View File

@@ -0,0 +1,51 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "Editor.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Editor_ListOpenAssets.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Editor_ListOpenAssets : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
virtual FString GetDescription() const override
{
return TEXT("List all currently open asset editors, showing which has focus and whether they have unsaved changes.");
}
virtual void Handle() override
{
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Sub)
{
UWingServer::Print(TEXT("Error: AssetEditorSubsystem not available\n"));
return;
}
TArray<UObject*> EditedAssets = Sub->GetAllEditedAssets();
if (EditedAssets.IsEmpty())
{
UWingServer::Print(TEXT("No asset editors are open.\n"));
return;
}
for (UObject* Asset : EditedAssets)
{
bool bDirty = Asset->GetOutermost()->IsDirty();
UWingServer::Printf(TEXT(" %s%s\n"),
bDirty ? TEXT("[unsaved] ") : TEXT(""),
*Asset->GetPathName());
}
}
};

View File

@@ -0,0 +1,48 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "Editor.h"
#include "Subsystems/AssetEditorSubsystem.h"
#include "Editor_OpenAsset.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Editor_OpenAsset : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Asset to open"))
FString Asset;
virtual FString GetDescription() const override
{
return TEXT("Open an asset in its editor and bring it to focus.");
}
virtual void Handle() override
{
WingFetcher F;
UObject* Obj = F.Walk(Asset).Cast<UObject>();
if (!Obj) return;
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Sub)
{
UWingServer::Print(TEXT("Error: AssetEditorSubsystem not available\n"));
return;
}
if (Sub->OpenEditorForAsset(Obj))
UWingServer::Printf(TEXT("Opened editor for %s\n"), *Obj->GetPathName());
else
UWingServer::Printf(TEXT("Error: Could not open editor for %s\n"), *Obj->GetPathName());
}
};

View File

@@ -0,0 +1,63 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingToolMenu.h"
#include "WingServer.h"
#include "ToolMenus.h"
#include "GraphNode_ChooseMenu.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_ChooseMenu : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target node"))
FString Node;
UPROPERTY(meta=(Description="Menu item as shown by GraphNode_ShowMenu"))
FString Item;
virtual FString GetDescription() const override
{
return TEXT("Execute a context menu action on a node or pin. "
"Supports SplitStructPin, AddPin, AddArrayElementPin, etc. "
"Use GraphNode_ShowMenu to see available actions. ");
}
private:
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* NodeObj = F.Walk(Node).Cast<UEdGraphNode>();
if (!NodeObj) return;
FToolMenuContext Context;
TArray<FToolMenuEntry> Entries = WingToolMenu::GetMenuItems(NodeObj, Context);
for (FToolMenuEntry &Entry : Entries)
{
FString LabelText = Entry.Label.Get().ToString();
if (!LabelText.Equals(Item, ESearchCase::IgnoreCase))
continue;
if (WingToolMenu::Execute(Entry, Context))
{
UWingServer::Printf(TEXT("Executed: %s\n"), *LabelText);
}
else
{
UWingServer::Printf(TEXT("ERROR: Action '%s' cannot execute (greyed out)\n"), *LabelText);
}
return;
}
UWingServer::Printf(TEXT("ERROR: Menu item '%s' not found. Use GraphNode_ShowMenu to see available items.\n"), *Item);
}
};

View File

@@ -0,0 +1,102 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphSchema.h"
#include "GraphNode_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSpawnNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString ActionName;
UPROPERTY()
int32 PosX = 0;
UPROPERTY()
int32 PosY = 0;
};
UCLASS()
class UWing_GraphNode_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target graph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {Type, posX, posY} objects. Use GraphNode_SearchTypes to find types."))
FWingJsonArray Nodes;
virtual FString GetDescription() const override
{
return TEXT("Create nodes using the editor's action database. "
"Use GraphNode_SearchTypes to find types.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
int32 TotalCount = Nodes.Array.Num();
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
{
FSpawnNodeEntry Entry;
if (!WingJson::PopulateFromJson(FSpawnNodeEntry::StaticStruct(), &Entry, NodeVal))
continue;
// Find the action by exact full name
TArray<TSharedPtr<FEdGraphSchemaAction>> Matches = WingUtils::SearchGraphActions(TargetGraph, Entry.ActionName, 0, /*ExactMatch=*/true);
if (Matches.Num() == 0)
{
UWingServer::Printf(TEXT("ERROR: No action found matching '%s'. Use GraphNodeSearchTypes to find available actions.\n"),
*Entry.ActionName);
continue;
}
if (Matches.Num() > 1)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous: %d actions match '%s'.\n"),
Matches.Num(), *Entry.ActionName);
continue;
}
// Perform the action
FVector2D Location(Entry.PosX, Entry.PosY);
UEdGraphNode* NewNode = Matches[0]->PerformAction(TargetGraph, nullptr, Location, /*bSelectNewNode=*/false);
if (!NewNode)
{
UWingServer::Printf(TEXT("ERROR: PerformAction returned null for '%s'.\n"), *Entry.ActionName);
continue;
}
if (!NewNode->NodeGuid.IsValid())
NewNode->CreateNewGuid();
UWingServer::Printf(TEXT("Spawned: %s (%s)\n"),
*WingUtils::FormatName(NewNode), *WingUtils::FormatName(NewNode->GetClass()));
SuccessCount++;
}
UWingServer::Printf(TEXT("Spawned %d/%d nodes.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -0,0 +1,68 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingServer.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "Materials/Material.h"
#include "IMaterialEditor.h"
#include "GraphNode_Delete.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_Delete : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Node to delete"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Delete a node from a graph. "
"Cannot delete undeletable nodes (entry points, root nodes, etc).");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
UEdGraph* Graph = FoundNode->GetGraph();
FString NodeTitle = WingUtils::FormatName(FoundNode);
FString GraphName = WingUtils::FormatName(Graph);
if (!FoundNode->CanUserDeleteNode())
{
UWingServer::Printf(TEXT("ERROR: Cannot delete node '%s' in graph '%s' — it is not deletable.\n"),
*NodeTitle, *GraphName);
return;
}
if (Cast<UMaterialGraphNode>(FoundNode))
{
// Use the material editor's DeleteNodes to properly remove
// both the graph node and the underlying material expression.
IMaterialEditor* MatEditor = F.CastEditor<UMaterial, IMaterialEditor>();
if (!MatEditor) return;
MatEditor->DeleteNodes({FoundNode});
}
else
{
FoundNode->BreakAllNodeLinks();
Graph->RemoveNode(FoundNode);
}
UWingServer::Printf(TEXT("Deleted node '%s' from graph '%s'.\n"), *NodeTitle, *GraphName);
}
};

View File

@@ -0,0 +1,40 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingGraphExport.h"
#include "GraphNode_Dump.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_Dump : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target node"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Dump a single node as readable text, including all pins and connections.");
}
private:
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* NodeObj = F.Walk(Node).Cast<UEdGraphNode>();
if (!NodeObj) return;
WingGraphExport Exporter(NodeObj);
UWingServer::Print(*Exporter.GetOutput());
UWingServer::Print(Exporter.GetDetails());
}
};

View File

@@ -0,0 +1,104 @@
#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 "GraphNode_Duplicate.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_Duplicate : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target graph"))
FString Graph;
UPROPERTY(meta=(Description="Array of node names to duplicate (as returned by FormatName)"))
FWingJsonArray Nodes;
UPROPERTY(meta=(Optional, Description="X offset for duplicated nodes"))
int32 OffsetX = 50;
UPROPERTY(meta=(Optional, Description="Y offset for duplicated nodes"))
int32 OffsetY = 50;
virtual FString GetDescription() const override
{
return TEXT("Duplicate one or more nodes in a Blueprint graph. "
"Creates copies offset from the originals with new GUIDs. "
"Connections are not preserved on the duplicates.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
if (Nodes.Array.Num() == 0)
{
UWingServer::Print(TEXT("ERROR: Nodes array is empty\n"));
return;
}
// Find all source nodes by name
TArray<UEdGraphNode*> SourceNodes;
for (const TSharedPtr<FJsonValue>& IdVal : Nodes.Array)
{
FString Name = IdVal->AsString();
UEdGraphNode* Found = nullptr;
for (UEdGraphNode* Node : TargetGraph->Nodes)
{
if (WingUtils::Identifies(Name, Node))
{
Found = Node;
break;
}
}
if (!Found)
{
UWingServer::Printf(TEXT("ERROR: Node '%s' not found in graph\n"), *Name);
continue;
}
SourceNodes.Add(Found);
}
if (SourceNodes.Num() == 0) return;
// Duplicate each node
for (UEdGraphNode* SourceNode : SourceNodes)
{
UEdGraphNode* NewNode = DuplicateObject<UEdGraphNode>(SourceNode, TargetGraph);
if (!NewNode)
{
UWingServer::Printf(TEXT("ERROR: Failed to duplicate %s\n"), *WingUtils::FormatName(SourceNode));
continue;
}
NewNode->CreateNewGuid();
NewNode->NodePosX += OffsetX;
NewNode->NodePosY += OffsetY;
for (UEdGraphPin* Pin : NewNode->Pins)
{
if (Pin)
Pin->LinkedTo.Empty();
}
TargetGraph->AddNode(NewNode, false, false);
UWingServer::Printf(TEXT("Duplicated: %s -> %s\n"), *WingUtils::FormatName(SourceNode), *WingUtils::FormatName(NewNode));
}
}
};

View File

@@ -0,0 +1,40 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphNode.h"
#include "GraphNode_GetComment.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_GetComment : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target node"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Get the comment text and bubble visibility of a node.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
UWingServer::Printf(TEXT("Node: %s\n"), *WingUtils::FormatName(FoundNode));
UWingServer::Printf(TEXT("Comment: %s\n"), *FoundNode->NodeComment);
UWingServer::Printf(TEXT("BubbleVisible: %s\n"), FoundNode->bCommentBubbleVisible ? TEXT("true") : TEXT("false"));
}
};

View File

@@ -0,0 +1,63 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
#include "GraphNode_SearchTypes.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_SearchTypes : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Search query string"))
FString Query;
UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50, max 500)"))
int32 MaxResults = 50;
UPROPERTY(meta=(Description="Target graph (needed for context-sensitive results)"))
FString Graph;
virtual FString GetDescription() const override
{
return TEXT("Search the action database for node types that can be spawned in a graph. "
"Works with any graph type (Blueprint, Material, etc.). "
"Returns full action names for use with GraphNodeCreate.");
}
virtual void Handle() override
{
int32 ClampedMax = FMath::Clamp(MaxResults, 1, 500);
WingFetcher F;
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
TArray<TSharedPtr<FEdGraphSchemaAction>> Actions = WingUtils::SearchGraphActions(TargetGraph, Query, ClampedMax, /*ExactMatch=*/false);
for (const TSharedPtr<FEdGraphSchemaAction>& Action : Actions)
{
UWingServer::Printf(TEXT("%s\n"), *WingUtils::ActionFullName(Action));
}
if (Actions.Num() == 0)
{
UWingServer::Print(TEXT("No matching node types found.\n"));
}
else if (Actions.Num() >= ClampedMax)
{
UWingServer::Printf(TEXT("WARNING: Reached limit of %d results. Refine your query or increase MaxResults.\n"), ClampedMax);
}
}
};

View File

@@ -0,0 +1,44 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingFunctionArgs.h"
#include "GraphNode_SetArgs.generated.h"
UCLASS()
class UWing_GraphNode_SetArgs : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to a graph node (function entry, function result, custom event, or tunnel)"))
FString Node;
UPROPERTY(meta=(Description="Comma-separated args, e.g. 'int x, float y'"))
FString Args;
virtual FString GetDescription() const override
{
return TEXT("Set the user-defined pins (arguments or return values) on a function entry, result, custom event, or tunnel node.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* NodeObj = F.Walk(Node).Cast<UEdGraphNode>();
if (!NodeObj) return;
if (!WingFunctionArgs::HasArgs(NodeObj))
{
UWingServer::Printf(TEXT("ERROR: Node does not support editable args\n"));
return;
}
if (!WingFunctionArgs::SetArgs(NodeObj, Args)) return;
UWingServer::Printf(TEXT("Args set to: %s\n"), *WingFunctionArgs::GetArgs(NodeObj));
}
};

View File

@@ -0,0 +1,46 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphNode.h"
#include "GraphNode_SetComment.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_SetComment : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target node"))
FString Node;
UPROPERTY(meta=(Description="Comment text to set"))
FString Comment;
virtual FString GetDescription() const override
{
return TEXT("Set a node's comment text, and make the comment visible. "
"Setting empty text will cause the comment to vanish.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* FoundNode = F.Walk(Node).Cast<UEdGraphNode>();
if (!FoundNode) return;
FoundNode->NodeComment = Comment;
FoundNode->bCommentBubbleVisible = !Comment.IsEmpty();
FoundNode->bCommentBubblePinned = !Comment.IsEmpty();
UWingServer::Printf(TEXT("Comment set on %s\n"), *WingUtils::FormatName(FoundNode));
}
};

View File

@@ -0,0 +1,140 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "MaterialGraph/MaterialGraphSchema.h"
#include "GraphNode_SetDefaults.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSetNodeDefaultEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
FString Name;
UPROPERTY()
FString Value;
};
UCLASS()
class UWing_GraphNode_SetDefaults : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target graph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {node, name, value} objects"))
FWingJsonArray Pins;
virtual FString GetDescription() const override
{
return TEXT("Set the default value of input pins or material expression properties on nodes.");
}
// -----------------------------------------------------------------------
// K2 graphs: set pin default values.
// -----------------------------------------------------------------------
void HandleK2Entry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj, const UEdGraphSchema_K2* K2Schema)
{
WingFetcher F(GraphObj);
UEdGraphPin* Pin = F.Node(Entry.Node).Pin(Entry.Name).Cast<UEdGraphPin>();
if (!Pin) return;
UEdGraphNode* Node = Pin->GetOwningNode();
if (Pin->Direction != EGPD_Input)
{
UWingServer::Printf(TEXT("error: %s is an output pin\n"), *WingUtils::FormatName(Pin));
return;
}
Pin->Modify();
FString UseDefaultValue;
TObjectPtr<UObject> UseDefaultObject = nullptr;
FText UseDefaultText;
K2Schema->GetPinDefaultValuesFromString(Pin->PinType, Node, Entry.Value, UseDefaultValue, UseDefaultObject, UseDefaultText, false);
FString Error = K2Schema->IsPinDefaultValid(Pin, UseDefaultValue, UseDefaultObject, UseDefaultText);
if (!Error.IsEmpty())
{
UWingServer::Printf(TEXT("error: %s: %s\n"), *WingUtils::FormatName(Pin), *Error);
return;
}
UWingServer::AddTouchedObject(Node);
K2Schema->TrySetDefaultValue(*Pin, Entry.Value);
}
// -----------------------------------------------------------------------
// Material graphs: set material expression properties.
// -----------------------------------------------------------------------
void HandleMaterialEntry(const FSetNodeDefaultEntry& Entry, UEdGraph* GraphObj)
{
WingFetcher F(GraphObj);
UEdGraphNode* Node = F.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) return;
TArray<WingProperty> All = WingProperty::GetAll(Node, CPF_Edit);
WingProperty P = WingProperty::FindOneExactMatch(All, Entry.Name);
if (!P) return;
UWingServer::AddTouchedObject(Node);
if (!P.SetText(Entry.Value))
return;
}
// -----------------------------------------------------------------------
virtual void Handle() override
{
// Fetch the graph once.
WingFetcher GraphFetcher;
UEdGraph* GraphObj = GraphFetcher.Walk(Graph).Cast<UEdGraph>();
if (!GraphObj) return;
const UEdGraphSchema* Schema = GraphObj->GetSchema();
const UEdGraphSchema_K2* K2Schema = Cast<UEdGraphSchema_K2>(Schema);
const UMaterialGraphSchema* MGSchema = Cast<UMaterialGraphSchema>(Schema);
if (!K2Schema && !MGSchema)
{
UWingServer::Printf(TEXT("error: unsupported graph schema %s\n"), *Schema->GetClass()->GetName());
return;
}
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
FSetNodeDefaultEntry Entry;
if (!WingJson::PopulateFromJson(FSetNodeDefaultEntry::StaticStruct(), &Entry, PinVal))
continue;
if (K2Schema)
HandleK2Entry(Entry, GraphObj, K2Schema);
else if (MGSchema)
HandleMaterialEntry(Entry, GraphObj);
}
UWingServer::Printf(TEXT("Done.\n"));
}
};

View File

@@ -0,0 +1,75 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraphNode.h"
#include "GraphNode_SetPositions.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FMoveNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
int32 X = 0;
UPROPERTY()
int32 Y = 0;
};
UCLASS()
class UWing_GraphNode_SetPositions : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target graph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {node, x, y} objects"))
FWingJsonArray Nodes;
virtual FString GetDescription() const override
{
return TEXT("Reposition one or more nodes in a Blueprint graph.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
for (const TSharedPtr<FJsonValue>& NodeVal : Nodes.Array)
{
FMoveNodeEntry Entry;
if (!WingJson::PopulateFromJson(FMoveNodeEntry::StaticStruct(), &Entry, NodeVal)) continue;
WingFetcher FN(TargetGraph);
UEdGraphNode* Node = FN.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) continue;
Node->NodePosX = Entry.X;
Node->NodePosY = Entry.Y;
SuccessCount++;
}
UWingServer::Printf(TEXT("Moved %d/%d nodes.\n"), SuccessCount, Nodes.Array.Num());
}
};

View File

@@ -0,0 +1,52 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingToolMenu.h"
#include "WingServer.h"
#include "ToolMenus.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "GraphNode_ShowMenu.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_ShowMenu : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target node"))
FString Node;
virtual FString GetDescription() const override
{
return TEXT("Show context menu actions available for a node and its pins.");
}
private:
virtual void Handle() override
{
WingFetcher F;
UEdGraphNode* NodeObj = F.Walk(Node).Cast<UEdGraphNode>();
if (!NodeObj) return;
if (Cast<UMaterialGraphNode>(NodeObj))
{
UWingServer::Printf(TEXT("Material graph nodes do not have usable context menus."));
return;
}
FToolMenuContext Context;
TArray<FToolMenuEntry> Entries = WingToolMenu::GetMenuItems(NodeObj, Context);
for (FToolMenuEntry &Entry : Entries)
{
FString LabelText = Entry.Label.Get().ToString();
UWingServer::Printf(TEXT("%s\n"), *LabelText);
}
if (Entries.IsEmpty()) UWingServer::Printf(TEXT("No selectable menu entries right now.\n"));
}
};

View File

@@ -0,0 +1,90 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "GraphPin_Connect.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetPin;
};
UCLASS()
class UWing_GraphPin_Connect : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target graph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {sourcePin, targetPin} objects"))
FWingJsonArray Connections;
virtual FString GetDescription() const override
{
return TEXT("Connect pins between nodes in a graph (Blueprint or Material).");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
int32 SuccessCount = 0;
int32 TotalCount = Connections.Array.Num();
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
FConnectPinsEntry Entry;
if (!WingJson::PopulateFromJson(FConnectPinsEntry::StaticStruct(), &Entry, ConnVal))
continue;
WingFetcher FS(G);
UEdGraphPin* SourcePin = FS.Walk(Entry.SourcePin).Cast<UEdGraphPin>();
if (!SourcePin) continue;
WingFetcher FT(G);
UEdGraphPin* TargetPin = FT.Walk(Entry.TargetPin).Cast<UEdGraphPin>();
if (!TargetPin) continue;
const UEdGraphSchema* Schema = G->GetSchema();
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
{
UWingServer::Printf(TEXT("error: Cannot connect %s.%s to %s.%s: %s\n"),
*WingUtils::FormatName(SourcePin->GetOwningNode()), *WingUtils::FormatName(SourcePin),
*WingUtils::FormatName(TargetPin->GetOwningNode()), *WingUtils::FormatName(TargetPin),
*Response.Message.ToString());
continue;
}
Schema->TryCreateConnection(SourcePin, TargetPin);
SuccessCount++;
}
UWingServer::Printf(TEXT("Connected %d/%d pins.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -0,0 +1,106 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "GraphPin_Disconnect.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FDisconnectPinEntry
{
GENERATED_BODY()
UPROPERTY()
FString Pin;
UPROPERTY(meta=(Optional))
FString TargetPin;
};
UCLASS()
class UWing_GraphPin_Disconnect : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target graph"))
FString Graph;
UPROPERTY(meta=(Description="Array of {pin, targetPin?} objects. If targetPin is omitted, all connections on the pin are broken."))
FWingJsonArray Disconnections;
virtual FString GetDescription() const override
{
return TEXT("Disconnect pins in a graph (Blueprint or Material). "
"Can disconnect a specific link or all links on a pin.");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
int32 SuccessCount = 0;
int32 TotalDisconnected = 0;
for (const TSharedPtr<FJsonValue>& DiscVal : Disconnections.Array)
{
FDisconnectPinEntry Entry;
if (!WingJson::PopulateFromJson(FDisconnectPinEntry::StaticStruct(), &Entry, DiscVal)) continue;
WingFetcher FP(G);
UEdGraphPin* Pin = FP.Walk(Entry.Pin).Cast<UEdGraphPin>();
if (!Pin) continue;
int32 DisconnectedCount = 0;
if (!Entry.TargetPin.IsEmpty())
{
WingFetcher FT(G);
UEdGraphPin* Target = FT.Walk(Entry.TargetPin).Cast<UEdGraphPin>();
if (!Target) continue;
if (!Pin->LinkedTo.Contains(Target))
{
UWingServer::Printf(TEXT("Error: %s.%s is not connected to %s.%s\n"),
*WingUtils::FormatName(Pin->GetOwningNode()), *WingUtils::FormatName(Pin),
*WingUtils::FormatName(Target->GetOwningNode()), *WingUtils::FormatName(Target));
continue;
}
Pin->BreakLinkTo(Target);
DisconnectedCount = 1;
}
else
{
DisconnectedCount = Pin->LinkedTo.Num();
if (DisconnectedCount > 0)
{
Pin->BreakAllPinLinks(true);
}
}
UWingServer::Printf(TEXT("Disconnected %d link(s) from %s.%s\n"),
DisconnectedCount,
*WingUtils::FormatName(Pin->GetOwningNode()), *WingUtils::FormatName(Pin));
SuccessCount++;
TotalDisconnected += DisconnectedCount;
}
UWingServer::Printf(TEXT("Done: %d/%d succeeded, %d links broken.\n"),
SuccessCount, Disconnections.Array.Num(), TotalDisconnected);
}
};

View File

@@ -0,0 +1,54 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingGraphExport.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "Materials/Material.h"
#include "MaterialGraph/MaterialGraph.h"
#include "Graph_Dump.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Graph_Dump : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Path to graph"))
FString Graph;
UPROPERTY(meta=(Optional, Description="True to include less-significant details"))
bool bDetails;
virtual FString GetDescription() const override
{
return TEXT("Dump blueprint or material graphs as readable text. ");
}
virtual void Handle() override
{
WingFetcher F;
UEdGraph *G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
WingGraphExport Exporter(G);
UWingServer::Print(*Exporter.GetOutput());
if (bDetails)
{
UWingServer::Print(Exporter.GetDetails());
}
else
{
UWingServer::Printf(TEXT("Note: use bDetails=true to see suppressed details."));
}
}
};

View File

@@ -0,0 +1,80 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingMaterialParameter.h"
#include "Materials/MaterialInstanceConstant.h"
#include "MaterialTypes.h"
#include "MaterialInstance_ClearParameter.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_MaterialInstance_ClearParameter : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target material instance"))
FString MaterialInstance;
UPROPERTY(meta=(Description="Parameter name to clear"))
FString Parameter;
UPROPERTY(meta=(Description="Parameter association: 'Global', 'Layer', or 'Blend'. Default: 'Global'", Optional))
FString ParameterAssociation = TEXT("Global");
UPROPERTY(meta=(Description="Layer/blend index (0-based). Only used when ParameterAssociation is 'Layer' or 'Blend'", Optional))
int32 ParameterLayer = INDEX_NONE;
virtual FString GetDescription() const override
{
return TEXT("Remove a parameter override from a Material Instance, reverting it to the parent material's value.");
}
virtual void Handle() override
{
WingFetcher F;
UMaterialInstanceConstant* MI = F.Asset(MaterialInstance).Cast<UMaterialInstanceConstant>();
if (!MI) return;
// Parse the association string.
EMaterialParameterAssociation Association;
if (!WingMaterialParameter::ParseMaterialParameterAssociation(ParameterAssociation, Association))
return;
FMaterialParameterInfo ParamID(*Parameter, Association, ParameterLayer);
// Remove the override from all parameter arrays.
auto RemoveFrom = [&](auto& Arr) {
return Arr.RemoveAll([&](auto& Entry) { return Entry.ParameterInfo == ParamID; });
};
int32 Removed = 0;
Removed += RemoveFrom(MI->ScalarParameterValues);
Removed += RemoveFrom(MI->VectorParameterValues);
Removed += RemoveFrom(MI->DoubleVectorParameterValues);
Removed += RemoveFrom(MI->TextureParameterValues);
Removed += RemoveFrom(MI->TextureCollectionParameterValues);
Removed += RemoveFrom(MI->RuntimeVirtualTextureParameterValues);
Removed += RemoveFrom(MI->SparseVolumeTextureParameterValues);
Removed += RemoveFrom(MI->FontParameterValues);
if (Removed == 0)
{
UWingServer::Printf(TEXT("No override found for parameter '%s' (association=%s layer=%d) on %s"),
*Parameter, *ParameterAssociation, ParameterLayer, *WingUtils::FormatName(MI));
return;
}
WingUtils::SaveGenericPackage(MI);
UWingServer::Printf(TEXT("Cleared override for '%s' on %s\n"),
*Parameter, *WingUtils::FormatName(MI));
}
};

View File

@@ -0,0 +1,72 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Materials/Material.h"
#include "Materials/MaterialInterface.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Factories/MaterialInstanceConstantFactoryNew.h"
#include "WingPackageMaker.h"
#include "MaterialInstance_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_MaterialInstance_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new Material Instance (e.g. '/Game/Materials/MI_GoldShiny')"))
FString AssetPath;
UPROPERTY(meta=(Description="Parent material package path (Material or Material Instance)"))
FString ParentMaterial;
virtual FString GetDescription() const override
{
return TEXT("Create a new Material Instance Constant asset with a specified parent material.");
}
virtual void Handle() override
{
WingPackageMaker Maker(AssetPath);
if (!Maker.Ok()) return;
// Load parent material by package path.
WingFetcher F;
UObject* ParentObj = F.Asset(ParentMaterial).GetObj();
UMaterialInterface* ParentMaterialObj = ParentObj ? Cast<UMaterialInterface>(ParentObj) : nullptr;
if (!ParentMaterialObj)
{
UWingServer::Printf(TEXT("ERROR: Parent material '%s' not found or not a material\n"), *ParentMaterial);
return;
}
// Create via factory + AssetTools.
UMaterialInstanceConstant* MI = Maker.CreateAsset<UMaterialInstanceConstant, UMaterialInstanceConstantFactoryNew>();
if (!MI) return;
// Set parent.
MI->Parent = ParentMaterialObj;
// Save.
bool bSaved = WingUtils::SaveGenericPackage(MI);
UWingServer::Printf(TEXT("Created %s\n"), *MI->GetPathName());
if (UMaterialInstance* ParentMI = Cast<UMaterialInstance>(ParentMaterialObj))
UWingServer::Printf(TEXT("Parent: %s\n"), *WingUtils::FormatName(ParentMI));
else if (UMaterial* ParentMat = Cast<UMaterial>(ParentMaterialObj))
UWingServer::Printf(TEXT("Parent: %s\n"), *WingUtils::FormatName(ParentMat));
else
UWingServer::Printf(TEXT("Parent: %s\n"), *ParentMaterialObj->GetPathName());
if (!bSaved)
UWingServer::Print(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -0,0 +1,58 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingMaterialParameter.h"
#include "Materials/MaterialInstanceConstant.h"
#include "MaterialTypes.h"
#include "MaterialInstance_DumpParameters.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_MaterialInstance_DumpParameters : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target material instance"))
FString MaterialInstance;
virtual FString GetDescription() const override
{
return TEXT("List all parameters on a Material Instance, showing current values and which are overridden.");
}
virtual void Handle() override
{
WingFetcher F;
UMaterialInstanceConstant* MI = F.Asset(MaterialInstance).Cast<UMaterialInstanceConstant>();
if (!MI) return;
auto AllParams = WingMaterialParameter::GetMaterialParameters(MI);
// Overridden parameters first.
bool bHasOverrides = false;
for (auto& [Info, Meta] : AllParams)
{
if (!Meta.bOverride) continue;
if (!bHasOverrides) { UWingServer::Print(TEXT("\nOverridden Parameters:\n")); bHasOverrides = true; }
WingMaterialParameter::FormatMaterialParameter(Info, Meta);
}
// Inherited (non-overridden) parameters.
bool bHasInherited = false;
for (auto& [Info, Meta] : AllParams)
{
if (Meta.bOverride) continue;
if (!bHasInherited) { UWingServer::Print(TEXT("\nInherited Parameters (not overridden):\n")); bHasInherited = true; }
WingMaterialParameter::FormatMaterialParameter(Info, Meta);
}
}
};

View File

@@ -0,0 +1,109 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "WingMaterialParameter.h"
#include "Materials/MaterialInstanceConstant.h"
#include "MaterialTypes.h"
#include "Misc/DefaultValueHelper.h"
#include "MaterialInstance_SetParameter.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_MaterialInstance_SetParameter : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target material instance"))
FString MaterialInstance;
UPROPERTY(meta=(Description="Parameter name to set"))
FString Parameter;
UPROPERTY(meta=(Description="Parameter association: 'Global', 'Layer', or 'Blend'. Default: 'Global'", Optional))
FString ParameterAssociation = TEXT("Global");
UPROPERTY(meta=(Description="Layer/blend index (0-based). Only used when ParameterAssociation is 'Layer' or 'Blend'", Optional))
int32 ParameterLayer = INDEX_NONE;
UPROPERTY(meta=(Description="Value to set (uses Unreal text format, e.g. '0.5' for scalar, '(R=1,G=0,B=0,A=1)' for vector)"))
FString Value;
virtual FString GetDescription() const override
{
return TEXT("Set a parameter override on a Material Instance.");
}
virtual void Handle() override
{
WingFetcher F;
UMaterialInstanceConstant* MI = F.Asset(MaterialInstance).Cast<UMaterialInstanceConstant>();
if (!MI) return;
// Parse the association string.
EMaterialParameterAssociation Association;
if (!WingMaterialParameter::ParseMaterialParameterAssociation(ParameterAssociation, Association))
return;
// Build the parameter ID to look up.
FMaterialParameterInfo ParamID(*Parameter, Association, ParameterLayer);
// Find it in the material's parameter map.
auto AllParams = WingMaterialParameter::GetMaterialParameters(MI);
FMaterialParameterMetadata* Found = AllParams.Find(ParamID);
if (!Found)
{
UWingServer::Printf(TEXT("No parameter named '%s' with association=%s layer=%d"),
*Parameter, *ParameterAssociation, ParameterLayer);
return;
}
if (Found->PrimitiveDataIndex != INDEX_NONE)
{
UWingServer::Printf(TEXT("Parameter '%s' uses custom primitive data and cannot be set on a material instance"), *Parameter);
return;
}
EMaterialParameterType Type = Found->Value.Type;
switch (Type)
{
case EMaterialParameterType::Scalar:
{
float ScalarValue;
if (!FDefaultValueHelper::ParseFloat(Value, ScalarValue))
{
UWingServer::Printf(TEXT("Failed to parse '%s' as a float"), *Value);
return;
}
MI->SetScalarParameterValueEditorOnly(ParamID, ScalarValue);
break;
}
case EMaterialParameterType::Vector:
{
FLinearColor Color;
if (!Color.InitFromString(Value))
{
UWingServer::Printf(TEXT("Failed to parse '%s' as a color/vector (expected format: '(R=1,G=0,B=0,A=1)')"), *Value);
return;
}
MI->SetVectorParameterValueEditorOnly(ParamID, Color);
break;
}
default:
UWingServer::Printf(TEXT("Parameters of type %d (see EMaterialParameterType) are not implemented"), (int)Type);
return;
}
WingUtils::SaveGenericPackage(MI);
UWingServer::Printf(TEXT("Set '%s' = %s on %s\n"),
*Parameter, *Value, *WingUtils::FormatName(MI));
}
};

View File

@@ -0,0 +1,62 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingUtils.h"
#include "Materials/Material.h"
#include "Material_Compile.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Material_Compile : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material name or package path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Force recompile a material and check for compilation errors.");
}
virtual void Handle() override
{
// Load material
WingFetcher F;
UMaterial* MaterialObj = F.Asset(Material).Cast<UMaterial>();
if (!MaterialObj) return;
// Force recompile
MaterialObj->ForceRecompileForRendering();
// Wait for compilation to finish, then check for errors
FMaterialResource* Resource = MaterialObj->GetMaterialResource(GMaxRHIFeatureLevel);
TArray<FString> Errors;
if (Resource)
{
Resource->FinishCompilation();
Errors = Resource->GetCompileErrors();
}
if (Errors.IsEmpty())
{
UWingServer::Printf(TEXT("%s compiled successfully.\n"), *WingUtils::FormatName(MaterialObj));
}
else
{
UWingServer::Printf(TEXT("%s compiled with %d error(s):\n"), *WingUtils::FormatName(MaterialObj), Errors.Num());
for (const FString& Err : Errors)
{
UWingServer::Printf(TEXT(" %s\n"), *Err);
}
}
}
};

View File

@@ -0,0 +1,45 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingUtils.h"
#include "Materials/Material.h"
#include "MaterialDomain.h"
#include "Factories/MaterialFactoryNew.h"
#include "WingPackageMaker.h"
#include "Material_Create.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Material_Create : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Full asset path for the new material"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("Create a new UMaterial asset");
}
virtual void Handle() override
{
WingPackageMaker Maker(Material);
if (!Maker.Ok()) return;
// Create via IAssetTools + factory.
UMaterial* MaterialObj = Maker.CreateAsset<UMaterial, UMaterialFactoryNew>();
if (!MaterialObj) return;
bool bSaved = WingUtils::SaveGenericPackage(MaterialObj);
UWingServer::Printf(TEXT("Created %s\n"), *MaterialObj->GetPathName());
if (!bSaved) UWingServer::Print(TEXT("WARNING: Package save failed\n"));
}
};

View File

@@ -0,0 +1,45 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingServer.h"
#include "WingUtils.h"
#include "WingMaterialParameter.h"
#include "MaterialTypes.h"
#include "Material_DumpParameters.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Material_DumpParameters : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Material path"))
FString Material;
virtual FString GetDescription() const override
{
return TEXT("List all parameters on a Material, showing their default values.");
}
virtual void Handle() override
{
WingFetcher F;
UMaterial* Mat = F.Asset(Material).Cast<UMaterial>();
if (!Mat) return;
auto AllParams = WingMaterialParameter::GetMaterialParameters(Mat);
for (auto& [Info, Meta] : AllParams)
{
WingMaterialParameter::FormatMaterialParameter(Info, Meta);
}
if (AllParams.IsEmpty()) UWingServer::Printf(TEXT("No material parameters.\n"));
}
};

View File

@@ -0,0 +1,98 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "Property_Dump.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Property_Dump : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target object"))
FString Object;
UPROPERTY(meta=(Optional, Description="Substring filter for property names"))
FString Query;
UPROPERTY(meta=(Optional, Description="Truncate values to 80 characters (default true)"))
bool Truncate = true;
UPROPERTY(meta=(Optional, Description="Only show properties declared on the object's own class, not inherited ones"))
bool Local = false;
virtual FString GetDescription() const override
{
return TEXT("List all blueprint-visible properties, showing current values and which are editable.");
}
virtual void Handle() override
{
// Resolve the path to an object and get its editable template.
WingFetcher F;
UObject* Template = F.Walk(Object).Cast<UObject>();
if (!Template) return;
TArray<WingProperty> AllProps = WingProperty::GetAll(Template, CPF_Edit);
TArray<WingProperty> Props = WingProperty::FindAllSubstring(AllProps, Query);
if (Local)
{
UClass* ObjClass = Template->GetClass();
Props.RemoveAll([ObjClass](const WingProperty& P) { return P->GetOwnerStruct() != ObjClass; });
}
// Group properties by category.
TMap<FString, TArray<WingProperty>> ByCategory;
for (WingProperty& P : Props)
{
FString Category = P->HasMetaData(TEXT("Category")) ? P->GetMetaData(TEXT("Category")) : FString();
ByCategory.FindOrAdd(Category).Add(P);
}
// Sort category names, putting empty category last.
TArray<FString> Categories;
ByCategory.GetKeys(Categories);
Categories.Sort([](const FString& A, const FString& B) {
if (A.IsEmpty()) return false;
if (B.IsEmpty()) return true;
return A < B;
});
for (const FString& Category : Categories)
{
if (Category.IsEmpty())
UWingServer::Print(TEXT("\nUncategorized:\n"));
else
UWingServer::Printf(TEXT("\n%s:\n"), *Category);
for (WingProperty& P : ByCategory[Category])
{
FString PropName = WingUtils::FormatName(P.Prop);
FString ValueStr = P.GetText();
if (Truncate && (ValueStr.Len() > 80))
ValueStr = ValueStr.Left(80) + TEXT("...");
bool bEditable = !P->HasAnyPropertyFlags(CPF_EditConst);
UWingServer::Printf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("editable") : TEXT("readonly"),
*UWingTypes::TypeToText(P.Prop),
*PropName,
*ValueStr);
}
}
if (Props.IsEmpty())
UWingServer::Print(TEXT(" (no blueprint-visible properties found)\n"));
}
};

View File

@@ -0,0 +1,46 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "Property_Get.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Property_Get : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target object"))
FString Object;
UPROPERTY(meta=(Description="Property name"))
FString Property;
virtual FString GetDescription() const override
{
return TEXT("Get the value of a single property.");
}
virtual void Handle() override
{
WingFetcher F;
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
TArray<WingProperty> All = WingProperty::GetAll(Obj, CPF_Edit);
WingProperty P = WingProperty::FindOneExactMatch(All, Property);
if (!P) return;
UWingServer::Print(P.GetText());
UWingServer::Print(TEXT("\n"));
}
};

View File

@@ -0,0 +1,79 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "Property_Set.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Property_Set : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Target object"))
FString Object;
UPROPERTY(meta=(Description="Object mapping property names to new values in Unreal text format"))
FWingJsonObject Properties;
virtual FString GetDescription() const override
{
return TEXT("Set one or more editable properties. Values use Unreal text format.");
}
virtual void Handle() override
{
// Resolve the path to an object and get its editable template.
WingFetcher F;
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
if (!Properties.Json || Properties.Json->Values.Num() == 0)
{
UWingServer::Print(TEXT("Error: No properties specified\n"));
return;
}
// Validation pass — resolve all properties and values before modifying anything.
TArray<WingProperty> All = WingProperty::GetAll(Obj, CPF_Edit);
TArray<TPair<WingProperty, FString>> Resolved;
for (const auto& Pair : Properties.Json->Values)
{
WingProperty P = WingProperty::FindOneExactMatch(All, Pair.Key);
if (!P) return;
FString ValueStr;
if (!Pair.Value->TryGetString(ValueStr))
{
UWingServer::Printf(TEXT("Error: Value for '%s' must be a string\n"), *Pair.Key);
return;
}
Resolved.Emplace(P, ValueStr);
}
// Apply all changes.
int32 SuccessCount = 0;
for (auto& [P, ValueStr] : Resolved)
{
if (!P.SetText(ValueStr))
continue;
SuccessCount++;
}
// Save.
bool bSaved = WingUtils::SaveGenericPackage(Obj);
UWingServer::Printf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num());
if (!bSaved)
UWingServer::Print(TEXT("Warning: Save failed\n"));
}
};

View File

@@ -0,0 +1,85 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingFetcher.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "ShowCommands.generated.h"
UCLASS()
class UWing_ShowCommands : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Optional, Description="Substring filter for command names"))
FString Query;
UPROPERTY(meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual FString GetDescription() const override
{
return TEXT("List all available commands with their descriptions.");
}
void EmitCommand(UClass* Class)
{
if (Verbose)
{
WingUtils::FormatCommandHelp(Class);
return;
}
UWingServer::Print(WingUtils::GetHandlerName(Class));
UWingServer::Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(Class, 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"));
}
void EmitCommandList(bool bHalfBaked)
{
FString QueryLower = Query.ToLower();
FString PrevGroup;
for (UClass* Class : WingUtils::CollectHandlerClasses())
{
bool bIsHalfBaked = Class->GetMetaData(TEXT("ModuleRelativePath")).StartsWith(TEXT("HalfBaked/"));
if (bIsHalfBaked != bHalfBaked)
continue;
FString ToolName = WingUtils::GetHandlerName(Class);
if (!ToolName.ToLower().Contains(QueryLower))
continue;
// Blank line between groups
FString Group = WingUtils::GetHandlerGroup(Class);
if (Group != PrevGroup)
{
if (!PrevGroup.IsEmpty())
UWingServer::Print(TEXT("\n"));
PrevGroup = Group;
}
EmitCommand(Class);
}
}
virtual void Handle() override
{
UWingServer::Printf(TEXT("\n"));
EmitCommandList(false);
// UWingServer::Print(TEXT("\n--- Half-Baked (may have issues) ---\n\n"));
// EmitCommandList(true);
UWingServer::Printf(TEXT("\n"));
}
};

View File

@@ -0,0 +1,104 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "AssetRegistry/AssetData.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "UObject/UObjectIterator.h"
#include "StructUtils/UserDefinedStruct.h"
#include "Engine/UserDefinedEnum.h"
#include "Engine/Blueprint.h"
#include "TypeName_Search.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_TypeName_Search : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(meta=(Description="Substring filter for type names"))
FString Query;
UPROPERTY(meta=(Optional, Description="Maximum number of results"))
int32 Limit = 100;
virtual FString GetDescription() const override
{
return TEXT("Search for type names usable in pin type specifications. "
"Returns short names that can be used with commands like Blueprint_ChangeVariableType.");
}
void TryMatchObject(TSet<UObject*> &Matches, UObject *Obj)
{
if (!Obj) return;
FString Name = Obj->GetName();
if (!Name.Contains(Query, ESearchCase::IgnoreCase)) return;
Matches.Add(Obj);
}
void TryMatchObjects(TSet<UObject*> &Matches, UClass *Class)
{
ForEachObjectOfClass(Class, [&](UObject *Obj){
if (Matches.Num() == Limit) return;
TryMatchObject(Matches, Obj);
}, true);
}
void TryMatchAssets(TSet<UObject*> &Matches, UClass *Class)
{
IAssetRegistry& Registry = *IAssetRegistry::Get();
TArray<FAssetData> AssetResults;
Registry.GetAssetsByClass(Class->GetClassPathName(), AssetResults, true);
for (const FAssetData& Data : AssetResults)
{
if (Matches.Num() == Limit) return;
FString Name = Data.AssetName.ToString();
if (!Name.Contains(Query, ESearchCase::IgnoreCase)) continue;
UObject *Obj = Data.GetAsset();
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
Obj = BP->GeneratedClass;
TryMatchObject(Matches, Obj);
}
}
virtual void Handle() override
{
TSet<UObject*> Matches;
TryMatchObjects(Matches, UScriptStruct::StaticClass());
TryMatchObjects(Matches, UClass::StaticClass());
TryMatchObjects(Matches, UEnum::StaticClass());
TryMatchAssets(Matches, UBlueprint::StaticClass());
TryMatchAssets(Matches, UUserDefinedStruct::StaticClass());
TryMatchAssets(Matches, UUserDefinedEnum::StaticClass());
TArray<FString> Results;
for (const UObject *Obj : Matches)
{
const TCHAR *Kind = TEXT("Unknown");
if (Cast<UEnum>(Obj))
Kind = TEXT("Enum");
else if (Cast<UScriptStruct>(Obj))
Kind = TEXT("Struct");
else if (const UClass* Class = Cast<UClass>(Obj))
Kind = Class->IsChildOf(UInterface::StaticClass()) ? TEXT("Interface") : TEXT("Class");
Results.Add(FString::Printf(TEXT("%s %s\n"), Kind, *UWingTypes::TypeToText(Obj)));
}
Results.Sort();
for (const auto &Result : Results)
{
UWingServer::Print(Result);
}
if (Results.Num() == Limit)
{
UWingServer::Printf(TEXT("Search limit reached, raise it with Limit=\n"));
}
}
};

View File

@@ -0,0 +1,90 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingServer.h"
#include "UserManual.generated.h"
UCLASS()
class UWing_UserManual : public UObject, public IWingHandler
{
GENERATED_BODY()
public:
virtual FString GetDescription() const override
{
return TEXT("Print the user manual.");
}
virtual void Handle() override
{
UWingServer::Print(TEXT(
"\n PATHS:"
"\n"
"\n Most commands require you to specify a path. A path starts"
"\n with an asset name, followed by comma-separated steps that"
"\n navigate into the asset. Example:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result"
"\n"
"\n The navigation steps supported are:"
"\n"
"\n graph — move from a blueprint or material to a graph."
"\n node — move from a graph to a graph node"
"\n pin — move from a graph node to a pin"
"\n component — move from a blueprint to a component"
"\n levelblueprint — move from a world to a blueprint"
"\n"
"\n Steps do not always require a parameter. For example, materials"
"\n only have one graph, so you can just say:"
"\n"
"\n /Game/Materials/MyMaterial,graph"
"\n"
"\n TYPES:"
"\n"
"\n To change variable types, or function prototypes, you will"
"\n use our syntax for types. Here are some simple examples:"
"\n"
"\n boolean, int64, double, string, etc"
"\n vector, rotator, hitresult, etc"
"\n actor, character, playercontroller, etc"
"\n eblendmode, emovementmode, etc"
"\n"
"\n Notice that it's 'actor', not 'AActor'."
"\n You can use the following notations for complex types:"
"\n"
"\n soft<abp_manny>, class<pawn>, softclass<pawn>"
"\n array<int>, set<string>, map<int, string>"
"\n"
"\n FUNCTION ARGUMENTS AND RETURN VALUES:"
"\n"
"\n Function argument lists are expressed as comma-separated"
"\n lists containing type and variable name:"
"\n"
"\n double D, PlayerController P, array<int> A"
"\n"
"\n To change the arguments or return values of a function, edit the"
"\n entry or exit node of the graph using GraphNode_SetArgs."
"\n You can view the arguments using GraphNode_Dump. If a return "
"\n node doesn't exist, you may have to create it using GraphNode_Create"
"\n before you can set return values. Custom event nodes also have"
"\n editable arguments."
"\n"
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain mxprop"
"\n properties, which actually come from the material expressions."
"\n You can edit these using Property_Set on the node."
"\n"
"\n COMMANDS YOU SHOULD KNOW ABOUT AND REMEMBER:"
"\n"
"\n UserManual: this explanation"
"\n ShowCommands: a full list of all the commands"
"\n Graph_Dump: a detailed listing of any UEdGraph"
"\n Property_Dump: show information on many objects"
"\n"
));
}
};

View File

@@ -0,0 +1,62 @@
* Command-handlers are classes that implement the WingHandler
interface. The command's json parameters automatically get
injected into the handler's properties using reflection
code. Check out a few handlers to see how this works.
* Class WingFetcher can precisely and easily retrieve objects
of all different kinds using a 'path'. Study the API
of this class, we use it everywhere. It is the best tool
when you need the caller to specify a blueprint, or a
graph, or a pin - you name it.
* When you want to scan for a list of assets, MCPAssets
is the best tool. This wraps Unreal's asset database
in a convenient interface. Please study this API.
* The only valid way to get a name for an object is using
WingUtils::FormatName(obj). We don't allow the use of
pin->GetName, or node->GetTitle, or any other name-fetching
routine. Using only WingUtils::FormatName guarantees
consistency: that means, names output by one tool can
be parsed by a different tool.
* To check whether a string matches an object's name,
there's only one valid way to do that as well: using
bool WingUtils::Identifies(Name, Obj).
* Concise output is better. The output of these handlers
will primarily be consumed by LLMs with finite context
windows. Avoid sending information the caller didn't ask
for. Don't echo the command parameters: the caller knows
what parameters he sent. Don't make things unnecessarily
verbose: "type=int varname=x" can be shortened to "int x."
Generate output in a form that *you* would want to
consume.
* You can pass the output StringBuilder directly into
WingFetcher and MCPAssets. If you do, these will automatically
generate good error messages. If either of these classes
returns 'false', you don't need to generate an error
message: it's already been done. Any other routine that
accepts MCPErrorCallback can do the same.
* It's good for handlers to do operations in batches,
where possible. SpawnNodes is better than SpawnNode.
When an LLM calls into an MCP, it often takes 15 to 30
seconds. It's *important* that ConnectPins can do batches,
because building a graph can involve connecting dozens
of pins.
* It is traditional to use UE_LOG in unreal code, but it
really doesn't work for us: you see, the LLM invoking the
MCP can't see the log messages, so what's the point?
Better to report problems via the response. Please remove
UE_LOG calls in handlers.
* When you're going to edit something, it's important to
mark things dirty. There's a very powerful tool for that:
WingUtils::PreEdit and WingUtils::PostEdit, which can also
be accessed through WingFetcher::PreEdit and PostEdit.
These routines are very careful about marking everything
dirty, so you don't have to worry about it.

View File

@@ -0,0 +1,59 @@
# Serious Issues Remaining in UEWingman Handlers
## Breaking API Changes
Several handlers switched from GUID-based node matching to
`WingUtils::Identifies()`. Since the only caller is our own MCP
bridge (which uses `FormatName` output from dump commands), this
is actually fine — but it's a one-way door.
- **ChangeStructNodeType, SetMaterialExpressionPosition,
DeleteMaterialExpression, DisconnectMaterialExpressionPin** —
node param now expects FormatName identifiers, not GUIDs
- **RemoveStructField, AddStructField** — switched from
MCPAssets (accepts bare names) to WingFetcher (requires full
paths)
- **SetPinDefaultValues** — entry struct changed from
`{Blueprint, Node, PinName, Value}` to `{Pin, Value}` where
Pin is a full fetcher path
- **SpawnNodesInGraph** — changed from `Blueprint` + `Graph`
params to single `Graph` path
## Potential Crashes
- **SearchAssets** — `Data.GetClass()` can return null with
`.Info()` (asset class not loaded). Would crash on
`FormatName`.
- **SpawnNodesInGraph** — assumes `GetOuter()` of a graph is
always a `UBlueprint`. Fails for level blueprints.
## Silent Error Handling Gaps
- **AddAnimStateToMachine, SetAnimStateAnimation,
SetAnimTransitionRule** — `FindStateMachineGraph` and
`FindStateByName` lack MCPErrorCallback. If the state machine
or state isn't found, error reporting is ad-hoc/incomplete.
## Behavioral Changes
- **SetBlendSpaceSamplePoints** — now aborts on animation
lookup failure instead of silently inserting a blank sample.
Probably better behavior, but different.
## Minor Concerns
- **SetNodePositions** — node search across all graphs could
match wrong node if names collide across graphs
- **ReplaceFunctionCallsInBlueprint** — connection survival
uses pointer comparison; could be unsafe if pins are
recreated during the replacement
- **DumpMaterialInstanceParameters** — parent chain lost class
type info (Material vs MaterialInstance)
## Design changes
- Saving assets is being done at somewhat unpredictable
points. It's not entirely clear that we *should* be
saving things every time an edit is made. It might be
better to have an explicit "Save" MCP command.

View File

@@ -0,0 +1,153 @@
#include "WingBlueprintVar.h"
#include "WingJson.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "EdGraphSchema_K2.h"
#include "Kismet2/BlueprintEditorUtils.h"
FBlueprintVar::FBlueprintVar(UBlueprint* BP, const FString& VarName)
{
FName VarFName(*VarName);
int32 VarIndex = FBlueprintEditorUtils::FindNewVariableIndex(BP, VarFName);
if (VarIndex == INDEX_NONE)
{
UWingServer::Printf(TEXT("ERROR: Variable '%s' not found in %s\n"), *VarName, *WingUtils::FormatName(BP));
return;
}
Desc = &BP->NewVariables[VarIndex];
// Try to find the default value property on the CDO.
if (BP->GeneratedClass)
{
UObject* CDO = BP->GeneratedClass->GetDefaultObject();
FProperty* Prop = BP->GeneratedClass->FindPropertyByName(VarFName);
if (CDO && Prop)
DefaultValueProp = WingProperty(Prop, CDO);
}
}
void FBlueprintVar::Dump()
{
LoadFlags();
LoadDefault();
TArray<WingProperty> Props = MergedProperties();
for (WingProperty& P : Props)
{
UWingServer::Printf(TEXT(" %s %s = %s\n"),
*UWingTypes::TypeToText(P.Prop),
*WingUtils::FormatName(P.Prop),
*P.GetText());
}
}
bool FBlueprintVar::ApplyJson(const FJsonObject* Json)
{
bool bHasDefault = Json->HasField(TEXT("DefaultValue"));
bool bHasType = Json->HasField(TEXT("VarType"));
if (bHasDefault && bHasType)
{
UWingServer::Print(TEXT(
"ERROR: Cannot set VarType and DefaultValue in the same call.\n"
"Change the type first, then recompile the blueprint,\n"
"then set the default.\n"));
return false;
}
LoadFlags();
TArray<WingProperty> Props = MergedProperties();
if (!WingJson::PopulateFromJson(Props, Json, true))
return false;
SaveFlags();
if (bHasDefault)
return SaveDefault();
return true;
}
void FBlueprintVar::LoadFlags()
{
InstanceEditable = !(Desc->PropertyFlags & CPF_DisableEditOnInstance);
BlueprintReadOnly = (Desc->PropertyFlags & CPF_BlueprintReadOnly) != 0;
ExposeToCinematics = (Desc->PropertyFlags & CPF_Interp) != 0;
ExposeOnSpawn = Desc->HasMetaData(FBlueprintMetadata::MD_ExposeOnSpawn);
Private = Desc->HasMetaData(FBlueprintMetadata::MD_Private);
if (Desc->HasMetaData(TEXT("tooltip")))
Description = Desc->GetMetaData(TEXT("tooltip"));
else
Description.Empty();
}
void FBlueprintVar::LoadDefault()
{
if (DefaultValueProp)
DefaultValue = DefaultValueProp.GetText();
else
DefaultValue.Empty();
}
void FBlueprintVar::SaveFlags()
{
// CPF flags
if (InstanceEditable)
Desc->PropertyFlags &= ~CPF_DisableEditOnInstance;
else
Desc->PropertyFlags |= CPF_DisableEditOnInstance;
if (BlueprintReadOnly)
Desc->PropertyFlags |= CPF_BlueprintReadOnly;
else
Desc->PropertyFlags &= ~CPF_BlueprintReadOnly;
if (ExposeToCinematics)
Desc->PropertyFlags |= CPF_Interp;
else
Desc->PropertyFlags &= ~CPF_Interp;
// Metadata flags
if (ExposeOnSpawn)
Desc->SetMetaData(FBlueprintMetadata::MD_ExposeOnSpawn, TEXT("true"));
else
Desc->RemoveMetaData(FBlueprintMetadata::MD_ExposeOnSpawn);
if (Private)
Desc->SetMetaData(FBlueprintMetadata::MD_Private, TEXT("true"));
else
Desc->RemoveMetaData(FBlueprintMetadata::MD_Private);
// Description/tooltip
if (!Description.IsEmpty())
Desc->SetMetaData(TEXT("tooltip"), Description);
else
Desc->RemoveMetaData(TEXT("tooltip"));
}
bool FBlueprintVar::SaveDefault()
{
if (DefaultValueProp)
return DefaultValueProp.SetText(DefaultValue);
return true;
}
TArray<WingProperty> FBlueprintVar::MergedProperties()
{
TArray<WingProperty> Props = WingProperty::GetAll(
FBPVariableDescription::StaticStruct(), Desc, CPF_Edit);
WingProperty::Remove(Props, TEXT("PropertyFlags"));
WingProperty::Remove(Props, TEXT("MetaDataArray"));
WingProperty::Remove(Props, TEXT("VarName"));
WingProperty::Remove(Props, TEXT("VarGuid"));
WingProperty::Remove(Props, TEXT("DefaultValue"));
Props.Append(WingProperty::GetAll(
FBlueprintVar::StaticStruct(), this, (EPropertyFlags)0));
// Remove DefaultValue if we don't have a CDO property to back it.
if (!DefaultValueProp)
WingProperty::Remove(Props, TEXT("DefaultValue"));
return Props;
}

View File

@@ -0,0 +1,30 @@
#include "WingCommandlet.h"
#include "WingServer.h"
#include "Containers/Ticker.h"
UWingCommandlet::UWingCommandlet()
{
IsClient = false;
IsEditor = true;
IsServer = false;
LogToConsole = true;
}
int32 UWingCommandlet::Main(const FString& Params)
{
// The UWingServer editor subsystem starts the server automatically.
// We just need to tick it, since FTickableEditorObject doesn't tick in commandlet mode.
double LastTime = FPlatformTime::Seconds();
while (!IsEngineExitRequested())
{
double CurrentTime = FPlatformTime::Seconds();
double DeltaTime = CurrentTime - LastTime;
LastTime = CurrentTime;
FTSTicker::GetCoreTicker().Tick(DeltaTime);
UWingServer::TickServer(DeltaTime);
FPlatformProcess::Sleep(0.01f);
}
return 0;
}

Some files were not shown because too many files have changed in this diff Show More