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,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;
}

View File

@@ -0,0 +1,346 @@
#include "WingFetcher.h"
#include "WingServer.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Engine/World.h"
#include "Materials/Material.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "IMaterialEditor.h"
#include "Engine/LevelScriptBlueprint.h"
#include "Subsystems/AssetEditorSubsystem.h"
WingFetcher::WalkFunc WingFetcher::GetWalker(const FString& Step)
{
if (Step.Equals(TEXT("graph"), ESearchCase::IgnoreCase)) return &WingFetcher::Graph;
if (Step.Equals(TEXT("node"), ESearchCase::IgnoreCase)) return &WingFetcher::Node;
if (Step.Equals(TEXT("pin"), ESearchCase::IgnoreCase)) return &WingFetcher::Pin;
if (Step.Equals(TEXT("component"), ESearchCase::IgnoreCase)) return &WingFetcher::Component;
if (Step.Equals(TEXT("levelblueprint"), ESearchCase::IgnoreCase)) return &WingFetcher::LevelBlueprint;
return nullptr;
}
void WingFetcher::SetObj(UObject* InObj)
{
UWingServer::AddTouchedObject(InObj);
Obj = InObj;
ResultPin = nullptr;
}
void WingFetcher::SetPin(UEdGraphPin* InPin)
{
ResultPin = InPin;
Obj = nullptr;
}
WingFetcher& WingFetcher::SetError()
{
bError = true;
Obj = nullptr;
ResultPin = nullptr;
OriginalAsset = nullptr;
Editor = nullptr;
return *this;
}
void WingFetcher::PathFailed(const TCHAR* Expected)
{
SetError();
if (ResultPin)
UWingServer::Printf(TEXT("ERROR: Path specifies a pin, but expected %s\n"), Expected);
else if (Obj)
UWingServer::Printf(TEXT("ERROR: Path specifies a %s, but expected %s\n"), *Obj->GetClass()->GetName(), Expected);
else
UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n"));
}
WingFetcher& WingFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expected)
{
SetError();
if (ResultPin)
UWingServer::Printf(TEXT("ERROR: Input to '%s' is a pin, but expected %s\n"), Walker, Expected);
else if (Obj)
UWingServer::Printf(TEXT("ERROR: Input to '%s' is %s, but expected %s\n"), Walker, *Obj->GetClass()->GetName(), Expected);
else
UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n"));
return *this;
}
WingFetcher& WingFetcher::Walk(const FString& Path)
{
if (bError) return *this;
TArray<FString> Segments;
Path.ParseIntoArray(Segments, TEXT(","));
if (Segments.Num() == 0)
{
UWingServer::Print(TEXT("ERROR: Empty path\n"));
return SetError();
}
for (int32 i = 0; i < Segments.Num(); i++)
{
if (!Obj && !ResultPin)
{
Asset(Segments[i]);
if (bError) return *this;
continue;
}
FString Key, Value;
if (!Segments[i].Split(TEXT(":"), &Key, &Value))
Key = Segments[i];
WalkFunc Func = GetWalker(Key);
if (!Func)
{
UWingServer::Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key);
return SetError();
}
(this->*Func)(Value);
if (bError) return *this;
}
return *this;
}
WingFetcher& WingFetcher::Asset(const FString& PackagePath)
{
if (bError) return *this;
SetObj(LoadObject<UObject>(nullptr, *PackagePath));
if (!Obj)
{
UWingServer::Printf(TEXT("ERROR: Could not load asset '%s'\n"), *PackagePath);
return SetError();
}
OriginalAsset = Obj;
// Open the editor for this asset (or bring it to front if already open).
UAssetEditorSubsystem* Sub = GEditor->GetEditorSubsystem<UAssetEditorSubsystem>();
if (!Sub || !Sub->OpenEditorForAsset(Obj))
{
UWingServer::Printf(TEXT("ERROR: Could not open editor for '%s'\n"), *PackagePath);
return SetError();
}
Editor = Sub->FindEditorForAsset(OriginalAsset, false);
if (!Editor)
{
UWingServer::Printf(TEXT("ERROR: Could not find editor instance for '%s'\n"), *PackagePath);
return SetError();
}
// If this is a material, use the editor's transient copy.
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))
{
IMaterialEditor *MatEditor = static_cast<IMaterialEditor*>(Editor);
SetObj(MatEditor->GetMaterialInterface()->GetBaseMaterial());
}
return *this;
}
bool WingFetcher::CheckAssetIsA(UClass* StaticClass)
{
if (bError) return false;
if (!OriginalAsset || !OriginalAsset->IsA(StaticClass))
{
UWingServer::Printf(TEXT("ERROR: Asset is %s, expected %s\n"),
OriginalAsset ? *OriginalAsset->GetClass()->GetName() : TEXT("null"),
*StaticClass->GetName());
SetError();
return false;
}
return true;
}
WingFetcher& WingFetcher::Graph(const FString& Value)
{
if (bError) return *this;
// Material with blank graph name → navigate to the material graph.
if (UMaterial* Mat = ::Cast<UMaterial>(Obj))
{
if (!Value.IsEmpty())
{
UWingServer::Printf(TEXT("ERROR: Materials do not have named graphs (got '%s')\n"), *Value);
return SetError();
}
WingUtils::EnsureMaterialGraph(Mat);
if (!Mat->MaterialGraph)
{
UWingServer::Printf(TEXT("ERROR: Material '%s' has no material graph\n"), *Mat->GetName());
return SetError();
}
SetObj(Mat->MaterialGraph);
return *this;
}
UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP)
return TypeMismatch(TEXT("graph"), TEXT("Blueprint or Material"));
TArray<UEdGraph*> Matches = WingUtils::AllGraphsNamed(BP, Value);
if (Matches.Num() == 0)
{
UWingServer::Printf(TEXT("ERROR: Graph '%s' not found in %s\n"), *Value, *BP->GetName());
return SetError();
}
if (Matches.Num() > 1)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous graph '%s' in %s — %d matches\n"), *Value, *BP->GetName(), Matches.Num());
return SetError();
}
SetObj(Matches[0]);
return *this;
}
WingFetcher& WingFetcher::Node(const FString& Value)
{
if (bError) return *this;
// If current object is a graph, search that graph
if (UEdGraph* G = ::Cast<UEdGraph>(Obj))
{
UEdGraphNode* Found = nullptr;
for (UEdGraphNode* N : G->Nodes)
{
if (!N || !WingUtils::Identifies(Value, N))
continue;
if (Found)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous node '%s' in graph %s\n"), *Value, *G->GetName());
return SetError();
}
Found = N;
}
if (!Found)
{
UWingServer::Printf(TEXT("ERROR: Node '%s' not found in graph %s\n"), *Value, *G->GetName());
return SetError();
}
SetObj(Found);
return *this;
}
// If current object is a blueprint, search all graphs
if (UBlueprint* BP = ::Cast<UBlueprint>(Obj))
{
UEdGraphNode* Found = nullptr;
for (UEdGraph* G : WingUtils::AllGraphs(BP))
{
for (UEdGraphNode* N : G->Nodes)
{
if (!N || !WingUtils::Identifies(Value, N))
continue;
if (Found)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous node '%s' in %s\n"), *Value, *BP->GetName());
return SetError();
}
Found = N;
}
}
if (!Found)
{
UWingServer::Printf(TEXT("ERROR: Node '%s' not found in %s\n"), *Value, *BP->GetName());
return SetError();
}
SetObj(Found);
return *this;
}
return TypeMismatch(TEXT("node"), TEXT("graph or Blueprint"));
}
WingFetcher& WingFetcher::Pin(const FString& Value)
{
if (bError) return *this;
UEdGraphNode* N = ::Cast<UEdGraphNode>(Obj);
if (!N)
return TypeMismatch(TEXT("pin"), TEXT("node"));
UEdGraphPin* Found = nullptr;
for (UEdGraphPin *P : N->Pins)
{
if (!WingUtils::Identifies(Value, P))
continue;
if (Found)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous pin '%s' on node %s\n"),
*Value, *WingUtils::FormatName(N));
return SetError();
}
Found = P;
}
if (!Found)
{
UWingServer::Printf(TEXT("ERROR: Pin '%s' not found on node %s\n"),
*Value, *WingUtils::FormatName(N));
return SetError();
}
SetPin(Found);
return *this;
}
WingFetcher& WingFetcher::Component(const FString& Value)
{
if (bError) return *this;
UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP)
return TypeMismatch(TEXT("component"), TEXT("Blueprint"));
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
UWingServer::Printf(TEXT("ERROR: Blueprint %s has no SimpleConstructionScript (not an Actor Blueprint)\n"), *BP->GetName());
return SetError();
}
FName SearchName(*Value);
for (USCS_Node* SCSNode : SCS->GetAllNodes())
{
if (SCSNode && SCSNode->GetVariableName() == SearchName)
{
SetObj(SCSNode->ComponentTemplate);
return *this;
}
}
UWingServer::Printf(TEXT("ERROR: Component '%s' not found in %s\n"), *Value, *BP->GetName());
return SetError();
}
WingFetcher& WingFetcher::LevelBlueprint(const FString& Value)
{
if (bError) return *this;
UWorld* World = ::Cast<UWorld>(Obj);
if (!World)
return TypeMismatch(TEXT("levelblueprint"), TEXT("World"));
if (!World->PersistentLevel)
{
UWingServer::Print(TEXT("ERROR: World has no PersistentLevel\n"));
return SetError();
}
ULevelScriptBlueprint* LevelBP = World->PersistentLevel->GetLevelScriptBlueprint(true);
if (!LevelBP)
{
UWingServer::Print(TEXT("ERROR: World has no level blueprint\n"));
return SetError();
}
SetObj(LevelBP);
return *this;
}

View File

@@ -0,0 +1,107 @@
#include "WingFunctionArgs.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_FunctionResult.h"
#include "K2Node_Tunnel.h"
#include "WingTypes.h"
#include "WingServer.h"
bool WingFunctionArgs::HasArgs(UEdGraphNode* Node)
{
UK2Node_EditablePinBase* Editable = Cast<UK2Node_EditablePinBase>(Node);
if (!Editable) return false;
return Editable->IsEditable();
}
FString WingFunctionArgs::GetArgs(UEdGraphNode* Node)
{
UK2Node_EditablePinBase* Editable = Cast<UK2Node_EditablePinBase>(Node);
if (!Editable) return FString();
TStringBuilder<256> SB;
for (const TSharedPtr<FUserPinInfo>& Pin : Editable->UserDefinedPins)
{
if (SB.Len() > 0) SB << TEXT(", ");
SB << UWingTypes::TypeToText(Pin->PinType) << TEXT(" ") << Pin->PinName.ToString();
}
return FString(SB);
}
EEdGraphPinDirection WingFunctionArgs::GetPinDirection(UK2Node_EditablePinBase* Node)
{
// FunctionResult takes inputs; Tunnel depends on its flags; everything else outputs.
if (Node->IsA<UK2Node_FunctionResult>())
return EGPD_Input;
if (UK2Node_Tunnel* Tunnel = Cast<UK2Node_Tunnel>(Node))
return Tunnel->bCanHaveInputs ? EGPD_Input : EGPD_Output;
return EGPD_Output;
}
bool WingFunctionArgs::ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs)
{
FString Trimmed = Args.TrimStartAndEnd();
if (Trimmed.IsEmpty()) return true;
TArray<FString> Parts;
Trimmed.ParseIntoArray(Parts, TEXT(","));
for (const FString& Part : Parts)
{
FString Token = Part.TrimStartAndEnd();
if (Token.IsEmpty()) continue;
// Split "type name" on the last space.
int32 LastSpace;
if (!Token.FindLastChar(TEXT(' '), LastSpace))
{
UWingServer::Printf(TEXT("ERROR: Expected 'type name' but got '%s'\n"), *Token);
return false;
}
FString TypeStr = Token.Left(LastSpace).TrimStartAndEnd();
FString NameStr = Token.Mid(LastSpace + 1).TrimStartAndEnd();
if (TypeStr.IsEmpty() || NameStr.IsEmpty())
{
UWingServer::Printf(TEXT("ERROR: Expected 'type name' but got '%s'\n"), *Token);
return false;
}
FParsedArg Arg;
if (!UWingTypes::TextToType(TypeStr, Arg.PinType)) return false;
Arg.PinName = FName(*NameStr);
OutArgs.Add(MoveTemp(Arg));
}
return true;
}
bool WingFunctionArgs::SetArgs(UEdGraphNode* Node, const FString& Args)
{
UK2Node_EditablePinBase* Editable = Cast<UK2Node_EditablePinBase>(Node);
if (!Editable || !Editable->IsEditable())
{
UWingServer::Printf(TEXT("ERROR: Node does not support editable pins\n"));
return false;
}
// Parse the args string.
TArray<FParsedArg> NewArgs;
if (!ParseArgs(Args, NewArgs)) return false;
EEdGraphPinDirection Direction = GetPinDirection(Editable);
// Replace the UserDefinedPins array directly.
Editable->UserDefinedPins.Empty();
for (const FParsedArg& Arg : NewArgs)
{
TSharedPtr<FUserPinInfo> PinInfo = MakeShareable(new FUserPinInfo());
PinInfo->PinName = Arg.PinName;
PinInfo->PinType = Arg.PinType;
PinInfo->DesiredPinDirection = Direction;
Editable->UserDefinedPins.Add(PinInfo);
}
// ReconstructNode rebuilds real pins from UserDefinedPins
// and rewires old connections by matching pin names.
Editable->ReconstructNode();
return true;
}

