Refactoring ue-wingman to be a command-line only tool

This commit is contained in:
2026-05-13 21:36:40 -04:00
parent ff9c045c8e
commit e0d45cc1db
39 changed files with 533 additions and 866 deletions

View File

@@ -25,7 +25,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Asset to delete"))
FString Asset;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, skip reference check and force delete"))
UPROPERTY(EditAnywhere, meta=(Description="If true, skip reference check and force delete"))
bool Force = false;
virtual void Register() override

View File

@@ -21,13 +21,13 @@ class UWing_Asset_Search : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring to match against asset package paths"))
UPROPERTY(EditAnywhere, meta=(Description="Substring to match against asset package paths"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
UPROPERTY(EditAnywhere, meta=(Description="Asset class name to filter by, e.g. Blueprint, Material, StaticMesh"))
FString Type;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results (default 50)"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results (default 50)"))
int32 Limit = 50;
virtual void Register() override

View File

@@ -32,16 +32,15 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Type of graph: function or macro"))
FString GraphType;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Create a new function or macro graph in a Blueprint."));
TEXT("Create a new function or macro graph in a Blueprint. "
"Variables must be expressed as 'kind type name (flags) = default'. "
"Kind can be input, output, or local."));
}
virtual void Handle() override
{
@@ -79,8 +78,7 @@ public:
// Parse and validate variables before making changes
WingVariables Vars;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
// Create the Graph
UEdGraph* NewGraph = FBlueprintEditorUtils::CreateNewGraph(BP, InternalID,

View File

@@ -27,7 +27,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Interface name to remove"))
FString Interface;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, keep the function graphs as regular functions"))
UPROPERTY(EditAnywhere, meta=(Description="If true, keep the function graphs as regular functions"))
bool PreserveFunctions = false;
virtual void Register() override

View File

@@ -37,5 +37,6 @@ public:
if (!P) return;
WingOut::Stdout.Print(P->GetText());
WingOut::Stdout.Print(TEXT("\n"));
}
};

View File

@@ -1,60 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "Details_SetMany.generated.h"
UCLASS()
class UWing_Details_SetMany : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target object"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Description="Object mapping property names to new values in Unreal text format"))
FWingJsonObject Properties;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Set one or more editable properties. Values use Unreal text format."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
if (!Properties.Json || Properties.Json->Values.Num() == 0)
{
WingOut::Stdout.Print(TEXT("Error: No properties specified\n"));
return;
}
TArray<FWingProperty> Props = FWingProperty::GetDetails(Obj, true);
// Validation pass — resolve all properties before modifying anything.
for (const auto& Pair : Properties.Json->Values)
{
FWingProperty* P = WingUtils::FindOneWithExternalID(Pair.Key, Props, TEXT("Property"), WingOut::Stdout);
if (!P) return;
}
// Assignment pass — store the values.
int SuccessCount = 0;
for (const auto& Pair : Properties.Json->Values)
{
FWingProperty* P = WingUtils::FindOneWithExternalID(Pair.Key, Props, TEXT("Property"), WingOut::Stdout);
if (P->SetJson(*Pair.Value, WingOut::Stdout)) SuccessCount++;
}
WingOut::Stdout.Printf(TEXT("Set %d/%d properties.\n"), SuccessCount, Properties.Json->Values.Num());
}
};

View File

@@ -0,0 +1,27 @@
#pragma once
#include "CoreMinimal.h"
#include "WingBasics.h"
#include "WingServer.h"
#include "WingManual.h"
#include "Documentation_Command.generated.h"
UCLASS()
class UWing_Documentation_Command : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
FString Command;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Detailed documentation for one or more commands."));
}
virtual void Handle() override
{
WingManual::Commands(EWingHandlerKind::Normal, Command, true);
}
};

View File

@@ -12,19 +12,13 @@ class UWing_Documentation_Commands : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("List all the main commands with their descriptions."));
TEXT("A concise list of all ue-wingman commands."));
}
virtual void Handle() override
{
WingManual::Commands(EWingHandlerKind::Normal, Query, Verbose);
WingManual::Commands(EWingHandlerKind::Normal, TEXT(""), false);
}
};

View File

@@ -12,10 +12,10 @@ class UWing_Documentation_CreateAssets : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="Substring filter for command names"))
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for command names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="If true, return full details including parameter types and descriptions"))
UPROPERTY(EditAnywhere, meta=(Description="If true, return full details including parameter types and descriptions"))
bool Verbose = false;
virtual void Register() override

View File

@@ -12,7 +12,7 @@ class UWing_Documentation_Manual : public UWingHandler
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Optional, Description="section of the manual"))
UPROPERTY(EditAnywhere, meta=(Description="section of the manual"))
FString Section;
virtual void Register() override
@@ -44,7 +44,7 @@ public:
}
else
{
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"));
WingOut::Stdout.Printf(TEXT("Unknown manual section '%s'\n"), *Section);
WingManual::PrintSectionNames(TEXT("Valid manual sections:"), Sections, WingOut::Stdout);
}
}

View File

@@ -28,13 +28,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Name of the new event dispatcher"))
FString Dispatcher;
UPROPERTY(EditAnywhere, meta=(Description="Input Variables, one per line, expressed as: type var = value"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variables"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Add a new event dispatcher to a Blueprint."));
TEXT("Add a new event dispatcher to a Blueprint. "
"Variables must be expressed as 'kind type name (flags) = default'. "
"Kind can only be 'input'."));
}
virtual void Handle() override
{
@@ -49,7 +52,7 @@ public:
// Parse the arguments.
WingVariables Vars;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
// Add the delegate variable
FEdGraphPinType DelegateType;

View File

@@ -1,95 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "WingGraphActions.h"
#include "WingGraphExport.h"
#include "EdGraph/EdGraph.h"
#include "GraphNode_Add.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FSpawnNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Type;
UPROPERTY()
int32 PosX = 0;
UPROPERTY()
int32 PosY = 0;
FWingGraphAction *Action;
};
UCLASS()
class UWing_GraphNode_Add : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {Type, posX, posY} objects. Use GraphNode_SearchTypes to find types."))
FWingJsonArray Nodes;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Create nodes using the editor's action database. "
"Use GraphNode_SearchTypes to find types."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
int32 TotalCount = Nodes.Array.Num();
FWingGraphActions GraphActions(TargetGraph);
// Parse the json array, turning it into an array of spawn node entries.
TArray<FSpawnNodeEntry> Entries;
FSpawnNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(nullptr, &Entry, FSpawnNodeEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) return;
TArray<FWingGraphAction*> Results = GraphActions.Search(Entry.Type, 2, true);
if (!WingUtils::CheckExactlyOneNamed(Results.Num(), TEXT("node type"), Entry.Type, WingOut::Stdout)) return;
Entry.Action = Results[0];
Entries.Add(Entry);
}
// Execute all.
for (const FSpawnNodeEntry &SpawnEntry : Entries)
{
UEdGraphNode* NewNode = SpawnEntry.Action->Execute(FVector2D(SpawnEntry.PosX, SpawnEntry.PosY));
if (NewNode)
{
WingOut::Stdout.Printf(TEXT("Spawned: %s\n"), *SpawnEntry.Type);
WingGraphExport Export(NewNode, false, true);
WingOut::Stdout.Print(Export.GetOutput());
}
else
{
WingOut::Stdout.Printf(TEXT("Failed: %s\n\n"), *SpawnEntry.Type);
continue;
}
}
}
};

View File

@@ -21,7 +21,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Target node"))
FString Node;
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
bool Details = false;
virtual void Register() override

View File

