UE Wingman renaming complete.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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