View File

@@ -0,0 +1,377 @@
#include "WingGraphExport.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node_Knot.h"
#include "EdGraphNode_Comment.h"
#include "K2Node_VariableGet.h"
#include "K2Node_CallFunction.h"
#include "K2Node_FunctionEntry.h"
#include "WingFunctionArgs.h"
#include "MaterialGraph/MaterialGraphNode.h"
WingGraphExport::WingGraphExport(UEdGraph* InGraph)
: Graph(InGraph)
{
SortNodes();
EmitLocalVariables();
EmitDetails();
EmitGraph();
EmitComments();
}
WingGraphExport::WingGraphExport(UEdGraphNode* InNode)
: Graph(InNode->GetGraph())
{
SortedNodes.Add(InNode);
Visited.Add(InNode);
EmitLocalVariables();
EmitDetails();
EmitGraph();
EmitComments();
}
////////////////////////////////////////////////////////
//
// General utilities for manipulating UEdGraph nodes.
//
////////////////////////////////////////////////////////
UEdGraphPin* WingGraphExport::GetLinkedTo(UEdGraphPin* Pin)
{
while (true)
{
if (Pin == nullptr) return nullptr;
if (Pin->LinkedTo.IsEmpty()) return nullptr;
UEdGraphPin *LinkedTo = Pin->LinkedTo[0];
if (!LinkedTo->GetOwningNode()->IsA<UK2Node_Knot>()) return LinkedTo;
Pin = FindFirstPin(LinkedTo->GetOwningNode(), Pin->Direction);
}
}
bool WingGraphExport::IsDefaultToSelf(UEdGraphPin* Pin)
{
// Only valid call-function nodes can have default-to-self.
UK2Node_CallFunction* CallNode = Cast<UK2Node_CallFunction>(Pin->GetOwningNode());
if (!CallNode) return false;
UFunction* Function = CallNode->GetTargetFunction();
if (!Function) return false;
// In a call function node, the pin name 'self' is reserved
// for the C++ 'this' pointer. This always has 'DefaultToSelf'
// behavior. Note that 'self' in other nodes is not special.
if (Pin->PinName == UEdGraphSchema_K2::PN_Self) return true;
// For any other pin name, we have to check for
// the presence of meta = (DefaultToSelf = "Pin")
const FString& DefaultToSelfPinName = Function->GetMetaData(FBlueprintMetadata::MD_DefaultToSelf);
return Pin->PinName.ToString() == DefaultToSelfPinName;
}
TArray<UEdGraphPin*> WingGraphExport::FilterPins(UEdGraphNode* Node, EEdGraphPinDirection Direction, FName Category)
{
TArray<UEdGraphPin*> Result;
for (UEdGraphPin* Pin : Node->Pins)
{
if (Direction != EGPD_MAX && Pin->Direction != Direction) continue;
if (!Category.IsNone() && Pin->PinType.PinCategory != Category) continue;
Result.Add(Pin);
}
return Result;
}
bool WingGraphExport::HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
return FilterPins(Node, Direction, UEdGraphSchema_K2::PC_Exec).Num() > 0;
}
UEdGraphPin* WingGraphExport::FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction)
{
for (UEdGraphPin* Pin : Node->Pins)
{
if (Pin->Direction == Direction) return Pin;
}
return nullptr;
}
////////////////////////////////////////////////////////
//
// Traverse and Emit the Nodes.
//
////////////////////////////////////////////////////////
FString WingGraphExport::FormatPinSource(UEdGraphPin* Pin)
{
// If connected, show source node.pin
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
if (LinkedTo != nullptr)
{
UEdGraphNode* LinkedToNode = LinkedTo->GetOwningNode();
// For variable get nodes, just show the variable name.
if (UK2Node_VariableGet* VarGet = Cast<UK2Node_VariableGet>(LinkedToNode))
return WingUtils::FormatName(VarGet->VariableReference);
FString PinLabel = WingUtils::FormatName(LinkedTo);
return FString::Printf(TEXT("%s.%s"), *WingUtils::FormatName(LinkedToNode), *PinLabel);
}
// String pins: always show in quotes (even if empty).
FName Category = Pin->PinType.PinCategory;
if (Category == UEdGraphSchema_K2::PC_String ||
Category == UEdGraphSchema_K2::PC_Name ||
Category == UEdGraphSchema_K2::PC_Text)
{
return FString::Printf(TEXT("\"%s\""), *Pin->DefaultValue);
}
// If has a non-empty default, show it.
if (!Pin->DefaultValue.IsEmpty())
{
return Pin->DefaultValue;
}
if (Pin->DefaultObject)
{
return Pin->DefaultObject->GetName();
}
if (!Pin->DefaultTextValue.IsEmpty())
{
return FString::Printf(TEXT("\"%s\""), *Pin->DefaultTextValue.ToString());
}
if (IsDefaultToSelf(Pin))
{
return TEXT("<self>");
}
else
{
return TEXT("<default>");
}
}
void WingGraphExport::Traverse(UEdGraphNode* Node)
{
if (Visited.Contains(Node)) return;
Visited.Add(Node);
// First, traverse input nodes
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input))
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
Traverse(LinkedPin->GetOwningNode());
// Add this node to the sorted list.
SortedNodes.Add(Node);
// Then, traverse exec output nodes only.
// Data outputs are not followed — data nodes get pulled in
// through their consumers' input traversal.
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Output, UEdGraphSchema_K2::PC_Exec))
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
Traverse(LinkedPin->GetOwningNode());
}
void WingGraphExport::SortNodes()
{
// Find starter nodes: have exec output but no exec input.
TArray<UEdGraphNode*> Starters;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (HasExecPin(Node, EGPD_Output) && !HasExecPin(Node, EGPD_Input))
{
Starters.Add(Node);
}
}
// Sort starters by Y position.
Starters.Sort([](const UEdGraphNode& A, const UEdGraphNode& B)
{
return A.NodePosY < B.NodePosY;
});
// Traverse from each starter.
for (UEdGraphNode* Starter : Starters)
{
Traverse(Starter);
}
// Traverse all nodes.
for (UEdGraphNode* Node : Graph->Nodes)
{
Traverse(Node);
}
}
void WingGraphExport::EmitNode(UEdGraphNode* Node)
{
if (Node->IsA<UEdGraphNode_Comment>()) return;
Output.Appendf(TEXT("\nnode %s: %s\n"), *WingUtils::FormatName(Node), *WingUtils::FormatNodeTitle(Node));
// Emit function args (if applicable).
if (WingFunctionArgs::HasArgs(Node))
Output.Appendf(TEXT(" args %s\n"), *WingFunctionArgs::GetArgs(Node));
// Emit material expression properties (if applicable).
EmitMaterialProperties(Node, Output, true);
// Emit input data pins.
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Input))
{
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
if (Pin->bHidden) continue;
Output.Appendf(TEXT(" input %s %s = %s\n"),
*UWingTypes::TypeToText(Pin->PinType),
*WingUtils::FormatName(Pin),
*FormatPinSource(Pin));
}
// Emit output data pins as a return line.
FString ReturnPins;
for (UEdGraphPin* Pin : FilterPins(Node, EGPD_Output))
{
if (Pin->PinType.PinCategory == UEdGraphSchema_K2::PC_Exec) continue;
if (Pin->bHidden) continue;
if (!ReturnPins.IsEmpty()) ReturnPins += TEXT(", ");
ReturnPins += WingUtils::FormatName(Pin);
}
if (!ReturnPins.IsEmpty())
{
Output.Appendf(TEXT(" output-pins %s\n"), *ReturnPins);
}
// Emit output exec pins as goto statements.
TArray<UEdGraphPin*> ExecOuts = FilterPins(Node, EGPD_Output, UEdGraphSchema_K2::PC_Exec);
for (UEdGraphPin* Pin : ExecOuts)
{
UEdGraphPin* LinkedTo = GetLinkedTo(Pin);
if (!LinkedTo) continue;
FString Target = WingUtils::FormatName(LinkedTo->GetOwningNode());
if (ExecOuts.Num() == 1)
Output.Appendf(TEXT(" goto %s\n"), *Target);
else
Output.Appendf(TEXT(" goto-if %s %s\n"), *WingUtils::FormatName(Pin), *Target);
}
}
void WingGraphExport::EmitMaterialProperty(UMaterialExpression* Expression, FProperty* Prop, FStringBuilderBase& Out)
{
FString ValueStr = WingUtils::GetPropertyValueText(Expression, Prop);
ValueStr.ReplaceInline(TEXT("\r\n"), TEXT(" "));
ValueStr.ReplaceInline(TEXT("\n"), TEXT(" "));
if (ValueStr.Len() > 80)
ValueStr = ValueStr.Left(80) + TEXT("...");
bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst);
Out.Appendf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("mxeditable") : TEXT("mxreadonly"),
*UWingTypes::TypeToText(Prop),
*WingUtils::FormatName(Prop),
*ValueStr);
}
void WingGraphExport::EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary)
{
UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Node);
if (!MatNode || !MatNode->MaterialExpression) return;
UMaterialExpression* Expression = MatNode->MaterialExpression;
FString PrimaryCategory = Expression->GetClass()->GetName();
TArray<FProperty*> Props = WingUtils::SearchProperties(Expression, FString(), CPF_Edit, false);
for (FProperty* Prop : Props)
{
FString Category = Prop->HasMetaData(TEXT("Category")) ? Prop->GetMetaData(TEXT("Category")) : FString();
if ((Category == PrimaryCategory) == bPrimary)
EmitMaterialProperty(Expression, Prop, Out);
}
}
void WingGraphExport::EmitLocalVariables()
{
for (UEdGraphNode* Node : Graph->Nodes)
{
UK2Node_FunctionEntry* EntryNode = Cast<UK2Node_FunctionEntry>(Node);
if (!EntryNode) continue;
for (const FBPVariableDescription& Var : EntryNode->LocalVariables)
{
FString Default = Var.DefaultValue.IsEmpty() ? TEXT("<default>") : Var.DefaultValue;
Output.Appendf(TEXT("local %s %s = %s\n"),
*UWingTypes::TypeToText(Var.VarType),
*WingUtils::FormatName(Var),
*Default);
}
break;
}
}
void WingGraphExport::EmitGraph()
{
for (UEdGraphNode* Node : SortedNodes)
{
if (Node->IsA<UK2Node_Knot>()) continue;
if (Node->IsA<UK2Node_VariableGet>()) continue;
EmitNode(Node);
}
Output.Append(TEXT("\n"));
}
void WingGraphExport::EmitDetails()
{
for (UEdGraphNode* Node : SortedNodes)
{
if (Node->IsA<UK2Node_Knot>()) continue;
if (Node->IsA<UEdGraphNode_Comment>()) continue;
if (Node->IsA<UK2Node_VariableGet>()) continue;
Details.Appendf(TEXT("\ndetails %s\n"), *WingUtils::FormatName(Node));
Details.Appendf(TEXT(" pos %d, %d\n"), Node->NodePosX, Node->NodePosY);
EmitMaterialProperties(Node, Details, false);
}
}
void WingGraphExport::EmitComments()
{
for (UEdGraphNode* CommentNode : SortedNodes)
{
if (!CommentNode->IsA<UEdGraphNode_Comment>()) continue;
int32 CX = CommentNode->NodePosX;
int32 CY = CommentNode->NodePosY;
int32 CW = CommentNode->NodeWidth;
int32 CH = CommentNode->NodeHeight;
// Emit header.
Output.Appendf(TEXT("\ncomment %s:\n"), *WingUtils::FormatName(CommentNode));
// Emit wrapped, indented body.
Output.Append(WingUtils::WrapText(CommentNode->NodeComment, 70, TEXT(" - ")));
Output.Append(TEXT("\n"));
// Find contained nodes.
TArray<FString> ContainedNames;
for (UEdGraphNode* Node : SortedNodes)
{
if (Node->IsA<UEdGraphNode_Comment>()) continue;
int32 NX = Node->NodePosX;
int32 NY = Node->NodePosY;
if (NX >= CX && NY >= CY && NX <= CX + CW && NY <= CY + CH)
ContainedNames.Add(WingUtils::FormatName(Node));
}
if (ContainedNames.Num() > 0)
{
Output.Appendf(TEXT(" applies to: %s\n"), *FString::Join(ContainedNames, TEXT(", ")));
}
}
}