@@ -1,77 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingGraphActions.h"
#include "EdGraph/EdGraph.h"
#include "GraphNode_SearchTypes.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphNode_SearchTypes : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain * wildcards"))
FWingJsonArray Queries;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
int32 MaxResults = 50;
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Search for node types that can be spawned in a graph. "
"Pass a string returned by this function to GraphNode_Add."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
// Validate all entries are strings before running any searches.
TArray<FString> QueryStrings;
QueryStrings.Reserve(Queries.Array.Num());
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
{
FString QueryStr;
if (!QueryVal->TryGetString(QueryStr))
{
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
return;
}
QueryStrings.Add(QueryStr);
}
FWingGraphActions GraphActions(TargetGraph);
for (const FString& Query : QueryStrings)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<FWingGraphAction*> Results = GraphActions.Search(Query, MaxResults, false);
for (const FWingGraphAction* Action : Results)
{
WingOut::Stdout.Printf(TEXT("%s\n"), *Action->Name);
}
if (Results.Num() == 0)
{
WingOut::Stdout.Print(TEXT("No matching node types found.\n"));
}
else if (Results.Num() >= MaxResults)
{
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
}
}
}
};

View File

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

View File

@@ -1,73 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphNode.h"
#include "GraphNode_SetPositions.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FMoveNodeEntry
{
GENERATED_BODY()
UPROPERTY()
FString Node;
UPROPERTY()
int32 X = 0;
UPROPERTY()
int32 Y = 0;
};
UCLASS()
class UWing_GraphNode_SetPositions : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {node, x, y} objects"))
FWingJsonArray Nodes;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Reposition one or more nodes in a Blueprint graph."));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* TargetGraph = F.Walk(Graph).Cast<UEdGraph>();
if (!TargetGraph) return;
int32 SuccessCount = 0;
FMoveNodeEntry Entry;
TArray<FWingProperty> Props = FWingProperty::GetAll(nullptr, &Entry, FMoveNodeEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& Elt : Nodes.Array)
{
if (!FWingProperty::PopulateFromJson(Props, *Elt, false, WingOut::Stdout)) continue;
WingFetcher FN(TargetGraph, WingOut::Stdout);
UEdGraphNode* Node = FN.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) continue;
Node->NodePosX = Entry.X;
Node->NodePosY = Entry.Y;
SuccessCount++;
}
WingOut::Stdout.Printf(TEXT("Moved %d/%d nodes.\n"), SuccessCount, Nodes.Array.Num());
}
};

View File

@@ -1,96 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphSchema.h"
#include "EdGraph/EdGraphPin.h"
#include "GraphPin_Connect.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
USTRUCT()
struct FConnectPinsEntry
{
GENERATED_BODY()
UPROPERTY()
FString SourcePin;
UPROPERTY()
FString TargetPin;
};
UCLASS()
class UWing_GraphPin_Connect : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of {sourcePin, targetPin} objects"))
FWingJsonArray Connections;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Connect pins between nodes in a graph (Blueprint or Material). "
"Pin IDs use fetcher path syntax relative to the graph, eg: "
"node:K2Node_CallFunction_0,pin:ReturnValue"));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
int32 SuccessCount = 0;
int32 TotalCount = Connections.Array.Num();
FConnectPinsEntry Entry;
TArray<FWingProperty> EntryProps = FWingProperty::GetAll(nullptr, &Entry, FConnectPinsEntry::StaticStruct(), true);
for (const TSharedPtr<FJsonValue>& ConnVal : Connections.Array)
{
if (!FWingProperty::PopulateFromJson(EntryProps, *ConnVal, false, WingOut::Stdout))
continue;
WingFetcher FS(G, WingOut::Stdout);
UWingGraphPinRef* SourcePinRef = FS.Walk(Entry.SourcePin).Cast<UWingGraphPinRef>();
if (!SourcePinRef) continue;
UEdGraphPin* SourcePin = WingUtils::CheckGetPin(SourcePinRef->Node, SourcePinRef->PinName, WingOut::Stdout);
if (!SourcePin) continue;
WingFetcher FT(G, WingOut::Stdout);
UWingGraphPinRef* TargetPinRef = FT.Walk(Entry.TargetPin).Cast<UWingGraphPinRef>();
if (!TargetPinRef) continue;
UEdGraphPin* TargetPin = WingUtils::CheckGetPin(TargetPinRef->Node, TargetPinRef->PinName, WingOut::Stdout);
if (!TargetPin) continue;
const UEdGraphSchema* Schema = G->GetSchema();
const FPinConnectionResponse Response = Schema->CanCreateConnection(SourcePin, TargetPin);
if (Response.Response == CONNECT_RESPONSE_DISALLOW)
{
WingOut::Stdout.Printf(TEXT("error: Cannot connect %s.%s to %s.%s: %s\n"),
*WingUtils::FormatName(SourcePin->GetOwningNode()), *WingUtils::FormatName(SourcePin),
*WingUtils::FormatName(TargetPin->GetOwningNode()), *WingUtils::FormatName(TargetPin),
*Response.Message.ToString());
continue;
}
Schema->TryCreateConnection(SourcePin, TargetPin);
SuccessCount++;
}
WingOut::Stdout.Printf(TEXT("Connected %d/%d pins.\n"), SuccessCount, TotalCount);
}
};

View File

@@ -1,77 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingFetcher.h"
#include "WingProperty.h"
#include "WingUtils.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "GraphPin_Disconnect.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_GraphPin_Disconnect : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Target graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Description="Array of pin ID strings"))
FWingJsonArray Pins;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Disconnect all connections on the specified pins. "
"Pin IDs use fetcher path syntax relative to the graph, eg: "
"node:K2Node_CallFunction_0,pin:ReturnValue"));
}
virtual void Handle() override
{
WingFetcher F(WingOut::Stdout);
UEdGraph* G = F.Walk(Graph).Cast<UEdGraph>();
if (!G) return;
int32 SuccessCount = 0;
int32 TotalDisconnected = 0;
for (const TSharedPtr<FJsonValue>& PinVal : Pins.Array)
{
FString PinPath;
if (!PinVal->TryGetString(PinPath))
{
WingOut::Stdout.Print(TEXT("ERROR: Expected a string pin ID.\n"));
continue;
}
WingFetcher FP(G, WingOut::Stdout);
UWingGraphPinRef* PinRef = FP.Walk(PinPath).Cast<UWingGraphPinRef>();
if (!PinRef) continue;
UEdGraphPin* Pin = WingUtils::CheckGetPin(PinRef->Node, PinRef->PinName, WingOut::Stdout);
if (!Pin) continue;
int32 DisconnectedCount = Pin->LinkedTo.Num();
if (DisconnectedCount > 0)
{
Pin->BreakAllPinLinks(true);
}
WingOut::Stdout.Printf(TEXT("Disconnected %d link(s) from %s.%s\n"),
DisconnectedCount,
*WingUtils::FormatName(Pin->GetOwningNode()), *WingUtils::FormatName(Pin));
SuccessCount++;
TotalDisconnected += DisconnectedCount;
}
WingOut::Stdout.Printf(TEXT("Done: %d/%d succeeded, %d links broken.\n"),
SuccessCount, Pins.Array.Num(), TotalDisconnected);
}
};

View File

@@ -22,7 +22,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to graph"))
FString Graph;
UPROPERTY(EditAnywhere, meta=(Optional, Description="True to show minor node properties"))
UPROPERTY(EditAnywhere, meta=(Description="True to show minor node properties"))
bool Details = false;
virtual void Register() override

View File

@@ -1,41 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "Sequence.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Sequence : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description=
"Array of subcommand JSON objects to execute in order. Each must contain 'command' and its parameters."))
FWingJsonArray Subcommands;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Execute multiple commands in one request. Each subcommand "
"produces its own content block in the response. The big win "
"performance-wise is that fewer MCP calls means fewer "
"round-trip invocations of the LLM."));
}
virtual void Handle() override
{
// The actual code that implements Sequence is hardwired into
// WingServer. Because of that, this handler is never actually called
// under normal conditions. The handler exists for two reasons: to
// provide documentation, and also to catch the case where somebody
// nests a sequence inside another sequence.
WingOut::Stdout.Print(
TEXT("ERROR: Sequence inside a Sequence is not allowed.\n"));
}
};

