UE Wingman renaming complete.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
242
Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_Diff.h
Normal file
242
Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_Diff.h
Normal 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);
|
||||
}
|
||||
};
|
||||
107
Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_Dump.h
Normal file
107
Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_Dump.h
Normal 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));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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."));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
112
Plugins/UEWingman/Source/UEWingman/HalfBaked/Class_Search.h
Normal file
112
Plugins/UEWingman/Source/UEWingman/HalfBaked/Class_Search.h
Normal 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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
71
Plugins/UEWingman/Source/UEWingman/HalfBaked/Enum_Create.h
Normal file
71
Plugins/UEWingman/Source/UEWingman/HalfBaked/Enum_Create.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
99
Plugins/UEWingman/Source/UEWingman/HalfBaked/Struct_Create.h
Normal file
99
Plugins/UEWingman/Source/UEWingman/HalfBaked/Struct_Create.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
52
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Backup.h
Normal file
52
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Backup.h
Normal 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);
|
||||
}
|
||||
};
|
||||
127
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Delete.h
Normal file
127
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Delete.h
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
71
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Rename.h
Normal file
71
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Rename.h
Normal 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);
|
||||
}
|
||||
};
|
||||
75
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Restore.h
Normal file
75
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Restore.h
Normal 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);
|
||||
}
|
||||
};
|
||||
96
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Search.h
Normal file
96
Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Search.h
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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();
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
102
Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Create.h
Normal file
102
Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Create.h
Normal 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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
40
Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Dump.h
Normal file
40
Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_Dump.h
Normal 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());
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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());
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
54
Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h
Normal file
54
Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h
Normal 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."));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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));
|
||||
}
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
@@ -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"));
|
||||
}
|
||||
};
|
||||
98
Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump.h
Normal file
98
Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
46
Plugins/UEWingman/Source/UEWingman/Handlers/Property_Get.h
Normal file
46
Plugins/UEWingman/Source/UEWingman/Handlers/Property_Get.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
79
Plugins/UEWingman/Source/UEWingman/Handlers/Property_Set.h
Normal file
79
Plugins/UEWingman/Source/UEWingman/Handlers/Property_Set.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
85
Plugins/UEWingman/Source/UEWingman/Handlers/ShowCommands.h
Normal file
85
Plugins/UEWingman/Source/UEWingman/Handlers/ShowCommands.h
Normal 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"));
|
||||
}
|
||||
};
|
||||
104
Plugins/UEWingman/Source/UEWingman/Handlers/TypeName_Search.h
Normal file
104
Plugins/UEWingman/Source/UEWingman/Handlers/TypeName_Search.h
Normal 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"));
|
||||
}
|
||||
}
|
||||
};
|
||||
90
Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h
Normal file
90
Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h
Normal 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"
|
||||
));
|
||||
}
|
||||
};
|
||||
@@ -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.
|
||||
59
Plugins/UEWingman/Source/UEWingman/Private/HandlerIssues.md
Normal file
59
Plugins/UEWingman/Source/UEWingman/Private/HandlerIssues.md
Normal 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.
|
||||
|
||||
153
Plugins/UEWingman/Source/UEWingman/Private/WingBlueprintVar.cpp
Normal file
153
Plugins/UEWingman/Source/UEWingman/Private/WingBlueprintVar.cpp
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
346
Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp
Normal file
346
Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp
Normal 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;
|
||||
}
|
||||
|
||||
107
Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp
Normal file
107
Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp
Normal 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;
|
||||
}
|
||||
377
Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp
Normal file
377
Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp
Normal 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(", ")));
|
||||
}
|
||||
}
|
||||
}
|
||||
132
Plugins/UEWingman/Source/UEWingman/Private/WingJson.cpp
Normal file
132
Plugins/UEWingman/Source/UEWingman/Private/WingJson.cpp
Normal 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());
|
||||
}
|
||||
34
Plugins/UEWingman/Source/UEWingman/Private/WingLogCapture.h
Normal file
34
Plugins/UEWingman/Source/UEWingman/Private/WingLogCapture.h
Normal 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));
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
#include "WingModule.h"
|
||||
#include "Modules/ModuleManager.h"
|
||||
|
||||
IMPLEMENT_MODULE(FWingModule, UEWingman);
|
||||
11
Plugins/UEWingman/Source/UEWingman/Private/WingModule.h
Normal file
11
Plugins/UEWingman/Source/UEWingman/Private/WingModule.h
Normal 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 {}
|
||||
};
|
||||
61
Plugins/UEWingman/Source/UEWingman/Private/WingNotifier.cpp
Normal file
61
Plugins/UEWingman/Source/UEWingman/Private/WingNotifier.cpp
Normal 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();
|
||||
}
|
||||
194
Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp
Normal file
194
Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp
Normal 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];
|
||||
}
|
||||
447
Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp
Normal file
447
Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp
Normal 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;
|
||||
}
|
||||
}
|
||||
278
Plugins/UEWingman/Source/UEWingman/Private/WingToolMenu.cpp
Normal file
278
Plugins/UEWingman/Source/UEWingman/Private/WingToolMenu.cpp
Normal 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;
|
||||
}
|
||||
524
Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp
Normal file
524
Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp
Normal 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;
|
||||
}
|
||||
|
||||
854
Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp
Normal file
854
Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp
Normal 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"));
|
||||
}
|
||||
}
|
||||
58
Plugins/UEWingman/Source/UEWingman/Public/WingBlueprintVar.h
Normal file
58
Plugins/UEWingman/Source/UEWingman/Public/WingBlueprintVar.h
Normal 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();
|
||||
};
|
||||
21
Plugins/UEWingman/Source/UEWingman/Public/WingCommandlet.h
Normal file
21
Plugins/UEWingman/Source/UEWingman/Public/WingCommandlet.h
Normal 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;
|
||||
};
|
||||
148
Plugins/UEWingman/Source/UEWingman/Public/WingFetcher.h
Normal file
148
Plugins/UEWingman/Source/UEWingman/Public/WingFetcher.h
Normal 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;
|
||||
}
|
||||
37
Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h
Normal file
37
Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h
Normal 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);
|
||||
};
|
||||
89
Plugins/UEWingman/Source/UEWingman/Public/WingGraphExport.h
Normal file
89
Plugins/UEWingman/Source/UEWingman/Public/WingGraphExport.h
Normal 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;
|
||||
};
|
||||
55
Plugins/UEWingman/Source/UEWingman/Public/WingHandler.h
Normal file
55
Plugins/UEWingman/Source/UEWingman/Public/WingHandler.h
Normal 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() {}
|
||||
};
|
||||
17
Plugins/UEWingman/Source/UEWingman/Public/WingJson.h
Normal file
17
Plugins/UEWingman/Source/UEWingman/Public/WingJson.h
Normal 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
Reference in New Issue
Block a user