View File

@@ -0,0 +1,132 @@
#include "WingJson.h"
#include "WingTypes.h"
#include "WingServer.h"
#include "UObject/UnrealType.h"
#include "UObject/EnumProperty.h"
#include "Dom/JsonValue.h"
bool WingJson::PopulateFromJson(WingProperty& P, const FJsonObject* Json, bool AllOptional)
{
FString JsonKey = P.Prop->GetName();
bool bOptional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
if (!Json->HasField(JsonKey))
{
if (!bOptional)
{
UWingServer::Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
return false;
}
return true;
}
void* ValuePtr = P.Prop->ContainerPtrToValuePtr<void>(P.Container);
// Special handling for FWingJsonObject and FWingJsonArray
if (FStructProperty* StructProp = CastField<FStructProperty>(P.Prop))
{
if (StructProp->Struct == FWingJsonObject::StaticStruct())
{
if (!Json->HasTypedField<EJson::Object>(JsonKey))
{
UWingServer::Printf(TEXT("ERROR: '%s' must be an object\n"), *JsonKey);
return false;
}
static_cast<FWingJsonObject*>(ValuePtr)->Json = Json->GetObjectField(JsonKey);
return true;
}
if (StructProp->Struct == FWingJsonArray::StaticStruct())
{
if (!Json->HasTypedField<EJson::Array>(JsonKey))
{
UWingServer::Printf(TEXT("ERROR: '%s' must be an array\n"), *JsonKey);
return false;
}
static_cast<FWingJsonArray*>(ValuePtr)->Array = Json->GetArrayField(JsonKey);
return true;
}
}
// Handle based on JSON value type.
TSharedPtr<FJsonValue> JsonValue = Json->TryGetField(JsonKey);
if (JsonValue->Type == EJson::Number)
{
double D = JsonValue->AsNumber();
if (FIntProperty* IntProp = CastField<FIntProperty>(P.Prop))
{ IntProp->SetPropertyValue(ValuePtr, (int32)D); return true; }
if (FFloatProperty* FloatProp = CastField<FFloatProperty>(P.Prop))
{ FloatProp->SetPropertyValue(ValuePtr, (float)D); return true; }
if (FDoubleProperty* DoubleProp = CastField<FDoubleProperty>(P.Prop))
{ DoubleProp->SetPropertyValue(ValuePtr, D); return true; }
if (FByteProperty* ByteProp = CastField<FByteProperty>(P.Prop))
{ ByteProp->SetPropertyValue(ValuePtr, (uint8)D); return true; }
UWingServer::Printf(TEXT("ERROR: '%s' received a number but expects %s\n"), *JsonKey, *P.Prop->GetCPPType());
return false;
}
if (JsonValue->Type == EJson::Boolean)
{
if (FBoolProperty* BoolProp = CastField<FBoolProperty>(P.Prop))
{ BoolProp->SetPropertyValue(ValuePtr, JsonValue->AsBool()); return true; }
UWingServer::Printf(TEXT("ERROR: '%s' received a boolean but expects %s\n"), *JsonKey, *P.Prop->GetCPPType());
return false;
}
if (JsonValue->Type == EJson::String)
{
return P.SetText(JsonValue->AsString());
}
UWingServer::Printf(TEXT("ERROR: '%s' must be a string, number, or boolean\n"), *JsonKey);
return false;
}
bool WingJson::PopulateFromJson(
TArray<WingProperty>& Props, const FJsonObject* Json, bool AllOptional)
{
bool Ok = true;
// Build a set of known property names for the unknown-field check.
TSet<FString> KnownKeys;
for (const WingProperty& P : Props)
KnownKeys.Add(P.Prop->GetName());
// Check for unknown fields in the JSON
for (const auto& KV : Json->Values)
{
if (!KnownKeys.Contains(KV.Key))
{
UWingServer::Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
Ok = false;
}
}
// Populate each property from JSON
for (WingProperty& P : Props)
{
if (!PopulateFromJson(P, Json, AllOptional)) Ok = false;
}
return Ok;
}
bool WingJson::PopulateFromJson(
UStruct* StructType, void* Container, const FJsonObject* Json)
{
TArray<WingProperty> Props = WingProperty::GetAll(StructType, Container, (EPropertyFlags)0);
return PopulateFromJson(Props, Json);
}
bool WingJson::PopulateFromJson(
UStruct* StructType, void* Container,
const TSharedPtr<FJsonValue>& JsonValue)
{
if (!JsonValue.IsValid() || (JsonValue->Type != EJson::Object))
{
UWingServer::Print(TEXT("ERROR: Expected a JSON object\n"));
return false;
}
return PopulateFromJson(StructType, Container, JsonValue->AsObject().Get());
}

View File

@@ -0,0 +1,34 @@
#pragma once
#include "CoreMinimal.h"
class FLogCaptureOutputDevice : public FOutputDevice
{
public:
TArray<FString> CapturedErrors;
bool bEnabled = true;
void Install() { GLog->AddOutputDevice(this); }
void Uninstall() { GLog->RemoveOutputDevice(this); }
// If the device is marked 'CanBeUsedOnMultipleThreads,'
// then UE_LOG will call Serialize from the current
// thread, otherwise, it will call Serialize from the
// logging thread. Without this, we wouldn't be able to
// tell whether an error is coming from the game thread.
virtual bool CanBeUsedOnMultipleThreads() const override { return true; }
virtual void Serialize(const TCHAR* V, ELogVerbosity::Type Verbosity, const FName& Category) override
{
// Only capture messages from the game thread.
// Other threads generate noise we don't care about.
if (!IsInGameThread() || !bEnabled) return;
if (Verbosity == ELogVerbosity::Warning ||
Verbosity == ELogVerbosity::Error ||
Verbosity == ELogVerbosity::Fatal)
{
CapturedErrors.Add(FString(V));
}
}
};

View File

@@ -0,0 +1,78 @@
#include "WingMaterialParameter.h"
#include "WingUtils.h"
#include "WingServer.h"
TMap<FMaterialParameterInfo, FMaterialParameterMetadata> WingMaterialParameter::GetMaterialParameters(UMaterialInterface* Material)
{
TMap<FMaterialParameterInfo, FMaterialParameterMetadata> Result;
if (!Material) return Result;
TMap<FMaterialParameterInfo, FMaterialParameterMetadata> Temp;
for (int32 i = 0; i < (int32)EMaterialParameterType::NumRuntime; i++)
{
Material->GetAllParametersOfType((EMaterialParameterType)i, Temp);
Result.Append(Temp);
}
return Result;
}
bool WingMaterialParameter::ParseMaterialParameterAssociation(const FString& Str, EMaterialParameterAssociation& OutAssociation)
{
if (Str.Equals(TEXT("Global"), ESearchCase::IgnoreCase))
OutAssociation = GlobalParameter;
else if (Str.Equals(TEXT("Layer"), ESearchCase::IgnoreCase))
OutAssociation = LayerParameter;
else if (Str.Equals(TEXT("Blend"), ESearchCase::IgnoreCase))
OutAssociation = BlendParameter;
else
{
UWingServer::Printf(TEXT("ERROR: Invalid ParameterAssociation '%s' (expected 'Global', 'Layer', or 'Blend')\n"), *Str);
return false;
}
return true;
}
void WingMaterialParameter::FormatMaterialParameter(const FMaterialParameterInfo& Info, const FMaterialParameterMetadata& Meta)
{
// Association prefix for layer/blend parameters.
FString Prefix;
if (Info.Association == LayerParameter)
Prefix = FString::Printf(TEXT("[Layer %d] "), Info.Index);
else if (Info.Association == BlendParameter)
Prefix = FString::Printf(TEXT("[Blend %d] "), Info.Index);
switch (Meta.Value.Type)
{
case EMaterialParameterType::Scalar:
UWingServer::Printf(TEXT(" %sScalar \"%s\" = %g\n"), *Prefix, *Info.Name.ToString(), Meta.Value.AsScalar());
break;
case EMaterialParameterType::Vector:
{
FLinearColor C = Meta.Value.AsLinearColor();
UWingServer::Printf(TEXT(" %sVector \"%s\" = (R=%.3f, G=%.3f, B=%.3f, A=%.3f)\n"),
*Prefix, *Info.Name.ToString(), C.R, C.G, C.B, C.A);
break;
}
case EMaterialParameterType::DoubleVector:
{
FVector4d V = Meta.Value.AsVector4d();
UWingServer::Printf(TEXT(" %sDoubleVector \"%s\" = (%.3f, %.3f, %.3f, %.3f)\n"),
*Prefix, *Info.Name.ToString(), V.X, V.Y, V.Z, V.W);
break;
}
case EMaterialParameterType::Texture:
{
UTexture* Tex = Cast<UTexture>(Meta.Value.AsTextureObject());
UWingServer::Printf(TEXT(" %sTexture \"%s\" = %s\n"),
*Prefix, *Info.Name.ToString(), Tex ? *WingUtils::FormatName(Tex) : TEXT("None"));
break;
}
case EMaterialParameterType::StaticSwitch:
UWingServer::Printf(TEXT(" %sStaticSwitch \"%s\" = %s\n"),
*Prefix, *Info.Name.ToString(), Meta.Value.AsStaticSwitch() ? TEXT("true") : TEXT("false"));
break;
default:
UWingServer::Printf(TEXT(" %sType%d \"%s\"\n"), *Prefix, (int)Meta.Value.Type, *Info.Name.ToString());
break;
}
}