View File

@@ -20,7 +20,7 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Substring filter for type names"))
FString Query;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results"))
UPROPERTY(EditAnywhere, meta=(Description="Maximum number of results"))
int32 Limit = 100;
virtual void Register() override

View File

@@ -21,22 +21,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Add new variables. Format: 'type name (flags) = default', one per line."));
TEXT("Add variables to a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "
"Each variable must be expressed as: 'kind type name (flags) = default'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -46,10 +40,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
if (!Vars.Check(WingOut::Stdout)) return;
if (!Vars.Create(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));

View File

@@ -21,23 +21,16 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variables, one per line"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variables, one per line"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variables, one per line"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variables, one per line"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Modify variables of a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "));
TEXT("Add variables to a blueprint, function graph, "
"macro graph, event dispatcher graph, or custom event node. "
"Each variable must be expressed as: 'kind type name (flags) = default'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -47,10 +40,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, false, WingOut::Stdout)) return;
if (!Vars.Check(WingOut::Stdout)) return;
if (!Vars.Modify(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));

View File

@@ -21,22 +21,15 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Path to a blueprint, graph, or custom event node"))
FString Object;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Blueprint variable names to remove, comma-separated"))
FString BlueprintVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Input variable names to remove, comma-separated"))
FString InputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Output variable names to remove, comma-separated"))
FString OutputVariables;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Local variable names to remove, comma-separated"))
FString LocalVariables;
UPROPERTY(EditAnywhere, meta=(Description="Variable descriptions"))
FWingRestOfArgv Variables;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Remove variables from a blueprint, graph, or custom event node."));
TEXT("Remove variables from a blueprint, graph, or custom event node. "
"Each variable must be expressed as: 'kind name'. "
"Kind can be blueprint, input, output, or local."));
}
virtual void Handle() override
{
@@ -46,10 +39,7 @@ public:
WingVariables Vars;
if (!Vars.SetBackingStore(Obj, WingOut::Stdout)) return;
if (!Vars.BlueprintVariables.ParseNamesString(BlueprintVariables, WingOut::Stdout)) return;
if (!Vars.InputVariables.ParseNamesString(InputVariables, WingOut::Stdout)) return;
if (!Vars.OutputVariables.ParseNamesString(OutputVariables, WingOut::Stdout)) return;
if (!Vars.LocalVariables.ParseNamesString(LocalVariables, WingOut::Stdout)) return;
if (!Vars.Parse(Variables.Argv, true, WingOut::Stdout)) return;
if (!Vars.Remove(WingOut::Stdout)) return;
WingOut::Stdout.Printf(TEXT("Success.\n"));
}

View File

@@ -33,10 +33,10 @@ public:
UPROPERTY(EditAnywhere, meta=(Description="Name for the new widget"))
FString Name;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Parent widget name. If omitted, sets as root."))
UPROPERTY(EditAnywhere, meta=(Description="Parent widget name. If omitted, sets as root."))
FString Parent;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Whether to expose the widget as a variable in the blueprint (default false)"))
UPROPERTY(EditAnywhere, meta=(Description="Whether to expose the widget as a variable in the blueprint (default false)"))
bool IsVariable = false;
virtual void Register() override
@@ -117,6 +117,10 @@ public:
BP->WidgetTree->RootWidget = NewWidget;
}
// Register a variable GUID for the new widget. UMG's compiler
// ensures every widget in the tree is present in this map.
BP->OnVariableAdded(NewWidget->GetFName());
WingOut::Stdout.Printf(TEXT("Created widget '%s' of type '%s'\n"), *Name, *Type);
}
};

View File

@@ -1,68 +0,0 @@
#pragma once
#include "CoreMinimal.h"
#include "WingServer.h"
#include "WingBasics.h"
#include "WingWidgets.h"
#include "Widget_SearchTypes.generated.h"
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
// ---------------------------------------------------------------------------
UCLASS()
class UWing_Widget_SearchTypes : public UWingHandler
{
GENERATED_BODY()
public:
UPROPERTY(EditAnywhere, meta=(Description="Array of query strings; each may contain *"))
FWingJsonArray Queries;
UPROPERTY(EditAnywhere, meta=(Optional, Description="Maximum number of results per query (default 50)"))
int32 MaxResults = 50;
virtual void Register() override
{
UWingServer::AddHandler(this,
TEXT("Search for widget types that can be added to a Widget Blueprint. "
"Returns names for use with Widget_Add."));
}
virtual void Handle() override
{
// Validate all entries are strings before running any searches.
TArray<FString> QueryStrings;
QueryStrings.Reserve(Queries.Array.Num());
for (const TSharedPtr<FJsonValue>& QueryVal : Queries.Array)
{
FString QueryStr;
if (!QueryVal->TryGetString(QueryStr))
{
WingOut::Stdout.Print(TEXT("ERROR: Queries must be an array of strings.\n"));
return;
}
QueryStrings.Add(QueryStr);
}
WingWidgets Widgets;
for (const FString& Query : QueryStrings)
{
WingOut::Stdout.Printf(TEXT("\n=== %s ===\n\n"), *Query);
TArray<WingWidgets::Type> Results = Widgets.Search(Query, MaxResults, false);
for (const WingWidgets::Type& Entry : Results)
{
WingOut::Stdout.Printf(TEXT("%s\n"), *Entry.MenuName);
}
if (Results.Num() == 0)
{
WingOut::Stdout.Print(TEXT("No matching widget types found.\n"));
}
else if (Results.Num() >= MaxResults)
{
WingOut::Stdout.Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults);
}
}
}
};

View File