View File

@@ -0,0 +1,4 @@
#include "WingModule.h"
#include "Modules/ModuleManager.h"
IMPLEMENT_MODULE(FWingModule, UEWingman);

View File

@@ -0,0 +1,11 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
class FWingModule : public IModuleInterface
{
public:
virtual void StartupModule() override {}
virtual void ShutdownModule() override {}
};

View File

@@ -0,0 +1,61 @@
#include "WingNotifier.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraph.h"
#include "Engine/Blueprint.h"
#include "Materials/Material.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "MaterialEditingLibrary.h"
void WingNotifier::AddTouchedObject(UObject* Obj)
{
if (!Obj) return;
bool bAlreadyInSet = false;
TouchedSet.Add(Obj, &bAlreadyInSet);
if (bAlreadyInSet) return;
TouchedArray.Add(Obj);
Obj->PreEditChange(nullptr);
}
void WingNotifier::SendNotifications()
{
TSet<UEdGraphNode*> Nodes;
TSet<UEdGraph*> Graphs;
TSet<UMaterial*> Materials;
TSet<UBlueprint*> Blueprints;
for (int32 i = TouchedArray.Num() - 1; i >= 0; --i)
{
UObject* Obj = TouchedArray[i];
Obj->PostEditChange();
Obj->MarkPackageDirty();
if (UEdGraphNode* Node = ::Cast<UEdGraphNode>(Obj))
Nodes.Add(Node);
if (UEdGraph* Graph = ::Cast<UEdGraph>(Obj))
Graphs.Add(Graph);
if (UBlueprint* BP = ::Cast<UBlueprint>(Obj))
Blueprints.Add(BP);
if (UMaterialInterface* MatIface = ::Cast<UMaterialInterface>(Obj))
if (UMaterial* BaseMat = MatIface->GetMaterial())
Materials.Add(BaseMat);
}
for (UEdGraphNode* Node : Nodes)
Node->ReconstructNode();
for (UEdGraph* Graph : Graphs)
Graph->NotifyGraphChanged();
for (UMaterial *Material : Materials)
UMaterialEditingLibrary::RebuildMaterialInstanceEditors(Material);
for (UBlueprint *Blueprint : Blueprints)
FBlueprintEditorUtils::MarkBlueprintAsStructurallyModified(Blueprint);
// FBlueprintEditorUtils::RefreshAllNodes(BP);
// FKismetEditorUtilities::CompileBlueprint(BP);
if (GEditor)
GEditor->RedrawAllViewports();
TouchedSet.Empty();
TouchedArray.Empty();
}

View File

@@ -0,0 +1,194 @@
#include "WingProperty.h"
#include "WingUtils.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "Engine/Blueprint.h"
#include "Materials/MaterialExpression.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "EdGraph/EdGraphPin.h"
#include "UObject/EnumProperty.h"
static bool IsPinTypeProperty(FProperty* Prop)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
return StructProp && StructProp->Struct == FEdGraphPinType::StaticStruct();
}
WingProperty::WingProperty(FProperty* InProp, void* InContainer)
: Prop(InProp), Container(InContainer) {}
FString WingProperty::GetText() const
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
if (IsPinTypeProperty(Prop))
return UWingTypes::TypeToText(*static_cast<FEdGraphPinType*>(ValuePtr));
FString Result;
Prop->ExportTextItem_Direct(Result, ValuePtr, nullptr, nullptr, PPF_None);
return Result;
}
bool WingProperty::TryParseEnum(UEnum* Enum, const FString& Text, int64 &OutValue)
{
int Index = Enum->GetIndexByNameString(Text);
if (Index == INDEX_NONE)
{
FString Prefix = Enum->GenerateEnumPrefix();
if (!Prefix.IsEmpty())
{
Index = Enum->GetIndexByNameString(Prefix + TEXT("_") + Text);
}
}
if (Index == INDEX_NONE)
{
UWingServer::Printf(TEXT("ERROR: '%s' is not a valid value for %s\n"),
*Text, *Enum->GetName());
OutValue = 0;
return false;
}
else
{
OutValue = Enum->GetValueByIndex(Index);
return true;
}
}
bool WingProperty::TrySetText(const FString &Value)
{
void* ValuePtr = Prop->ContainerPtrToValuePtr<void>(Container);
// Pin types get parsed by UWingTypes.
if (IsPinTypeProperty(Prop))
return UWingTypes::TextToType(Value, *static_cast<FEdGraphPinType*>(ValuePtr));
// Byte Enum types get parsed by TryParseEnum, above.
if (FByteProperty* ByteProp = CastField<FByteProperty>(Prop))
{
if (UEnum* Enum = ByteProp->Enum)
{
int64 EnumValue;
if (!TryParseEnum(Enum, Value, EnumValue)) return false;
ByteProp->SetPropertyValue(ValuePtr, (uint8)EnumValue);
return true;
}
}
// Regular Enum types get parsed by TryParseEnum, above.
if (FEnumProperty* EnumProp = CastField<FEnumProperty>(Prop))
{
int64 EnumValue;
if (!TryParseEnum(EnumProp->GetEnum(), Value, EnumValue)) return false;
EnumProp->GetUnderlyingProperty()->SetIntPropertyValue(ValuePtr, EnumValue);
return true;
}
// Non-enum properties use ImportText
const TCHAR* Result = Prop->ImportText_Direct(*Value, ValuePtr, nullptr, PPF_None);
if (!Result)
{
UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s' (type: %s)\n"),
*Value, *WingUtils::FormatName(Prop), *Prop->GetCPPType());
return false;
}
return true;
}
bool WingProperty::SetText(const FString& Value)
{
if (!TrySetText(Value)) return false;
if (Prop->GetOwnerClass()->IsChildOf(UMaterialExpression::StaticClass()))
{
UMaterialExpression* Expr = static_cast<UMaterialExpression*>(Container);
Expr->ForcePropertyValueChanged(Prop);
}
return true;
}
void WingProperty::Collect(UStruct* StructType, void* Container, TArray<WingProperty> &Props, EPropertyFlags Flags)
{
for (TFieldIterator<FProperty> It(StructType); It; ++It)
{
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
Props.Emplace(*It, Container);
}
}
void WingProperty::Remove(TArray<WingProperty>& Props, const FString& Name)
{
Props.RemoveAll([&](const WingProperty& P) { return P.Prop->GetName() == Name; });
}
TArray<WingProperty> WingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
{
if (!Obj) return {};
TArray<WingProperty> Result;
// Blueprints don't have editable properties. So
// instead, we fetch properties from the generated CDO,
// which is probably what the user intended.
//
if (UBlueprint *BP = ::Cast<UBlueprint>(Obj))
{
if (BP->GeneratedClass == nullptr)
{
UWingServer::Printf(TEXT("ERROR: Blueprint '%s' has no GeneratedClass\n"), *Obj->GetName());
return {};
}
Obj = BP->GeneratedClass->GetDefaultObject();
}
Collect(Obj->GetClass(), Obj, Result, Flags);
// If it's a Material Graph node, also collect properties from
// the associated material expression.
//
if (UMaterialGraphNode* MatNode = Cast<UMaterialGraphNode>(Obj))
{
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{
Collect(Expr->GetClass(), Expr, Result, Flags);
}
}
return Result;
}
TArray<WingProperty> WingProperty::GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags)
{
TArray<WingProperty> Result;
Collect(StructType, Container, Result, Flags);
return Result;
}
TArray<WingProperty> WingProperty::FindAllSubstring(const TArray<WingProperty>& Props, const FString& Substring)
{
if (Substring.IsEmpty()) return Props;
TArray<WingProperty> Result;
for (const WingProperty& P : Props)
{
if (WingUtils::FormatName(P.Prop).Contains(Substring, ESearchCase::IgnoreCase))
Result.Add(P);
}
return Result;
}
WingProperty WingProperty::FindOneExactMatch(const TArray<WingProperty>& Props, const FString& Name)
{
TArray<WingProperty> Matches;
for (const WingProperty& P : Props)
{
if (WingUtils::Identifies(Name, P.Prop))
Matches.Add(P);
}
if (Matches.Num() == 0)
{
UWingServer::Printf(TEXT("ERROR: Property '%s' not found\n"), *Name);
return WingProperty();
}
if (Matches.Num() > 1)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous property '%s'\n"), *Name);
return WingProperty();
}
return Matches[0];
}

View File