@@ -6,17 +6,23 @@
void WingManual::PrintHandlerPrototype(const FWingHandlerConfig& Handler)
{
WingOut::Stdout.Print(TEXT("ue-wingman "));
WingOut::Stdout.Print(Handler.Name);
WingOut::Stdout.Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(Handler.HandlerClass.Get(), EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) WingOut::Stdout.Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) WingOut::Stdout.Print(TEXT("?"));
WingOut::Stdout.Print(PropIt->GetName());
FStructProperty* StructProp = CastField<FStructProperty>(*PropIt);
const bool bIsRest =
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
if (bIsRest)
{
WingOut::Stdout.Printf(TEXT(" [%s...]"), *PropIt->GetName());
}
else
{
WingOut::Stdout.Printf(TEXT(" %s"), *PropIt->GetName());
}
}
WingOut::Stdout.Print(TEXT(")\n"));
WingOut::Stdout.Print(TEXT("\n"));
}
void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
@@ -26,197 +32,193 @@ void WingManual::PrintHandlerArguments(const FWingHandlerConfig& Handler)
{
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"));
FString Desc = Prop->GetMetaData(TEXT("Description"));
if (Desc.IsEmpty()) Desc = TEXT("No documentation");
if (bOptional)
{
WingOut::Stdout.Printf(TEXT(" %s (optional %s)"), *Name, *Type);
}
else
{
WingOut::Stdout.Printf(TEXT(" %s (%s)"), *Name, *Type);
}
if (!Desc.IsEmpty()) WingOut::Stdout.Printf(TEXT(" — %s"), *Desc);
WingOut::Stdout.Print(TEXT("\n"));
WingOut::Stdout.Printf(TEXT(" %s - %s\n"), *Name, *Desc);
}
}
void WingManual::PrintHandlerDescription(const FWingHandlerConfig& Handler)
{
if (Handler.Documentation.IsEmpty()) return;
WingOut::Stdout.Print(WingUtils::WrapText(Handler.Documentation, 80, TEXT(" // ")));
WingOut::Stdout.Printf(TEXT("\n%s\n\n"), *Handler.Documentation);
}
void WingManual::PrintHandlerHelp(const FWingHandlerConfig& Handler)
{
WingOut::Stdout.Print(TEXT("\n"));
PrintHandlerPrototype(Handler);
PrintHandlerArguments(Handler);
PrintHandlerDescription(Handler);
PrintHandlerPrototype(Handler);
PrintHandlerArguments(Handler);
PrintHandlerDescription(Handler);
WingOut::Stdout.Print(TEXT("\n"));
}
void UWingManualSections::FetcherPaths()
{
WingOut::Stdout.Print(TEXT(
"\n FETCHER PATHS:"
"\n"
"\n Most commands require you to specify a 'fetcher path'."
"\n A fetcher path starts with an asset name, followed by"
"\n steps that navigate into the asset. Some Examples:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
"\n"
"\n The navigation steps supported are:"
"\n"
"\n graph — move from a blueprint or material to a graph."
"\n node — move from a graph to a graph node"
"\n pin — move from a graph node to a pin"
"\n component — move from a blueprint to a component"
"\n levelblueprint — move from a world to a blueprint"
"\n widget — move from a widget blueprint to a widget"
"\n structprop — move into a struct property of an object"
"\n"
"\n Notice that paths use escaped fnames. See the section"
"\n on escape sequences in fnames below sfor more information."
"\n"
"\n Steps do not always require a parameter. For example, materials"
"\n only have one graph, so you can just say:"
"\n"
"\n /Game/Materials/MyMaterial,graph"
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n FETCHER PATHS:"
"\n"
"\n Most commands require you to specify a 'fetcher path'."
"\n A fetcher path starts with an asset name, followed by"
"\n steps that navigate into the asset. Some Examples:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,widget:Canvas.122"
"\n /Game/Testing/BP_Test,graph:Rescale.Actor,node:K2Node_CallFunction_0,pin:Scale"
"\n /Game/Chars/BP_Manny,component:Camera.Boom"
"\n"
"\n The navigation steps supported are:"
"\n"
"\n graph — move from a blueprint or material to a graph."
"\n node — move from a graph to a graph node"
"\n pin — move from a graph node to a pin"
"\n component — move from a blueprint to a component"
"\n levelblueprint — move from a world to a blueprint"
"\n widget — move from a widget blueprint to a widget"
"\n structprop — move into a struct property of an object"
"\n"
"\n Notice that paths use escaped fnames. See the section"
"\n on escape sequences in fnames below sfor more information."
"\n"
"\n Steps do not always require a parameter. For example, materials"
"\n only have one graph, so you can just say:"
"\n"
"\n /Game/Materials/MyMaterial,graph"
"\n"
));
}
void UWingManualSections::ExpressingTypes()
{
WingOut::Stdout.Print(TEXT(
"\n EXPRESSING TYPES:"
"\n"
"\n To change the type of a variable, or to express function parameters,"
"\n you will use our syntax for types. Here are some valid examples:"
"\n"
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
"\n Array<Int>, Set<String>, Map<Int,Actor>"
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
"\n"
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
"\n is used as a type, the typename is just BP_Foo. You can search"
"\n for valid types using the TypeName_Search command."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n EXPRESSING TYPES:"
"\n"
"\n To change the type of a variable, or to express function parameters,"
"\n you will use our syntax for types. Here are some valid examples:"
"\n"
"\n Bool, String, Vector, Rotator, HitResult, Actor, Character,"
"\n PlayerController, EBlendMode, EMovementMode, BP_Manny, BP_Quinn,"
"\n Array<Int>, Set<String>, Map<Int,Actor>"
"\n Soft<ABP_Manny>, Class<Pawn>, SoftClass<Pawn>"
"\n"
"\n Notice that it's 'Actor', not 'AActor'. Type names are not"
"\n case-sensitive. When a blueprint like /Game/Testing/BP_Foo"
"\n is used as a type, the typename is just BP_Foo. You can search"
"\n for valid types using the TypeName_Search command."
"\n"
));
}
void UWingManualSections::VariableDeclarations()
{
WingOut::Stdout.Print(TEXT(
"\n VARIABLE DECLARATIONS:"
"\n"
"\n We have our own syntax for variable declarations: a type,"
"\n a name, optional flags, and an optional default value,"
"\n always on one line:"
"\n"
"\n Array<Actor> Actors"
"\n Float F (InstanceEditable)"
"\n String S = This is the default value"
"\n"
"\n The commands Variables_Add, Variables_Modify,"
"\n and Variables_Remove can be used to edit "
"\n blueprint variables, graph local variables, graph input"
"\n variables, graph output variables, and custom"
"\n event node input variables. Event dispatchers are"
"\n also graphs, so they too can be edited."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n VARIABLE DECLARATIONS:"
"\n"
"\n We have our own syntax for variable declarations:"
"\n"
"\n kind type name (optional flags) = optional default value"
"\n"
"\n Kind can be:"
"\n"
"\n blueprint - eg, instance variables"
"\n input - a function argument or macro input"
"\n output - a function return value or macro output"
"\n local - local variables"
"\n"
"\n Here are some examples:"
"\n"
"\n input Array<Actor> Actors"
"\n output Float F (InstanceEditable)"
"\n blueprint String S = This is the default value"
"\n"
"\n The commands Variables_Add, Variables_Modify,"
"\n and Variables_Remove can be used to edit "
"\n blueprint variables, graph local variables, graph input"
"\n variables, graph output variables, and custom"
"\n event node input variables. Event dispatchers are"
"\n also graphs, so they too can be edited."
"\n"
));
}
void UWingManualSections::EscapeSequencesInFNames()
{
WingOut::Stdout.Print(TEXT(
"\n ESCAPE SEQUENCES IN FNAMES:"
"\n"
"\n When we output FNames, we use HTML escape sequences for the"
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
"\n We also translate spaces to periods."
"\n"
"\n When sending FNames to UE Wingman, you *must* escape the marks"
"\n listed above, but you *may* escape any character. To send an FName"
"\n with a space in it, either use &#32; or a period."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n ESCAPE SEQUENCES IN FNAMES:"
"\n"
"\n When we output FNames, we use HTML escape sequences for the"
"\n following marks: \\\"'(),.:;<=>&, and for certain other characters."
"\n We also translate spaces to periods."
"\n"
"\n When sending FNames to UE Wingman, you *must* escape the marks"
"\n listed above, but you *may* escape any character. To send an FName"
"\n with a space in it, either use &#32; or a period."
"\n"
));
}
void UWingManualSections::MaterialEditing()
{
WingOut::Stdout.Print(TEXT(
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain"
"\n properties which actually come from the material expressions."
"\n You can edit these using Details_Set on the node."
"\n"
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
"\n a single node what would otherwise take many.\n"
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain"
"\n properties which actually come from the material expressions."
"\n You can edit these using Details_Set on the node."
"\n"
"\n Don't overlook custom HLSL nodes. These can accomplish in\n"
"\n a single node what would otherwise take many.\n"
"\n"
));
}
void UWingManualSections::NodeContextMenus()
{
WingOut::Stdout.Print(TEXT(
"\n NODE CONTEXT MENUS:"
"\n"
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
"\n to the node context menu. This menu includes both node"
"\n operations and pin operations (e.g. Split Struct Pin,"
"\n Add Pin)."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n NODE CONTEXT MENUS:"
"\n"
"\n GraphNode_ShowMenu and GraphNode_ChooseMenu give access"
"\n to the node context menu. This menu includes both node"
"\n operations and pin operations (e.g. Split Struct Pin,"
"\n Add Pin)."
"\n"
));
}
void UWingManualSections::VariableGettersAndSetters()
{
WingOut::Stdout.Print(TEXT(
"\n VARIABLE GETTERS AND SETTERS:"
"\n"
"\n Access to local vars, function parameters, and "
"\n blueprint vars is through getter and setter nodes. "
"\n These can be found in GraphNode_SearchTypes by "
"\n searching for 'Variable'. Some examples:"
"\n"
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n VARIABLE GETTERS AND SETTERS:"
"\n"
"\n Access to local vars, function parameters, and "
"\n blueprint vars is through getter and setter nodes. "
"\n These can be found in GraphNode_SearchTypes by "
"\n searching for 'Variable'. Some examples:"
"\n"
"\n SKEL_WB_Menu_C|Variables|Default|GetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|Default|SetPlaceTangible"
"\n SKEL_WB_Menu_C|Variables|WB_Menu|GetMenuPanel"
"\n"
));
}
void UWingManualSections::ImportantCommands()
{
WingOut::Stdout.Print(TEXT(
"\n IMPORTANT COMMANDS:"
"\n"
"\n Documentation_Manual: print manual sections"
"\n Documentation_Commands: a list of all the main commands"
"\n Documentation_CreateAssets: Additional commands that create new assets"
"\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Details_Dump: Dump the details panel for a given object"
"\n Details_Set: Manipulate the details panel for a given object"
"\n Sequence: Batch commands together for faster execution"
"\n"
"\n You can use Documentation_Commands(Query=Command,Verbose=true)"
"\n to get detailed help for a specific command."
"\n"
));
WingOut::Stdout.Print(TEXT(
"\n IMPORTANT COMMANDS:"
"\n"
"\n Documentation_Manual: print manual sections"
"\n Documentation_Commands: A concise list of all ue-wingman commands"
"\n Documentation_Command: Detailed documentation for a single ue-wingman command"
"\n Documentation_CreateAssets: Additional commands that create new assets"
"\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Details_Dump: Dump the details panel for a given object"
"\n Details_Set: Manipulate the details panel for a given object"
"\n"
));
}
TSet<FName> WingManual::GetSections()
@@ -232,7 +234,7 @@ TSet<FName> WingManual::GetSections()
void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Sections, WingOut Output)
{
if (Sections.IsEmpty()) return;
if (Prefix) Output.Print(Prefix);
if (Prefix) Output.Print(Prefix);
bool bFirst = true;
for (const FName& Section : Sections)
{
@@ -240,7 +242,7 @@ void WingManual::PrintSectionNames(const TCHAR *Prefix, const TSet<FName>& Secti
bFirst = false;
Output.Printf(TEXT("%s"), *Section.ToString());
}
if (Prefix) Output.Print(TEXT("\n"));
if (Prefix) Output.Print(TEXT("\n"));
}
bool WingManual::PrintSection(FName Section)
@@ -256,10 +258,12 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
FString QueryLower = Query.ToLower();
FString PrevGroup;
bool any = false;
for (const FWingHandlerConfig& H : UWingServer::AllHandlers())
{
if (H.Kind != Kind) continue;
if (!H.Name.ToLower().Contains(QueryLower)) continue;
any = true;
// Blank line between groups
if (!Verbose)
@@ -278,4 +282,9 @@ void WingManual::Commands(EWingHandlerKind Kind, const FString& Query, bool Verb
else
PrintHandlerPrototype(H);
}
if (!any)
{
WingOut::Stdout.Print(TEXT("No matching commands. To see a full list, type:\n"));
WingOut::Stdout.Print(TEXT(" ue-wingman Documentation_Commands.\n"));
}
}

View File

@@ -194,57 +194,6 @@ bool FWingProperty::SetText(FString Value, WingOut Errors) const
return true;
}
bool FWingProperty::SetJson(const FJsonValue &JsonValue, WingOut Errors) const
{
if (!CheckEditable(Errors)) return false;
if (JsonValue.Type == EJson::String)
{
return SetText(JsonValue.AsString(), Errors);
}
if (JsonValue.Type == EJson::Number)
{
return SetDouble(JsonValue.AsNumber(), Errors);
}
if (JsonValue.Type == EJson::Boolean)
{
return SetBool(JsonValue.AsBool(), Errors);
}
if (JsonValue.Type == EJson::Object)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && (StructProp->Struct == FWingJsonObject::StaticStruct()))
{
FWingJsonObject Val;
Val.Json = JsonValue.AsObject();
Prop->SetValue_InContainer(Container, &Val);
return true;
}
PrintExpectsReceived(TEXT("json object"), Errors);
return false;
}
if (JsonValue.Type == EJson::Array)
{
FStructProperty* StructProp = CastField<FStructProperty>(Prop);
if (StructProp && (StructProp->Struct == FWingJsonArray::StaticStruct()))
{
FWingJsonArray Val;
Val.Array = JsonValue.AsArray();
Prop->SetValue_InContainer(Container, &Val);
return true;
}
PrintExpectsReceived(TEXT("json array"), Errors);
return false;
}
PrintExpectsReceived(TEXT("Unrecognized Json Data"), Errors);
return false;
}
TOptional<UObject*> FWingProperty::GetObject(WingOut Errors) const
{
FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(Prop);
@@ -493,56 +442,54 @@ TArray<FWingProperty> FWingProperty::GetDetails(UObject* Obj, bool Mutable)
return Result;
}
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json, bool AllOptional, WingOut Errors)
bool FWingProperty::PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors)
{
bool Ok = true;
// Build a set of known property names for the unknown-field check.
TSet<FName> KnownKeys;
for (const FWingProperty& P : Props) KnownKeys.Add(P->GetFName());
// Check for unknown fields in the JSON
for (const auto& KV : Json.Values)
int32 ArgIndex = 0;
for (int32 PropIndex = 0; PropIndex < Props.Num(); ++PropIndex)
{
FName Name = WingUtils::CheckInternalizeID(KV.Key, Errors);
if (!KnownKeys.Contains(Name))
{
Errors.Printf(TEXT("ERROR: Unknown parameter '%s'\n"), *KV.Key);
Ok = false;
}
}
FWingProperty& P = Props[PropIndex];
FStructProperty* StructProp = CastField<FStructProperty>(P.Prop);
const bool bIsRest =
StructProp && (StructProp->Struct == FWingRestOfArgv::StaticStruct());
// Populate each property from JSON
for (FWingProperty& P : Props)
{
FString JsonKey = WingUtils::FormatName(P.Prop);
TSharedPtr<FJsonValue> Value = Json.TryGetField(JsonKey);
if (!Value)
if (bIsRest)
{
bool Optional = AllOptional || P.Prop->HasMetaData(TEXT("Optional"));
if (!Optional)
if (PropIndex + 1 != Props.Num())
{
Errors.Printf(TEXT("ERROR: Missing required parameter '%s'\n"), *JsonKey);
Ok = false;
Errors.Printf(TEXT("ERROR: '%s' must be the last parameter\n"),
*WingUtils::FormatName(P.Prop));
return false;
}
FWingRestOfArgv Rest;
for (int32 I = ArgIndex; I < Argv.Num(); ++I)
{
Rest.Argv.Add(Argv[I]);
}
P.Prop->SetValue_InContainer(P.Container, &Rest);
ArgIndex = Argv.Num();
continue;
}
if (!P.SetJson(*Value, Errors)) Ok = false;
}
return Ok;
}
bool FWingProperty::PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json, bool AllOptional, WingOut Errors)
{
// Make sure they passed in a JSON object.
TSharedPtr<FJsonObject> Obj = Json.AsObject();
if (Obj == nullptr)
if (ArgIndex >= Argv.Num())
{
Errors.Printf(TEXT("ERROR: Missing parameter '%s'\n"),
*WingUtils::FormatName(P.Prop));
return false;
}
if (!P.SetText(Argv[ArgIndex], Errors)) return false;
ArgIndex++;
}
if (ArgIndex < Argv.Num())
{
Errors.Printf(TEXT("property data should be stored in a json object\n"));
Errors.Printf(TEXT("ERROR: Too many parameters, starting with '%s'\n"),
*Argv[ArgIndex]);
return false;
}
return PopulateFromJson(Props, *Obj, AllOptional, Errors);
return true;
}

View File

@@ -81,7 +81,7 @@ void UWingServer::Deinitialize()
bShuttingDown = true;
for (auto& Msg : PendingMessages)
{
Msg->Response.SetValue(FString());
Msg->Response.SetValue(TArray<uint8>());
}
PendingMessages.Empty();
}
@@ -150,7 +150,7 @@ void UWingServer::Tick(float DeltaTime)
// If we have a request, process it.
if (Request.IsValid())
{
FString Response = HandleRequest(Request->Line);
TArray<uint8> Response = HandleRequest(Request->Request);
Request->Response.SetValue(Response);
}
}
@@ -169,71 +169,24 @@ TStatId UWingServer::GetStatId() const
// HandleRequest — Given a command, execute it.
// ============================================================
FString UWingServer::HandleRequest(const FString& Line)
TArray<uint8> UWingServer::HandleRequest(const TArray<uint8>& RequestBytes)
{
// Parse the request as JSON before doing anything else.
TSharedPtr<FJsonValue> Value;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(Line);
if (!FJsonSerializer::Deserialize(Reader, Value))
return PackageResponses({TEXT("Invalid Json")});
const TSharedPtr<FJsonObject>* RequestPtr = nullptr;
if (!Value->TryGetObject(RequestPtr))
return PackageResponses({TEXT("Json must be an object")});
TSharedPtr<FJsonObject> Request = *RequestPtr;
TArray<FString> Argv;
FString ResponseText;
FString Command;
Request->TryGetStringField(TEXT("command"), Command);
if (Command == TEXT("Sequence"))
if (DeserializeArgv(RequestBytes, Argv))
{
const TArray<TSharedPtr<FJsonValue>>* Subcommands = nullptr;
if (!Request->TryGetArrayField(TEXT("subcommands"), Subcommands))
return PackageResponses({TEXT("Sequence requires a 'subcommands' array.")});
TArray<FString> Responses;
Responses.Reserve(Subcommands->Num());
for (const TSharedPtr<FJsonValue>& Sub : *Subcommands)
{
const TSharedPtr<FJsonObject>* SubObjPtr = nullptr;
if (!Sub->TryGetObject(SubObjPtr))
Responses.Add(TEXT("Subcommand must be a JSON object."));
else
Responses.Add(HandleJsonRequest(*SubObjPtr));
}
return PackageResponses(Responses);
PreCallHandler();
TryCallHandler(Argv);
ResponseText = PostCallHandler();
}
else ResponseText = TEXT("Invalid argv encoding (bug in ue-wingman.py)\n");
return PackageResponses({HandleJsonRequest(Request)});
FTCHARToUTF8 Utf8(*ResponseText);
return TArray<uint8>(reinterpret_cast<const uint8*>(Utf8.Get()), Utf8.Length());
}
FString UWingServer::PackageResponses(const TArray<FString>& Responses)
{
TArray<TSharedPtr<FJsonValue>> Blocks;
Blocks.Reserve(Responses.Num());
for (const FString& Response : Responses)
{
// Unreal's JSON writer terminates string serialization at the first
// embedded null byte rather than escaping it, which would silently
// truncate output. Sanitize null bytes to spaces.
FString Sanitized = Response;
for (int32 i = 0; i < Sanitized.Len(); ++i)
{
if (Sanitized[i] == TEXT('\0')) Sanitized[i] = TEXT(' ');
}
TSharedPtr<FJsonObject> Block = MakeShared<FJsonObject>();
Block->SetStringField(TEXT("type"), TEXT("text"));
Block->SetStringField(TEXT("text"), Sanitized);
Blocks.Add(MakeShared<FJsonValueObject>(Block));
}
FString OutJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutJson);
FJsonSerializer::Serialize(Blocks, Writer);
return OutJson;
}
FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
void UWingServer::PreCallHandler()
{
LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true;
@@ -241,9 +194,10 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
SuggestedManualSections.Empty();
bSuggestHandlerHelp = false;
LastHandler = nullptr;
}
TryCallHandler(Request);
FString UWingServer::PostCallHandler()
{
Notifier.SendNotifications();
LogCapture.bEnabled = false;
for (const FString& Msg : LogCapture.CapturedErrors)
@@ -269,17 +223,15 @@ FString UWingServer::HandleJsonRequest(TSharedPtr<FJsonObject> Request)
return Result;
}
void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
void UWingServer::TryCallHandler(const TArray<FString>& Argv)
{
// Extract the command from the request.
FString Command;
if (!Request->TryGetStringField(TEXT("command"), Command))
if (Argv.Num() < 1)
{
WingOut::Stdout.Printf(TEXT("Request does not contain 'command' parameter"));
WingOut::Stdout.Printf(TEXT("We recommend sending command='Documentation_Manual'."));
WingOut::Stdout.Print(TEXT("Missing command\n"));
return;
}
Request->RemoveField(TEXT("command"));
FString Command = Argv[0];
// Find the handler for the specified command.
FWingHandlerConfig* Found = FindHandler(Command);
@@ -296,9 +248,9 @@ void UWingServer::TryCallHandler(TSharedPtr<FJsonObject> Request)
UWingHandler* Handler = Cast<UWingHandler>(HandlerObj.Get());
Handler->Configuration = Found;
// Populate the handler object with the request parameters.
// Populate the handler object with argv parameters.
TArray<FWingProperty> Props = FWingProperty::GetVisible(Handler, true);
if (!FWingProperty::PopulateFromJson(Props, *Request, false, WingOut::Stdout))
if (!FWingProperty::PopulateFromArgv(Props, MakeArrayView(Argv).RightChop(1), WingOut::Stdout))
{
UWingServer::SuggestHandlerHelp();
return;
@@ -356,102 +308,105 @@ void UWingServer::CleanupFinishedClients()
void UWingServer::ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client)
{
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
constexpr int32 MinUnusedRecvSpace = 4096;
FSocket* Socket = Client->Socket;
TArray<uint8> RecvBuf;
RecvBuf.SetNumUninitialized(MinUnusedRecvSpace);
int32 RecvLen = 0;
WaitForAssetRegistry();
while (true)
TArray<uint8> Request;
if (!ReceiveRequest(Socket, Request))
{
FString Request;
if (ExtractRequestFromBuffer(RecvBuf, RecvLen, Request))
{
FString Response;
if (!ProcessRequestOnGameThread(Request, Response))
{
Client->bDone = true;
return;
}
// Write the response back, null-terminated (blocking)
FTCHARToUTF8 Utf8(*Response);
if (!SendAll(Socket, reinterpret_cast<const uint8*>(Utf8.Get()),
Utf8.Length() + 1))
{
Client->bDone = true;
return;
}
continue;
}
if (!ReceiveMoreBytesIntoBuffer(Socket, RecvBuf, RecvLen))
{
break;
}
Client->bDone = true;
return;
}
TArray<uint8> Response;
if (!ProcessRequestOnGameThread(Request, Response))
{
Client->bDone = true;
return;
}
SendAll(Socket, Response.GetData(), Response.Num());
Client->bDone = true;
}
bool UWingServer::ExtractRequestFromBuffer(
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest)
uint32 UWingServer::UnpackBigEndian(const uint8 *Data)
{
const uint8* EndOfRequest = static_cast<const uint8*>(
memchr(RecvBuf.GetData(), '\0', RecvLen));
if (EndOfRequest == nullptr)
return
((uint32)Data[0] << 24) |
((uint32)Data[1] << 16) |
((uint32)Data[2] << 8) |
(uint32)Data[3];
}
bool UWingServer::DeserializeArgv(
const TArray<uint8>& RequestBytes, TArray<FString>& Argv)
{
Argv.Empty();
int32 Offset = 0;
while (Offset < RequestBytes.Num())
{
return false;
if (RequestBytes.Num() - Offset < 4)
{
Argv.Empty();
return false;
}
uint32 Length = UnpackBigEndian(RequestBytes.GetData() + Offset);
Offset += 4;
if ((uint32)(RequestBytes.Num() - Offset) < Length)
{
Argv.Empty();
return false;
}
Argv.Add(FString::ConstructFromPtrSize(
reinterpret_cast<const UTF8CHAR*>(RequestBytes.GetData() + Offset),
Length));
Offset += (int32)Length;
}
const int32 MessageLen =
static_cast<int32>(EndOfRequest - RecvBuf.GetData());
OutRequest = FString::ConstructFromPtrSize(
reinterpret_cast<const UTF8CHAR*>(RecvBuf.GetData()), MessageLen);
const int32 RemainingBytes = RecvLen - (MessageLen + 1);
if (RemainingBytes > 0)
{
FMemory::Memmove(
RecvBuf.GetData(),
RecvBuf.GetData() + MessageLen + 1,
RemainingBytes);
}
RecvLen = RemainingBytes;
return true;
}
bool UWingServer::ReceiveMoreBytesIntoBuffer(
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen)
bool UWingServer::ReceiveRequest(FSocket* Socket, TArray<uint8>& OutRequest)
{
constexpr int32 MaxRecvBufBytes = 1024 * 1024;
constexpr int32 MinUnusedRecvSpace = 4096;
constexpr int32 ChunkSize = 8192;
int32 UnusedSpace = RecvBuf.Num() - RecvLen;
if (UnusedSpace < MinUnusedRecvSpace)
TArray<uint8> RecvBuf;
RecvBuf.Reserve(ChunkSize);
// Unreal's FSocket API is fundamentally broken: recv cannot
// differentiate between a socket that has been cleanly closed
// and a socket that has had an error. So we have no choice
// but to just read until recv returns false (which could be a
// clean close or an error). Then, we check if we have a cleanly
// encoded payload: if so, we assume everything is fine.
while (true)
{
if (RecvBuf.Num() >= MaxRecvBufBytes)
uint8 Temp[ChunkSize];
int32 BytesRead = 0;
if (!Socket->Recv(Temp, ChunkSize, BytesRead))
{
break;
}
if (BytesRead <= 0) break;
if (RecvBuf.Num() + BytesRead > MaxRecvBufBytes)
{
return false;
}
RecvBuf.SetNumUninitialized(RecvBuf.Num() * 2);
UnusedSpace = RecvBuf.Num() - RecvLen;
RecvBuf.Append(Temp, BytesRead);
}
int32 BytesRead = 0;
if (!Socket->Recv(RecvBuf.GetData() + RecvLen, UnusedSpace, BytesRead))
{
return false;
}
if (BytesRead <= 0)
{
return false;
}
if (RecvBuf.Num() < 4) return false;
uint32 Size = UnpackBigEndian(RecvBuf.GetData());
if ((uint32)RecvBuf.Num() != (4u + Size)) return false;
RecvBuf.RemoveAt(0, 4);
RecvLen += BytesRead;
OutRequest = MoveTemp(RecvBuf);
return true;
}
@@ -471,13 +426,13 @@ bool UWingServer::SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend)
}
bool UWingServer::ProcessRequestOnGameThread(
const FString& Request, FString& Response)
const TArray<uint8>& Request, TArray<uint8>& Response)
{
// Enqueue the message for game-thread processing.
TSharedPtr<UWingServer::FPendingMessage> Msg =
MakeShared<UWingServer::FPendingMessage>();
Msg->Line = Request;
TFuture<FString> Future = Msg->Response.GetFuture();
Msg->Request = Request;
TFuture<TArray<uint8>> Future = Msg->Response.GetFuture();
{
FScopeLock Lock(&GWingServer->Mutex);

View File

@@ -82,7 +82,7 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
{
if ((!Allow) && (!Variables.IsEmpty()))
{
Errors.Printf(TEXT("In this context, %s must be empty."), ListName);
Errors.Printf(TEXT("This object does not support %s.\n"), ListName);
return false;
}
for (const Var &Variable : Variables)
@@ -112,116 +112,6 @@ bool WingVariableList::CheckSanity(const TSet<FName> &GoodFlags, bool Allow, Win
return true;
}
bool WingVariableList::ParseString(const FString &Input, WingOut Errors)
{
Variables.Empty();
TArray<FString> Lines;
Input.ParseIntoArrayLines(Lines);
for (const FString& Line : Lines)
{
WingTokenizer Tok(Line);
if (Tok.NextType() == 0) continue;
Var V;
V.DefaultSpecified = false;
if (!ParseOneVariable(Tok, V, Errors)) return false;
Variables.Add(MoveTemp(V));
}
return true;
}
bool WingVariableList::ParseNamesString(const FString &Input, WingOut Errors)
{
Variables.Empty();
WingTokenizer Tok(Input);
while (Tok.TokenIs(Tok.Identifier))
{
FName Name = Tok.NextName();
Var V;
V.Name = Name;
Variables.Add(V);
V.DefaultSpecified = false;
Tok.Advance();
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(0))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("Unexpected token %s in variable list"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariableList::ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors)
{
// Parse type.
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
return false;
// Parse name.
if (Tok.NextType() != Tok.Identifier)
{
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
return false;
}
V.Name = Tok.NextName();
Tok.Advance();
// Parse optional flags: (flag1, flag2, ...)
if (Tok.TokenIs('('))
{
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
}
// Parse optional default value: = rest-of-line
if (Tok.NextType() == Tok.RestOfLine)
{
V.DefaultSpecified = true;
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
Tok.Advance();
}
// Should be at end of line.
if (Tok.NextType() != 0)
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariableList::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
{
Tok.Advance(); // Step over open-paren
while (Tok.TokenIs(Tok.Identifier))
{
Out.Add(Tok.NextName());
Tok.Advance();
// Commas are optional.
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(')'))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
Tok.Advance(); // Step over close-paren
return true;
}
void WingVariables::Empty()
{
BlueprintVariables.Empty();
@@ -254,6 +144,107 @@ void WingVariables::Print(WingOut Out)
OutputVariables.Print(Out);
}
WingVariableList *WingVariables::GetList(FName Name)
{
if (Name == TEXT("blueprint")) return &BlueprintVariables;
if (Name == TEXT("input")) return &InputVariables;
if (Name == TEXT("output")) return &OutputVariables;
if (Name == TEXT("local")) return &LocalVariables;
return nullptr;
}
bool WingVariables::ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors)
{
// Parse Kind.
if (GetList(Tok.NextName()) == nullptr)
{
Errors.Print(TEXT("ERROR: Variable description should start with 'blueprint', 'input', 'output', or 'local'"));
return false;
}
Kind = Tok.NextName();
Tok.Advance();
// Parse type.
if (!NameOnly)
{
UWingTypes::Requirements Req;
Req.BlueprintType = true;
Req.Blueprintable = false;
Req.AllowContainer = true;
if (!UWingTypes::TextToType(Tok, V.Type, Req, false, Errors))
return false;
}
// Parse name.
if (Tok.NextType() != Tok.Identifier)
{
Errors.Print(TEXT("ERROR: Expected variable name after type\n"));
return false;
}
V.Name = Tok.NextName();
Tok.Advance();
// Parse optional flags: (flag1, flag2, ...)
if ((!NameOnly) && Tok.TokenIs('('))
{
if (!ParseVariableFlags(Tok, V.Flags, Errors)) return false;
}
// Parse optional default value: = rest-of-line
if (!NameOnly && (Tok.NextType() == Tok.RestOfLine))
{
V.DefaultSpecified = true;
V.DefaultValue = FString(Tok.NextRest().TrimStartAndEnd());
Tok.Advance();
}
// Should be at end of line.
if (Tok.NextType() != 0)
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: Unexpected token after variable declaration: '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
return true;
}
bool WingVariables::ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors)
{
Tok.Advance(); // Step over open-paren
while (Tok.TokenIs(Tok.Identifier))
{
Out.Add(Tok.NextName());
Tok.Advance();
// Commas are optional.
if (Tok.TokenIs(',')) Tok.Advance();
}
if (!Tok.TokenIs(')'))
{
Tok.SaveCursor(NAME_None);
Errors.Printf(TEXT("ERROR: flag list contains invalid token '%s'\n"),
*FString(Tok.GetRange(NAME_None, 1)));
return false;
}
Tok.Advance(); // Step over close-paren
return true;
}
bool WingVariables::Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors)
{
for (const FString& Onevar : Vars)
{
WingTokenizer Tok(Onevar);
FName Kind;
Var V;
if (!ParseOneVariable(Tok, Kind, V, NameOnly, Errors)) return false;
WingVariableList *List = GetList(Kind);
List->Add(V);
}
return true;
}
void WingVariables::Load(WingOut Errors)
{
Empty();

View File

@@ -62,32 +62,18 @@ public:
////////////////////////////////////////////////////////////
//
// Json wrappers.
//
// Normally, the json request is automatically used to
// populate the properties of the handler, so the handler
// doesn't have to deal with json. However, in a few cases,
// the handler actually does want to see some json. These
// wrappers allow a handler to request raw json data instead
// of pre-processed values.
// A simple type to store the remaining arguments in
// an Argv Array.
//
////////////////////////////////////////////////////////////
USTRUCT()
struct FWingJsonObject
struct FWingRestOfArgv
{
GENERATED_BODY()
TSharedPtr<FJsonObject> Json;
};
// Marker struct for handler parameters that accept a JSON array.
// PopulateFromJson stashes the actual JSON array into the Array field.
//
USTRUCT()
struct FWingJsonArray
{
GENERATED_BODY()
TArray<TSharedPtr<FJsonValue>> Array;
UPROPERTY()
TArray<FString> Argv;
};
////////////////////////////////////////////////////////////
@@ -200,3 +186,4 @@ public:
bool Editable;
};

View File

@@ -41,7 +41,6 @@ struct FWingProperty
bool SetInt64(int64 I, WingOut Errors) const;
bool SetBool(bool B, WingOut Errors) const;
bool SetText(FString Value, WingOut Errors) const;
bool SetJson(const FJsonValue &Value, WingOut Errors) const;
// Fetch a value. If an error occurs such as a type
// mismatch, returns an empty optional and prints an
@@ -86,6 +85,7 @@ struct FWingProperty
// If mutable is false, all properties will be marked non-editable.
//
static TArray<FWingProperty> GetVisible(UObject *Obj, void *Container, UStruct *Struct, bool Mutable);
static bool PopulateFromArgv(TArray<FWingProperty>& Props, TConstArrayView<FString> Argv, WingOut Errors);
// Convenience versions of GetAll and GetVisible for UObjects.
//
@@ -121,16 +121,6 @@ struct FWingProperty
//
static TArray<FWingProperty> GetDetails(UObject* Obj, bool Mutable);
// Functions to populate properties from a JSON object.
//
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject& Json,
bool AllOptional, WingOut Errors);
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonValue& Json,
bool AllOptional, WingOut Errors);
// Functions to populate properties from a JSON object.
//
private:
static bool IsUnsigned(FNumericProperty* Prop);
static bool IsPinTypeProperty(FProperty *Prop);