@@ -0,0 +1,447 @@
#include "WingServer.h"
#include "WingHandler.h"
#include "WingJson.h"
#include "WingLogCapture.h"
#include "WingUtils.h"
#include "UObject/StrongObjectPtr.h"
#include "Materials/MaterialExpression.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.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 "EdGraph/EdGraphPin.h"
#include "EdGraphSchema_K2.h"
#include "K2Node.h"
#include "K2Node_CallFunction.h"
#include "K2Node_Event.h"
#include "K2Node_CustomEvent.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_EditablePinBase.h"
#include "K2Node_VariableGet.h"
#include "K2Node_VariableSet.h"
#include "K2Node_BreakStruct.h"
#include "K2Node_MakeStruct.h"
#include "K2Node_MacroInstance.h"
#include "K2Node_DynamicCast.h"
#include "K2Node_CallParentFunction.h"
#include "K2Node_IfThenElse.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "Dom/JsonValue.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Serialization/JsonSerializer.h"
#include "Interfaces/IPv4/IPv4Address.h"
#include "Interfaces/IPv4/IPv4Endpoint.h"
#include "SocketSubsystem.h"
#include "Sockets.h"
#include "Async/Async.h"
#include "UObject/SavePackage.h"
#include "Misc/Paths.h"
#include "Misc/FileHelper.h"
#include "Misc/Guid.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "UObject/UObjectIterator.h"
#include "Misc/PackageName.h"
#include "UObject/LinkerLoad.h"
#include "Engine/UserDefinedEnum.h"
#include "Editor.h"
#include "Materials/Material.h"
#include "Materials/MaterialInstanceConstant.h"
#include "Materials/MaterialFunction.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/MaterialExpressionConstant2Vector.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/MaterialExpressionAppendVector.h"
#include "Materials/MaterialExpressionAdd.h"
#include "Materials/MaterialExpressionMultiply.h"
#include "Materials/MaterialExpressionLinearInterpolate.h"
#include "Materials/MaterialExpressionClamp.h"
#include "Materials/MaterialExpressionOneMinus.h"
#include "Materials/MaterialExpressionPower.h"
#include "Materials/MaterialExpressionTime.h"
#include "Materials/MaterialExpressionWorldPosition.h"
#include "Materials/MaterialExpressionFunctionInput.h"
#include "Materials/MaterialExpressionFunctionOutput.h"
#include "Materials/MaterialExpressionMaterialFunctionCall.h"
#include "MaterialGraph/MaterialGraph.h"
#include "MaterialGraph/MaterialGraphNode.h"
#include "MaterialGraph/MaterialGraphSchema.h"
// Animation Blueprint support
#include "Animation/AnimBlueprint.h"
#include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/Skeleton.h"
#include "AnimGraphNode_StateMachine.h"
#include "AnimGraphNode_AssetPlayerBase.h"
#include "AnimGraphNode_SequencePlayer.h"
#include "AnimGraphNode_BlendSpacePlayer.h"
#include "AnimGraphNode_Base.h"
#include "AnimStateNode.h"
#include "AnimStateTransitionNode.h"
#include "AnimStateConduitNode.h"
#include "AnimStateEntryNode.h"
#include "AnimationStateMachineGraph.h"
#include "AnimationGraph.h"
#include "AnimationTransitionGraph.h"
UWingServer* UWingServer::GWingServer = nullptr;
// ============================================================
// Initialization and Shutdown
// ============================================================
void UWingServer::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
GWingServer = this;
// Create TCP listen socket
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
ListenSocket = SocketSub->CreateSocket(NAME_Stream, TEXT("WingServer"), false);
if (!ListenSocket)
{
UE_LOG(LogTemp, Error, TEXT("UEWingman: Failed to create listen socket"));
return;
}
ListenSocket->SetReuseAddr(true);
ListenSocket->SetNonBlocking(true);
TSharedRef<FInternetAddr> Addr = SocketSub->CreateInternetAddr();
bool bIsValid = false;
Addr->SetIp(TEXT("127.0.0.1"), bIsValid);
Addr->SetPort(Port);
if (!ListenSocket->Bind(*Addr))
{
UE_LOG(LogTemp, Error, TEXT("UEWingman: Failed to bind to port %d"), Port);
SocketSub->DestroySocket(ListenSocket);
ListenSocket = nullptr;
return;
}
if (!ListenSocket->Listen(4))
{
UE_LOG(LogTemp, Error, TEXT("UEWingman: Failed to listen on port %d"), Port);
SocketSub->DestroySocket(ListenSocket);
ListenSocket = nullptr;
return;
}
BuildWingHandlerRegistry();
LogCapture.bEnabled = false;
LogCapture.Install();
bRunning = true;
UE_LOG(LogTemp, Display, TEXT("UEWingman: MCP server listening on tcp://localhost:%d"), Port);
}
void UWingServer::Deinitialize()
{
if (!bRunning)
{
Super::Deinitialize();
return;
}
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
// Set shutdown flag and drain pending messages under lock
{
FScopeLock Lock(&Mutex);
bShuttingDown = true;
for (auto& Msg : PendingMessages)
{
Msg->Response.SetValue(FString());
}
PendingMessages.Empty();
}
// Close all client sockets (unblocks their blocking reads)
for (auto& Client : Clients)
{
if (Client->Socket)
{
Client->Socket->Close();
}
}
// Wait for client threads to exit
for (auto& Client : Clients)
{
Client->ThreadFuture.Wait();
if (Client->Socket)
{
SocketSub->DestroySocket(Client->Socket);
}
}
Clients.Empty();
// Close listen socket
if (ListenSocket)
{
ListenSocket->Close();
SocketSub->DestroySocket(ListenSocket);
ListenSocket = nullptr;
}
LogCapture.Uninstall();
bRunning = false;
bShuttingDown = false;
GWingServer = nullptr;
UE_LOG(LogTemp, Display, TEXT("UEWingman: Server stopped."));
Super::Deinitialize();
}
// ============================================================
// FTickableEditorObject interface
// ============================================================
void UWingServer::Tick(float DeltaTime)
{
// Accept new connections (non-blocking)
AcceptNewConnections();
// Clean up finished client threads
CleanupFinishedClients();
// Dequeue one pending message
TSharedPtr<FPendingMessage> Request;
{
FScopeLock Lock(&Mutex);
if (PendingMessages.Num() > 0)
{
Request = PendingMessages[0];
PendingMessages.RemoveAt(0);
}
}
// If we have a request, process it.
if (Request.IsValid())
{
FString Response = HandleRequest(Request->Line);
Request->Response.SetValue(Response);
}
}
void UWingServer::TickServer(float DeltaTime)
{
if (GWingServer) GWingServer->Tick(DeltaTime);
}
bool UWingServer::IsTickable() const
{
return bRunning;
}
TStatId UWingServer::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(UWingServer, STATGROUP_Tickables);
}
// ============================================================
// HandleRequest — Given a command, execute it.
// ============================================================
FString UWingServer::HandleRequest(const FString& Line)
{
LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true;
HandlerOutput.Reset();
TryCallHandler(Line);
Notifier.SendNotifications();
LogCapture.bEnabled = false;
for (const FString& Msg : LogCapture.CapturedErrors)
{
UWingServer::Printf(TEXT("UE_LOG: %s\n"), *Msg);
}
LogCapture.CapturedErrors.Empty();
FString Result = HandlerOutput.ToString();
HandlerOutput.Reset();
for (int32 i = 0; i < Result.Len(); ++i)
{
if (Result[i] == TEXT('\0')) Result[i] = TEXT(' ');
}
return Result;
}
void UWingServer::TryCallHandler(const FString &Line)
{
// Turn the request string into a JSON tree.
TSharedPtr<FJsonObject> Request;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
FJsonSerializer::Deserialize(Reader, Request);
if (!Request.IsValid())
{
UWingServer::Printf(TEXT("Request is not valid JSON"));
return;
}
// Extract the command from the request.
FString Command;
if (!Request->TryGetStringField(TEXT("command"), Command))
{
UWingServer::Printf(TEXT("Request does not contain 'command' parameter"));
return;
}
Request->RemoveField(TEXT("command"));
// Find the handler UClass for the specified command.
UClass** HandlerClass = WingHandlerRegistry.Find(Command);
if (!HandlerClass)
{
UWingServer::Printf(TEXT("Unknown command: %s"), *Command);
return;
}
// Make an object of the handler class.
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
IWingHandler* Handler = Cast<IWingHandler>(HandlerObj.Get());
// Populate the handler object with the request parameters.
if (!WingJson::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request))
{
UWingServer::Printf(TEXT("\nUsage:\n\n"));
WingUtils::FormatCommandHelp(*HandlerClass);
return;
}
// Invoke the handler.
Handler->Handle();
}
// ============================================================
// Connection Maintenance
// ============================================================
void UWingServer::AcceptNewConnections()
{
if (!ListenSocket) return;
bool bHasPending = false;
if (!ListenSocket->HasPendingConnection(bHasPending) || !bHasPending) return;
FSocket* ClientSocket = ListenSocket->Accept(TEXT("MCPClient"));
if (!ClientSocket) return;
ClientSocket->SetNonBlocking(false); // client threads use blocking I/O
TSharedPtr<FClientConnection> Client = MakeShared<FClientConnection>();
Client->Socket = ClientSocket;
Client->ThreadFuture = Async(EAsyncExecution::Thread, [this, Client]() { ClientThreadFunc(this, Client); });
Clients.Add(Client);
UE_LOG(LogTemp, Display, TEXT("UEWingman: Client connected."));
}
void UWingServer::CleanupFinishedClients()
{
ISocketSubsystem* SocketSub = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM);
for (int32 i = Clients.Num() - 1; i >= 0; --i)
{
if (!Clients[i]->bDone) continue;
Clients[i]->ThreadFuture.Wait();
if (Clients[i]->Socket)
{
SocketSub->DestroySocket(Clients[i]->Socket);
}
Clients.RemoveAt(i);
}
}
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
{
FSocket* Socket = Client->Socket;
FString LineBuffer;
uint8 RecvBuf[4096];
while (true)
{
int32 BytesRead = 0;
if (!Socket->Recv(RecvBuf, sizeof(RecvBuf) - 1, BytesRead))
{
break; // socket error or closed
}
if (BytesRead <= 0)
{
break; // connection closed
}
RecvBuf[BytesRead] = 0;
LineBuffer += UTF8_TO_TCHAR((const ANSICHAR*)RecvBuf);
// Process complete lines
int32 NewlineIdx;
while (LineBuffer.FindChar(TEXT('\n'), NewlineIdx))
{
FString Line = LineBuffer.Left(NewlineIdx).TrimEnd();
LineBuffer.RightChopInline(NewlineIdx + 1);
if (Line.IsEmpty()) continue;
// Wait for the asset registry to finish its initial scan.
{
IAssetRegistry& AR = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry").Get();
while (AR.IsLoadingAssets()) FPlatformProcess::Sleep(0.25f);
}
// Enqueue the line for game-thread processing
TSharedPtr<UWingServer::FPendingMessage> Msg = MakeShared<UWingServer::FPendingMessage>();
Msg->Line = Line;
TFuture<FString> Future = Msg->Response.GetFuture();
{
FScopeLock Lock(&Server->Mutex);
if (Server->bShuttingDown)
{
Client->bDone = true;
return;
}
Server->PendingMessages.Add(Msg);
}
// Block until the game thread processes this message
FString Response = Future.Get();
// Write the response back, null-terminated (blocking)
FTCHARToUTF8 Utf8(*Response);
int32 BytesSent = 0;
Socket->Send((const uint8*)Utf8.Get(), Utf8.Length() + 1, BytesSent);
}
}
Client->bDone = true;
UE_LOG(LogTemp, Display, TEXT("UEWingman: Client disconnected."));
}
// ============================================================
// BuildWingHandlerRegistry
// ============================================================
void UWingServer::BuildWingHandlerRegistry()
{
for (UClass* Class : WingUtils::CollectHandlerClasses())
{
WingHandlerRegistry.FindOrAdd(WingUtils::GetHandlerName(Class)) = Class;
}
}

View File