View File

@@ -62,9 +62,6 @@ public:
static void AddHandler(UObject* Obj, const FString& Name, UObject* Config, EWingHandlerKind Kind, UClass* FactoryClass, const FString& Documentation);
static const TArray<FWingHandlerConfig>& AllHandlers() { return GWingServer->WingHandlerRegistry; }
/** Package a list of response texts into a single serialized JSON content-block array. */
static FString PackageResponses(const TArray<FString>& Responses);
private:
static UWingServer* GWingServer;
@@ -79,10 +76,11 @@ private:
FDelegateHandle LoadingPhasesCompleteHandle;
FWingHandlerConfig* FindHandler(const FString& Name);
// Handle a complete JSON line and return the response JSON
FString HandleRequest(const FString& Line);
FString HandleJsonRequest(TSharedPtr<FJsonObject> Request);
void TryCallHandler(TSharedPtr<FJsonObject> Request);
// Handle a complete request and return the response bytes.
TArray<uint8> HandleRequest(const TArray<uint8>& RequestBytes);
void PreCallHandler();
FString PostCallHandler();
void TryCallHandler(const TArray<FString>& Argv);
// ----- TCP server -----
FSocket* ListenSocket = nullptr;
@@ -99,22 +97,23 @@ private:
TArray<TSharedPtr<FClientConnection>> Clients;
void AcceptNewConnections();
void CleanupFinishedClients();
static uint32 UnpackBigEndian(const uint8 *Data);
static bool DeserializeArgv(
const TArray<uint8>& RequestBytes, TArray<FString>& Argv);
static void ClientThreadFunc(UWingServer* Server, TSharedPtr<FClientConnection> Client);
static bool ExtractRequestFromBuffer(
TArray<uint8>& RecvBuf, int32& RecvLen, FString& OutRequest);
static bool ReceiveMoreBytesIntoBuffer(
FSocket* Socket, TArray<uint8>& RecvBuf, int32& RecvLen);
static bool ReceiveRequest(
FSocket* Socket, TArray<uint8>& OutRequest);
static bool SendAll(FSocket* Socket, const uint8* Data, int32 BytesToSend);
static bool ProcessRequestOnGameThread(
const FString& Request, FString& Response);
const TArray<uint8>& Request, TArray<uint8>& Response);
static void WaitForAssetRegistry();
// ----- The Critical Section -----
struct FPendingMessage
{
FString Line;
TPromise<FString> Response;
FPendingMessage() : Response(TPromise<FString>()) {}
TArray<uint8> Request;
TPromise<TArray<uint8>> Response;
FPendingMessage() : Response(TPromise<TArray<uint8>>()) {}
};
FCriticalSection Mutex;
TArray<TSharedPtr<FPendingMessage>> PendingMessages;