@@ -0,0 +1,278 @@
#include "WingToolMenu.h"
#include "ToolMenuEntry.h"
#include "ToolMenuDelegates.h"
#include "ToolMenuContext.h"
#include "ToolMenus.h"
#include "WingUtils.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraphSchema_K2.h"
#include "Framework/Commands/UIAction.h"
// ============================================================
// Private member access via template explicit-instantiation loophole.
//
// The C++ standard says "the usual access checking rules do not
// apply to names used to specify explicit instantiations." So
// &FToolMenuEntry::Action is legal as a template argument in an
// explicit instantiation, even though Action is private.
//
// The WingPrivateAccessor template captures the member pointer and exposes it
// through a friend function that we can call from normal code.
//
// See: https://bloglitb.blogspot.com/2011/12/access-to-private-members-safer.html
// ============================================================
template<typename Tag, typename Tag::type M>
struct WingPrivateAccessor
{
friend typename Tag::type GetPtr(Tag) { return M; }
};
// ----- FToolMenuEntry::Action -----
struct Tag_FToolMenuEntry_Action
{
using type = FToolUIActionChoice FToolMenuEntry::*;
friend type GetPtr(Tag_FToolMenuEntry_Action);
};
template struct WingPrivateAccessor<Tag_FToolMenuEntry_Action, &FToolMenuEntry::Action>;
static const FToolUIActionChoice& GetAction(const FToolMenuEntry& Entry)
{
return Entry.*GetPtr(Tag_FToolMenuEntry_Action());
}
// ----- FToolMenuEntry::Command -----
struct Tag_FToolMenuEntry_Command
{
using type = TSharedPtr<const FUICommandInfo> FToolMenuEntry::*;
friend type GetPtr(Tag_FToolMenuEntry_Command);
};
template struct WingPrivateAccessor<Tag_FToolMenuEntry_Command, &FToolMenuEntry::Command>;
static bool HasCommand(const FToolMenuEntry& Entry)
{
return Entry.*GetPtr(Tag_FToolMenuEntry_Command()) != nullptr;
}
// ============================================================
// Given a menu entry label, and a pin name (possibly empty),
// generate a better label for an LLM to type. The goal here
// is to have consistent spacing, consistent casing, so that
// the LLM can easily remember what to type.
// ============================================================
FText WingToolMenu::MakeBetterLabel(const UEdGraphPin *Pin, const FText &EntryLabel)
{
FString Sanitized = EntryLabel.ToString();
int32 Dst = 0;
bool Upper = true;
for (int32 Src = 0; Src < Sanitized.Len(); Src++)
{
TCHAR c = Sanitized[Src];
if (FChar::IsAlnum(c))
{
if (Upper) c = FChar::ToUpper(c);
Sanitized[Dst++] = c;
Upper = false;
}
else
{
Upper = true;
if ((c <= 0x20)||(c == 0x7F)) continue;
if (c == ':') c = '-';
Sanitized[Dst++] = c;
}
}
Sanitized.LeftInline(Dst);
if (Pin)
{
Sanitized = FString::Printf(TEXT("Pin:%s:%s"), *WingUtils::FormatName(Pin), *Sanitized);
}
return FText::FromString(Sanitized);
}
// ============================================================
// Check if an array of entries contains a specific label.
// ============================================================
bool WingToolMenu::ContainsText(const TArray<FText> &Texts, const FText &Value)
{
for (const FText &Text : Texts)
{
if (Value.IdenticalTo(Text))
{
return true;
}
}
return false;
}
// ============================================================
// AddEntry — create a synthetic menu entry with a direct action.
// ============================================================
void WingToolMenu::AddEntry(TArray<FToolMenuEntry>& Entries, UEdGraphPin* Pin,
const TCHAR* Label, FCanExecuteAction CanExec, FExecuteAction Exec)
{
if (!CanExec.Execute())
return;
FToolMenuEntry Entry = FToolMenuEntry::InitMenuEntry(
NAME_None,
MakeBetterLabel(Pin, FText::FromString(Label)),
FText::GetEmpty(),
FSlateIcon(),
FUIAction(MoveTemp(Exec), MoveTemp(CanExec)));
Entries.Add(MoveTemp(Entry));
}
void WingToolMenu::AddSyntheticEntries(TArray<FToolMenuEntry> &Entries, UEdGraphNode *NodePtr)
{
const UEdGraphSchema_K2 *K2Schema = Cast<UEdGraphSchema_K2>(NodePtr->GetSchema());
if (K2Schema == nullptr) return;
// TWeakObjectPtr<UEdGraphNode> Node(NodePtr);
for (UEdGraphPin *PinPtr : NodePtr->Pins)
{
if (PinPtr->bHidden) continue;
FEdGraphPinReference Pin(PinPtr);
AddEntry(Entries, PinPtr, TEXT("SplitStructPin"),
[=](){ UEdGraphPin *P=Pin.Get(); return P && K2Schema->CanSplitStructPin(*P); },
[=](){ UEdGraphPin *P=Pin.Get(); if (P) K2Schema->SplitPin(P); });
AddEntry(Entries, PinPtr, TEXT("RecombineStructPin"),
[=](){ UEdGraphPin *P=Pin.Get(); return P && K2Schema->CanRecombineStructPin(*P); },
[=](){ UEdGraphPin *P=Pin.Get(); if (P) K2Schema->RecombinePin(P); });
}
}
// ============================================================
// Get the Menu Items for a given node and pin. This doesn't
// do anything with the labels yet.
// ============================================================
TArray<FToolMenuEntry> WingToolMenu::GetMenuItems(
UGraphNodeContextMenuContext* GNC, const FToolMenuContext &TMC)
{
TArray<FToolMenuEntry> Result;
UToolMenu* Menu = NewObject<UToolMenu>();
GNC->Node->GetNodeContextMenuActions(Menu, GNC);
//GNC->Node->GetSchema()->GetContextMenuActions(Menu, GNC);
for (FToolMenuSection& Section : Menu->Sections)
{
Result.Append(Section.Blocks);
}
return Result;
}
TArray<FToolMenuEntry> WingToolMenu::GetMenuItems(UEdGraphNode *Node, const FToolMenuContext &Context)
{
// Create the two context objects.
TArray<FToolMenuEntry> Result;
UGraphNodeContextMenuContext* GNC = NewObject<UGraphNodeContextMenuContext>();
// Fetch the menu items for the node.
GNC->Init(Node->GetGraph(), Node, nullptr, false);
TArray<FToolMenuEntry> NodeEntries = GetMenuItems(GNC, Context);
// Improve the labels for the node entries, and also
// record the original labels.
TArray<FText> OriginalLabels;
for (FToolMenuEntry &Entry: NodeEntries)
{
FText Label = Entry.Label.Get();
OriginalLabels.Add(Label);
if (!CanExecute(Entry, Context)) continue;
Entry.Label = MakeBetterLabel(nullptr, Label);
Result.Add(Entry);
}
// Fetch the Menu items for the pins. Discard
// pins whose original label exactly matches the
// original label of a node entry.
for (const UEdGraphPin *Pin : Node->Pins)
{
if (Pin->bHidden) continue;
FString PinName = WingUtils::FormatName(Pin);
GNC->Init(Node->GetGraph(), Node, Pin, false);
TArray<FToolMenuEntry> PinEntries = GetMenuItems(GNC, Context);
for (FToolMenuEntry &PinEntry : PinEntries)
{
FText Label = PinEntry.Label.Get();
if (!ContainsText(OriginalLabels, Label))
{
if (CanExecute(PinEntry, Context))
{
PinEntry.Label = MakeBetterLabel(Pin, Label);
Result.Add(PinEntry);
}
}
}
}
AddSyntheticEntries(Result, Node);
return Result;
}
// ============================================================
// Menu entry resolution
//
// We only handle the three Action-based callback mechanisms:
// 1. FToolUIAction — delegates that take a FToolMenuContext
// 2. FToolDynamicUIAction — same, Blueprint-friendly variant
// 3. FUIAction — plain delegates, no context needed
//
// Command-based entries are skipped — they depend on editor
// selection/focus state which we can't reliably provide.
// ============================================================
bool WingToolMenu::CanExecute(const FToolMenuEntry& Entry, const FToolMenuContext& Context)
{
if (HasCommand(Entry))
return false;
const FToolUIActionChoice& Choice = GetAction(Entry);
if (const FToolUIAction* ToolAction = Choice.GetToolUIAction())
{
if (ToolAction->CanExecuteAction.IsBound())
return ToolAction->CanExecuteAction.Execute(Context);
return ToolAction->ExecuteAction.IsBound();
}
if (const FToolDynamicUIAction* DynamicAction = Choice.GetToolDynamicUIAction())
{
if (DynamicAction->CanExecuteAction.IsBound())
return DynamicAction->CanExecuteAction.Execute(Context);
return DynamicAction->ExecuteAction.IsBound();
}
if (const FUIAction* Action = Choice.GetUIAction())
return Action->IsBound() && Action->CanExecute();
return false;
}
bool WingToolMenu::Execute(const FToolMenuEntry& Entry, const FToolMenuContext& Context)
{
if (HasCommand(Entry))
return false;
const FToolUIActionChoice& Choice = GetAction(Entry);
if (const FToolUIAction* ToolAction = Choice.GetToolUIAction())
{
ToolAction->ExecuteAction.ExecuteIfBound(Context);
return true;
}
if (const FToolDynamicUIAction* DynamicAction = Choice.GetToolDynamicUIAction())
{
DynamicAction->ExecuteAction.ExecuteIfBound(Context);
return true;
}
if (const FUIAction* Action = Choice.GetUIAction())
return Action->Execute();
return false;
}

View File

@@ -0,0 +1,524 @@
#include "WingTypes.h"
#include "WingServer.h"
#include "Editor.h"
#include "EdGraphSchema_K2.h"
#include "Engine/Blueprint.h"
#include "UObject/UObjectIterator.h"
// ---------------------------------------------------------------------------
// Choose Short Name
// ---------------------------------------------------------------------------
FString UWingTypes::GetNameWithoutUnderscoreC(const UObject *Obj)
{
FString Name = Obj->GetName();
if (Name.EndsWith(TEXT("_C")))
{
if (const UClass* Class = Cast<UClass>(Obj))
{
if (Class->ClassGeneratedBy != nullptr)
Name.LeftChopInline(2);
}
}
return Name;
}
void UWingTypes::ReserveShortName(FName Name)
{
FString NameStr = Name.ToString();
ShortToPath.Add(NameStr.ToLower(), FString(TEXT("PRIMITIVE")));
}
FString UWingTypes::ChooseShortName(const UObject* Obj)
{
if (!Cast<UScriptStruct>(Obj) && !Cast<UClass>(Obj) && !Cast<UEnum>(Obj))
return FString();
FString Path = Obj->GetPathName();
FString *OldShort = PathToShort.Find(Path);
if (OldShort != nullptr) return *OldShort;
FString Name = GetNameWithoutUnderscoreC(Obj);
FString Lower = Name.ToLower();
if (!ShortToPath.Contains(Lower))
{
ShortToPath.Add(Lower, Path);
PathToShort.Add(Path, Name);
return Name;
}
for (int32 i = 2; ; ++i)
{
FString NumberedLower = FString::Printf(TEXT("%s%d"), *Lower, i);
if (!ShortToPath.Contains(NumberedLower))
{
FString NumberedName = FString::Printf(TEXT("%s_%d"), *Name, i);
ShortToPath.Add(NumberedLower, Path);
PathToShort.Add(Path, NumberedName);
return NumberedName;
}
}
}
void UWingTypes::ChooseShortNames(UPackage* Package)
{
if (Package == nullptr) return;
ForEachObjectWithPackage(Package, [&](UObject* Obj)
{
ChooseShortName(Obj);
return true;
}, false);
}
// ---------------------------------------------------------------------------
// TypeToText
// ---------------------------------------------------------------------------
FString UWingTypes::TypeToTextInner(FName Category, FName SubCategory, UObject* SubCategoryObject)
{
if ((Category == UEdGraphSchema_K2::PC_Boolean) ||
(Category == UEdGraphSchema_K2::PC_Int) ||
(Category == UEdGraphSchema_K2::PC_Int64) ||
(Category == UEdGraphSchema_K2::PC_Name) ||
(Category == UEdGraphSchema_K2::PC_String) ||
(Category == UEdGraphSchema_K2::PC_Text))
{
return Category.ToString();
}
if (Category == UEdGraphSchema_K2::PC_Real)
{
return SubCategory.ToString();
}
if (Category == UEdGraphSchema_K2::PC_Byte)
{
if (SubCategoryObject)
return ChooseShortName(SubCategoryObject);
return Category.ToString();
}
if (Category == UEdGraphSchema_K2::PC_Enum)
{
if (SubCategoryObject)
return ChooseShortName(SubCategoryObject);
return FString();
}
if (SubCategoryObject)
{
FString Short = ChooseShortName(SubCategoryObject);
if (Short.IsEmpty()) return FString();
if (Category == UEdGraphSchema_K2::PC_Struct)
return Short;
if (Category == UEdGraphSchema_K2::PC_Object)
return Short;
if (Category == UEdGraphSchema_K2::PC_Class)
return FString::Printf(TEXT("Class<%s>"), *Short);
if (Category == UEdGraphSchema_K2::PC_SoftObject)
return FString::Printf(TEXT("Soft<%s>"), *Short);
if (Category == UEdGraphSchema_K2::PC_SoftClass)
return FString::Printf(TEXT("SoftClass<%s>"), *Short);
if (Category == UEdGraphSchema_K2::PC_Interface)
return Short;
}
return FString();
}
FString UWingTypes::TypeToText(const FEdGraphPinType& PinType)
{
UWingTypes* Types = GEditor->GetEditorSubsystem<UWingTypes>();
if (!Types) return FString();
FString Inner = Types->TypeToTextInner(PinType.PinCategory, PinType.PinSubCategory, PinType.PinSubCategoryObject.Get());
if (Inner.IsEmpty())
return FString();
if (PinType.IsArray())
return FString::Printf(TEXT("Array<%s>"), *Inner);
if (PinType.IsSet())
return FString::Printf(TEXT("Set<%s>"), *Inner);
if (PinType.IsMap())
{
FString ValueInner = Types->TypeToTextInner(
PinType.PinValueType.TerminalCategory,
PinType.PinValueType.TerminalSubCategory,
PinType.PinValueType.TerminalSubCategoryObject.Get());
if (ValueInner.IsEmpty())
return FString();
return FString::Printf(TEXT("Map<%s, %s>"), *Inner, *ValueInner);
}
return Inner;
}
FString UWingTypes::TypeToText(const FProperty *Property)
{
FEdGraphPinType PinType;
if (!GetDefault<UEdGraphSchema_K2>()->ConvertPropertyToPinType(Property, PinType))
{
return TEXT("void");
}
else
{
return TypeToText(PinType);
}
}
FString UWingTypes::TypeToText(const UObject* Obj)
{
UWingTypes* Types = GEditor->GetEditorSubsystem<UWingTypes>();
if (!Types) return FString();
return Types->ChooseShortName(Obj);
}
// ---------------------------------------------------------------------------
// Subsystem lifecycle
// ---------------------------------------------------------------------------
void UWingTypes::Initialize(FSubsystemCollectionBase& Collection)
{
Super::Initialize(Collection);
// Collect all packages and sort by name for stable short-name assignment.
TArray<UPackage*> Packages;
for (TObjectIterator<UPackage> It; It; ++It)
Packages.Add(*It);
Packages.Sort([](const UPackage& A, const UPackage& B) { return A.GetName() < B.GetName(); });
// Reserve the short names of the primitives.
ReserveShortName(UEdGraphSchema_K2::PC_Boolean);
ReserveShortName(UEdGraphSchema_K2::PC_Int);
ReserveShortName(UEdGraphSchema_K2::PC_Int64);
ReserveShortName(UEdGraphSchema_K2::PC_Float);
ReserveShortName(UEdGraphSchema_K2::PC_Double);
ReserveShortName(UEdGraphSchema_K2::PC_Byte);
ReserveShortName(UEdGraphSchema_K2::PC_Name);
ReserveShortName(UEdGraphSchema_K2::PC_String);
ReserveShortName(UEdGraphSchema_K2::PC_Text);
// Scan priority packages first, then everything else in sorted order.
ChooseShortNames(FindPackage(nullptr, TEXT("/Script/CoreUObject")));
ChooseShortNames(FindPackage(nullptr, TEXT("/Script/Engine")));
ChooseShortNames(FindPackage(nullptr, TEXT("/Script/Integration")));
// Choose short names for everything else.
for (UPackage* Pkg : Packages)
ChooseShortNames(Pkg);
UE_LOG(LogTemp, Display, TEXT("WingTypes: Registered %d types"), ShortToPath.Num());
}
void UWingTypes::Deinitialize()
{
Super::Deinitialize();
}
// ---------------------------------------------------------------------------
// Tokenizer
// ---------------------------------------------------------------------------
void UWingTypes::Tokenize(const FString& Input)
{
Tokens.Empty();
Cursor = 0;
int32 i = 0;
while (i < Input.Len())
{
TCHAR Ch = Input[i];
// Skip whitespace.
if (FChar::IsWhitespace(Ch))
{
++i;
continue;
}
// Try to parse an identifier.
int32 Start = i;
while (i < Input.Len() && (FChar::IsAlnum(Input[i]) || Input[i] == '_'))
++i;
if (i > Start)
{
Tokens.Add(Input.Mid(Start, i - Start));
continue;
}
// Anything that's not an identifier or whitespace
// gets classified as a single-character token.
Tokens.Add(FString(1, &Ch));
++i;
}
}
// ---------------------------------------------------------------------------
// Path to Object Conversion
// ---------------------------------------------------------------------------
bool UWingTypes::ResolvePath(const FString &Name, const FString &Path, FEdGraphPinType &OutType)
{
// Load the object.
UObject* Obj = LoadObject<UObject>(nullptr, *Path);
if (!Obj)
{
Error = FString::Printf(TEXT("Failed to load type '%s' at path '%s'"), *Name, *Path);
return false;
}
// If it's a blueprint, use its generated class.
if (UBlueprint* BP = Cast<UBlueprint>(Obj))
{
Obj = BP->GeneratedClass;
if (!Obj)
{
Error = FString::Printf(TEXT("Blueprint '%s' has no generated class"), *Name);
return false;
}
}
// Determine the category from the object type.
if (Cast<UScriptStruct>(Obj))
{
OutType.PinCategory = UEdGraphSchema_K2::PC_Struct;
}
else if (UClass* Class = Cast<UClass>(Obj))
{
if (Class->IsChildOf(UInterface::StaticClass()))
OutType.PinCategory = UEdGraphSchema_K2::PC_Interface;
else
OutType.PinCategory = UEdGraphSchema_K2::PC_Object;
}
else if (Cast<UEnum>(Obj))
{
OutType.PinCategory = UEdGraphSchema_K2::PC_Byte;
}
else
{
// This really shouldn't happen.
Error = FString::Printf(TEXT("'%s' is not a struct, class, enum, or interface"), *Name);
return false;
}
OutType.PinSubCategoryObject = Obj;
return true;
}
// ---------------------------------------------------------------------------
// Parsing Types
// ---------------------------------------------------------------------------
bool UWingTypes::TokenIs(const TCHAR* Text) const
{
if (Cursor >= Tokens.Num()) return false;
return Tokens[Cursor].Equals(Text, ESearchCase::IgnoreCase);
}
bool UWingTypes::TokenIs(const TCHAR* Text, TCHAR Next) const
{
if (Cursor >= Tokens.Num() - 1) return false;
return (Tokens[Cursor].Equals(Text, ESearchCase::IgnoreCase)) &&
(Tokens[Cursor+1].Len() == 1) &&
(Tokens[Cursor+1][0] == Next);
}
bool UWingTypes::TokenIs(TCHAR Next) const
{
if (Cursor >= Tokens.Num()) return false;
return (Tokens[Cursor].Len() == 1) &&
(Tokens[Cursor][0] == Next);
}
bool UWingTypes::TokenIsID() const
{
if (Cursor >= Tokens.Num()) return false;
return FChar::IsAlnum(Tokens[Cursor][0]);
}
bool UWingTypes::ParseEOF()
{
if (Cursor != Tokens.Num())
{
Error = TEXT("Extra tokens at end of input");
return false;
}
return true;
}
bool UWingTypes::ParseChar(TCHAR c)
{
if (!TokenIs(c))
{
Error = FString::Printf(TEXT("Expected %c"), c);
return false;
}
Cursor++;
return true;
}
bool UWingTypes::ParsePlainIdentifier(FEdGraphPinType& OutType)
{
if (!TokenIsID())
{
Error = TEXT("Expected Identifier");
return false;
}
FString Name = Tokens[Cursor++];
FString *Path = ShortToPath.Find(Name.ToLower());
if (Path == nullptr)
{
Error = TEXT("Unrecognized Type");
return false;
}
if (*Path == TEXT("PRIMITIVE"))
{
OutType.PinCategory = FName(*Name);
if ((OutType.PinCategory == UEdGraphSchema_K2::PC_Double) ||
(OutType.PinCategory == UEdGraphSchema_K2::PC_Float))
{
OutType.PinSubCategory = OutType.PinCategory;
OutType.PinCategory = UEdGraphSchema_K2::PC_Real;
}
return true;
}
else
{
return ResolvePath(Name, *Path, OutType);
}
}
bool UWingTypes::ParseWrapped(FName Wrapper, FEdGraphPinType& OutType)
{
Cursor++;
if (!ParseChar('<')) return false;
if (!ParsePlainIdentifier(OutType)) return false;
if (!Cast<UClass>(OutType.PinSubCategoryObject))
{
Error = FString::Printf(TEXT("%s is not a Class"), *OutType.PinSubCategoryObject->GetName());
return false;
}
if (!ParseChar('>')) return false;
OutType.PinCategory = Wrapper;
return true;
}
bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType)
{
if (TokenIs(TEXT("Soft"), '<'))
{
return ParseWrapped(UEdGraphSchema_K2::PC_SoftObject, OutType);
}
else if (TokenIs(TEXT("Class"), '<'))
{
return ParseWrapped(UEdGraphSchema_K2::PC_Class, OutType);
}
else if (TokenIs(TEXT("SoftClass"), '<'))
{
return ParseWrapped(UEdGraphSchema_K2::PC_SoftClass, OutType);
}
else return ParsePlainIdentifier(OutType);
}
bool UWingTypes::ParseArrayOrSet(FEdGraphPinType& OutType)
{
Cursor++;
if (!ParseChar('<')) return false;
if (!ParseMaybeWrapped(OutType)) return false;
if (!ParseChar('>')) return false;
return true;
}
bool UWingTypes::ParseMap(FEdGraphPinType& OutType)
{
Cursor++;
if (!ParseChar('<')) return false;
if (!ParsePlainIdentifier(OutType)) return false;
if (!ParseChar(',')) return false;
FEdGraphPinType ValueType;
if (!ParseMaybeWrapped(ValueType)) return false;
OutType.PinValueType.TerminalCategory = ValueType.PinCategory;
OutType.PinValueType.TerminalSubCategory = ValueType.PinSubCategory;
OutType.PinValueType.TerminalSubCategoryObject = ValueType.PinSubCategoryObject;
if (!ParseChar('>')) return false;
return true;
}
bool UWingTypes::ParseType(FEdGraphPinType& OutType)
{
if (TokenIs(TEXT("Array"), '<'))
{
OutType.ContainerType = EPinContainerType::Array;
if (!ParseArrayOrSet(OutType)) return false;
}
else if (TokenIs(TEXT("Set"), '<'))
{
OutType.ContainerType = EPinContainerType::Set;
if (!ParseArrayOrSet(OutType)) return false;
}
else if (TokenIs(TEXT("Map"), '<'))
{
OutType.ContainerType = EPinContainerType::Map;
if (!ParseMap(OutType)) return false;
}
else
{
if (!ParseMaybeWrapped(OutType)) return false;
}
if (!ParseEOF()) return false;
return true;
}
FString UWingTypes::TryTextToType(const FString& Text, FEdGraphPinType& OutPinType)
{
UWingTypes* Types = GEditor->GetEditorSubsystem<UWingTypes>();
check(Types);
Types->Error.Empty();
Types->Tokenize(Text);
OutPinType = FEdGraphPinType();
if (!Types->ParseType(OutPinType)) return Types->Error;
return FString();
}
bool UWingTypes::TextToType(const FString& Text, FEdGraphPinType& OutPinType)
{
FString Error = TryTextToType(Text, OutPinType);
if (Error.IsEmpty()) return true;
UWingServer::Print(Error);
return false;
}
UClass* UWingTypes::TextToOneObjectType(const FString& Text)
{
FEdGraphPinType PinType;
if (!TextToType(Text, PinType)) return nullptr;
UClass* Class = Cast<UClass>(PinType.PinSubCategoryObject.Get());
if ((!Class) || (PinType.PinCategory != UEdGraphSchema_K2::PC_Object) ||
(PinType.IsContainer()))
{
UWingServer::Printf(TEXT("ERROR: '%s' is not a plain object class\n"), *Text);
return nullptr;
}
return Class;
}
UClass* UWingTypes::TextToOneInterfaceType(const FString& Text)
{
FEdGraphPinType PinType;
if (!TextToType(Text, PinType)) return nullptr;
UClass* Class = Cast<UClass>(PinType.PinSubCategoryObject.Get());
if ((!Class) || (PinType.PinCategory != UEdGraphSchema_K2::PC_Interface) ||
(PinType.IsContainer()))
{
UWingServer::Printf(TEXT("ERROR: '%s' is not an interface class\n"), *Text);
return nullptr;
}
return Class;
}

View File

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

View File