View File

@@ -57,6 +57,9 @@ public:
// Empty the variable list.
void Empty() { Variables.Empty(); }
// Add a variable.
void Add(const Var &Var) { Variables.Add(Var); }
// Return true if the variables are empty.
bool IsEmpty() { return Variables.IsEmpty(); }
@@ -72,21 +75,11 @@ public:
// Check the sanity of the vars in the array. If allow
// is false, then no variables are allowed in the array.
bool CheckSanity(const TSet<FName> &GoodFlags, bool Allow, WingOut Errors);
// Parse variables from a string.
bool ParseString(const FString &Input, WingOut Errors);
// Parse variable names only from a string.
bool ParseNamesString(const FString &Input, WingOut Errors);
private:
bool ParseOneVariable(WingTokenizer &Tok, Var &V, WingOut Errors);
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
};
class WingVariables
{
public:
public:
using Var = WingVariableList::Var;
WingVariables() {}
@@ -125,6 +118,10 @@ public:
void Print(WingOut Out);
// Parse variables.
bool Parse(const TArray<FString> &Vars, bool NameOnly, WingOut Errors);
// Load: clear the workspace, then
// copy everything from the backing store into the workspace.
@@ -193,4 +190,8 @@ private:
void AddUserPinInfo(const Var &V, EEdGraphPinDirection Dir, UK2Node_EditablePinBase *Node);
bool ErrorNoBackingStore(WingOut Errors);
bool ParseVariableFlags(WingTokenizer &Tok, TSet<FName> &Out, WingOut Errors);
bool ParseOneVariable(WingTokenizer &Tok, FName &Kind, Var &V, bool NameOnly, WingOut Errors);
WingVariableList *GetList(FName Name);
};