@@ -0,0 +1,58 @@
#pragma once
#include "CoreMinimal.h"
#include "WingProperty.h"
#include "Engine/Blueprint.h"
#include "WingBlueprintVar.generated.h"
// Editor-friendly view of a blueprint variable's properties.
// Wraps an FBPVariableDescription, exposing commonly-used flags
// and metadata as simple UPROPERTYs that the property system can
// populate from JSON.
USTRUCT()
struct FBlueprintVar
{
GENERATED_BODY()
FBPVariableDescription* Desc = nullptr;
WingProperty DefaultValueProp;
FBlueprintVar() = default;
FBlueprintVar(UBlueprint* BP, const FString& VarName);
bool NotFound() const { return Desc == nullptr; }
UPROPERTY(EditAnywhere, meta=(Optional, Description="Default value in Unreal text format"))
FString DefaultValue;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Variable description/tooltip"))
FString Description;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Allow editing on instances"))
bool InstanceEditable = false;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Read-only in blueprints"))
bool BlueprintReadOnly = false;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Expose as a pin when spawning"))
bool ExposeOnSpawn = false;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Private to this blueprint"))
bool Private = false;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Expose to cinematics/sequencer"))
bool ExposeToCinematics = false;
// Load from Desc, populate from JSON, save back to Desc.
bool ApplyJson(const FJsonObject* Json);
// Print all properties and their current values.
void Dump();
private:
void LoadFlags();
void LoadDefault();
void SaveFlags();
bool SaveDefault();
TArray<WingProperty> MergedProperties();
};

View File

@@ -0,0 +1,21 @@
#pragma once
#include "CoreMinimal.h"
#include "Commandlets/Commandlet.h"
#include "WingCommandlet.generated.h"
/**
* Commandlet that keeps the engine alive so the UEWingman editor subsystem
* can serve MCP requests without the full editor UI.
*
* Usage: UnrealEditor-Cmd.exe Project.uproject -run=UEWingman
*/
UCLASS()
class UWingCommandlet : public UCommandlet
{
GENERATED_BODY()
public:
UWingCommandlet();
virtual int32 Main(const FString& Params) override;
};

View File

@@ -0,0 +1,148 @@
#pragma once
#include "CoreMinimal.h"
#include "WingUtils.h"
class UEdGraphPin;
class IAssetEditorInstance;
struct FWalker;
// WingFetcher: Load an Asset and find an object within it.
// To find an object, you use a path. This is typical:
//
// F.Walk(TEXT("/Game/Mat/M_Test,graph,node:Param_1"))
//
// A path always starts from an asset name. The path above
// starts at a material asset, then it walks to the material
// graph, then from there to a specific graph node.
//
// Instead of specifying the path as a string, you can also
// specify it using a sequence of procedural steps, like
// this:
//
// F.Asset(TEXT("/Game/Materials/M_Test"));
// F.Graph();
// F.Node(TEXT("Param_1"));
//
// When you're finally at the object you want, you usually
// use the Cast method to get a pointer to the object.
//
// If any step fails, the WingFetcher will print an error
// message that can be seen by the MCP's caller. It will
// also set an error flag. Once the error flag is set, all
// further ops become no-ops. After that point, fetching
// any data will return nullptr.
//
class WingFetcher
{
public:
// Walk a path from an asset to an object
// within that asset. If you call walk a
// second time, it will walk additional steps.
//
WingFetcher& Walk(const FString& Path);
// Walk a path using individual path
// steps instead of a path. All these steps generate
// errors if they cannot find the desired element.
//
WingFetcher& Asset(const FString& PackagePath);
WingFetcher& Graph(const FString& Value);
WingFetcher& Node(const FString& Value);
WingFetcher& Pin(const FString& Value);
WingFetcher& Component(const FString& Value);
WingFetcher& LevelBlueprint(const FString& Value);
// Return true if there haven't been any errors.
// Note that errors always automatically generate
// output to WingServer::Printf.
//
bool Ok() const { return !bError; }
// Try to fetch the current object as a UObject of
// the specified type. If it isn't one, generates an
// error and returns nullptr.
//
template<class T> T *Cast()
{
if (bError) return nullptr;
T* Result = ::Cast<T>(Obj);
if (Result == nullptr) PathFailed(*T::StaticClass()->GetName());
return Result;
}
// Get the current object as a UObject if it is one,
// otherwise nullptr. Does not generate errors.
//
UObject* GetObj() const { return Obj; }
// Get the asset from where it all began: the first
// step in the walk path. If the asset couldn't be
// loaded, returns nullptr. Does not generate errors.
//
UObject* GetAsset() const { return OriginalAsset; }
// Get the asset from where it all began: the first
// step in the walk path, as a specified type. Errors
// if it cannot cast to the specified type.
//
template<class T> T* CastAsset()
{
if (!CheckAssetIsA(T::StaticClass())) return nullptr;
return ::Cast<T>(OriginalAsset);
}
// When an asset is loaded, an editor is automatically
// opened. Get the editor. You must specify the type
// that you expect the asset to be, and the type to cast
// the editor to. Does not generate errors.
//
template<class AssetType, class EditorType>
EditorType* CastEditor()
{
if (!CheckAssetIsA(AssetType::StaticClass())) return nullptr;
return static_cast<EditorType*>(Editor);
}
// Initialize empty. You need to call Asset, or walk
// a path that starts with an asset.
//
WingFetcher() {}
// Initialize with an object. From there, you can walk
// to sub-objects.
//
WingFetcher(UObject* O) : Obj(O) {}
private:
// The Current Object. Only one of these can be non-null.
UObject* Obj = nullptr;
UEdGraphPin* ResultPin = nullptr;
// The Starting Asset and the Editor we Opened.
UObject* OriginalAsset = nullptr;
IAssetEditorInstance* Editor = nullptr;
// True if an error has occurred.
bool bError = false;
// Internal methods.
using WalkFunc = WingFetcher& (WingFetcher::*)(const FString&);
void SetObj(UObject* InObj);
void SetPin(UEdGraphPin* InPin);
WingFetcher& SetError();
void PathFailed(const TCHAR *Kind);
WingFetcher& TypeMismatch(const TCHAR* Walker, const TCHAR* Expected);
bool CheckAssetIsA(UClass* StaticClass);
WalkFunc GetWalker(const FString &Step);
};
template<> inline UEdGraphPin* WingFetcher::Cast<UEdGraphPin>()
{
if (bError) return nullptr;
if (!ResultPin) PathFailed(TEXT("UEdGraphPin"));
return ResultPin;
}

View File

@@ -0,0 +1,37 @@
#pragma once
#include "CoreMinimal.h"
class UEdGraphNode;
class UK2Node_EditablePinBase;
struct FEdGraphPinType;
struct WingFunctionArgs
{
// Returns true if the node is an EditablePinBase subclass that
// actually supports user-defined pins (FunctionEntry, FunctionResult,
// CustomEvent, or Tunnel).
static bool HasArgs(UEdGraphNode* Node);
// Returns the user-defined pins as a string like "int x, float y".
static FString GetArgs(UEdGraphNode* Node);
// Sets the user-defined pins from a string like "int x, float y".
// Returns true on success.
static bool SetArgs(UEdGraphNode* Node, const FString& Args);
private:
// A parsed argument: type + name.
struct FParsedArg
{
FEdGraphPinType PinType;
FName PinName;
};
// Parse "int x, float y" into an array of FParsedArg.
// Returns false and prints an error on failure.
static bool ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs);
// Determine the pin direction for user-defined pins on this node.
static EEdGraphPinDirection GetPinDirection(UK2Node_EditablePinBase* Node);
};

View File

@@ -0,0 +1,89 @@
#pragma once
#include "CoreMinimal.h"
#include "Engine/Blueprint.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "EdGraph/EdGraphPin.h"
class UMaterialExpression;
class WingGraphExport
{
public:
WingGraphExport(UEdGraph* InGraph);
WingGraphExport(UEdGraphNode* InNode);
const FString GetOutput() { return Output.ToString(); }
const FString GetDetails() { return Details.ToString(); }
private:
////////////////////////////////////////////////////////
//
// General utilities for manipulating UEdGraph nodes.
//
////////////////////////////////////////////////////////
// Get the pin that this pin is linked to. If the
// pin is linked to multiple pins, returns the first.
// If the pin is linked to a knot node, follow the
// chain of knot nodes and find the pin at the other
// end of the chain. Returns nullptr if this pin
// is not linked to anything.
//
static UEdGraphPin* GetLinkedTo(UEdGraphPin *Pin);
// Return true if the pin in question defaults
// to self.
//
static bool IsDefaultToSelf(UEdGraphPin* Pin);
// Get a subset of the pins in the node, filtered
// by direction, category, or both.
//
static TArray<UEdGraphPin*> FilterPins(UEdGraphNode* Node,
EEdGraphPinDirection Direction = EGPD_MAX, FName Category = FName());
// Return true if the node has an exec pin that points
// in the specified direction.
//
static bool HasExecPin(UEdGraphNode* Node, EEdGraphPinDirection Direction);
// Find the first pin that points in the specified direction.
//
static UEdGraphPin* FindFirstPin(UEdGraphNode* Node, EEdGraphPinDirection Direction);
////////////////////////////////////////////////////////
//
// Traverse and Emit the Nodes.
//
////////////////////////////////////////////////////////
FString FormatPinSource(UEdGraphPin* Pin);
void Traverse(UEdGraphNode* Node);
void SortNodes();
void EmitNode(UEdGraphNode* Node);
void EmitMaterialProperty(UMaterialExpression* Expression, FProperty* Prop, FStringBuilderBase& Out);
void EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderBase& Out, bool bPrimary);
void EmitLocalVariables();
void EmitGraph();
void EmitDetails();
void EmitComments();
////////////////////////////////////////////////////////
//
// Values recorded during traversal.
//
////////////////////////////////////////////////////////
UEdGraph* Graph;
// Data populated by passes.
TArray<UEdGraphNode*> SortedNodes;
TSet<UEdGraphNode*> Visited;
// Output buffers.
TStringBuilder<4096> Output;
TStringBuilder<4096> Details;
};

View File

@@ -0,0 +1,55 @@
#pragma once
#include "CoreMinimal.h"
#include "UObject/Interface.h"
#include "UObject/Object.h"
#include "Dom/JsonObject.h"
#include "WingHandler.generated.h"
// Marker struct for handler parameters that accept a JSON object.
// PopulateFromJson stashes the actual JSON object into the Json field.
USTRUCT()
struct FWingJsonObject
{
GENERATED_BODY()
TSharedPtr<FJsonObject> Json;
};
// Marker struct for handler parameters that accept a JSON array.
// PopulateFromJson stashes the actual JSON array into the Array field.
USTRUCT()
struct FWingJsonArray
{
GENERATED_BODY()
TArray<TSharedPtr<FJsonValue>> Array;
};
// Interface for self-registering MCP tool handlers.
//
// Implementing classes declare their parameters as UPROPERTY fields, which are
// automatically populated from the incoming JSON request and used to
// generate the tool's JSON Schema for MCP tools/list.
//
// Class metadata:
// ToolName - the MCP tool name (e.g. "spawn_node")
//
// Property metadata:
// Optional - marks a parameter as not required
//
UINTERFACE(MinimalAPI, meta=(CannotImplementInterfaceInBlueprint))
class UWingHandler : public UInterface
{
GENERATED_BODY()
};
class UEWINGMAN_API IWingHandler
{
GENERATED_BODY()
public:
// Human-readable tool description for MCP tools/list.
virtual FString GetDescription() const = 0;
// Called after parameter fields have been populated from JSON.
virtual void Handle() {}
};

View File

@@ -0,0 +1,17 @@
#pragma once
#include "CoreMinimal.h"
#include "WingHandler.h"
#include "WingProperty.h"
#include "Dom/JsonObject.h"
// JSON utility functions used by MCP handlers.
// This is effectively a namespace — all methods are static.
class WingJson
{
public:
static bool PopulateFromJson(WingProperty& Prop, const FJsonObject* Json, bool AllOptional = false);
static bool PopulateFromJson(TArray<WingProperty>& Props, const FJsonObject* Json, bool AllOptional = false);
static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& JsonValue);
static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Json);
};

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