Framework for printing abridged manual sections in response to syntactic mistakes

This commit is contained in:
2026-03-26 16:17:06 -04:00
parent 93b396578f
commit 2bb8baac4c
12 changed files with 395 additions and 217 deletions

View File

@@ -27,7 +27,7 @@ public:
FString Graph; FString Graph;
UPROPERTY(meta=(Optional, Description="True to include less-significant details")) UPROPERTY(meta=(Optional, Description="True to include less-significant details"))
bool bDetails; bool IncludeDetails;
virtual FString GetDescription() const override virtual FString GetDescription() const override
{ {
@@ -42,7 +42,7 @@ public:
WingGraphExport Exporter(G); WingGraphExport Exporter(G);
UWingServer::Print(*Exporter.GetOutput()); UWingServer::Print(*Exporter.GetOutput());
if (bDetails) if (IncludeDetails)
{ {
UWingServer::Print(Exporter.GetDetails()); UWingServer::Print(Exporter.GetDetails());
} }

View File

@@ -6,7 +6,7 @@
#include "WingServer.h" #include "WingServer.h"
#include "WingTypes.h" #include "WingTypes.h"
#include "WingJson.h" #include "WingJson.h"
#include "WingUtils.h" #include "WingManual.h"
#include "ShowCommands.generated.h" #include "ShowCommands.generated.h"
UCLASS() UCLASS()
@@ -26,60 +26,37 @@ public:
return TEXT("List all available commands with their descriptions."); return TEXT("List all available commands with their descriptions.");
} }
void EmitCommand(UClass* Class) virtual void Handle() override
{
if (Verbose)
{
WingUtils::PrintHandlerHelp(Class);
return;
}
UWingServer::Print(WingUtils::GetHandlerName(Class));
UWingServer::Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(Class, EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) UWingServer::Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) UWingServer::Print(TEXT("?"));
UWingServer::Print(PropIt->GetName());
}
UWingServer::Print(TEXT(")\n"));
}
void EmitCommandList(bool bHalfBaked)
{ {
FString QueryLower = Query.ToLower(); FString QueryLower = Query.ToLower();
FString PrevGroup; FString PrevGroup;
for (UClass* Class : WingUtils::CollectHandlerClasses()) for (UClass* Class : WingUtils::CollectHandlerClasses())
{ {
bool bIsHalfBaked = Class->GetMetaData(TEXT("ModuleRelativePath")).StartsWith(TEXT("HalfBaked/"));
if (bIsHalfBaked != bHalfBaked)
continue;
FString ToolName = WingUtils::GetHandlerName(Class); FString ToolName = WingUtils::GetHandlerName(Class);
if (!ToolName.ToLower().Contains(QueryLower)) if (!ToolName.ToLower().Contains(QueryLower))
continue; continue;
// Blank line between groups // Blank line between groups
FString Group = WingUtils::GetHandlerGroup(Class); if (!Verbose)
if (Group != PrevGroup)
{ {
if (!PrevGroup.IsEmpty()) FString Group = WingUtils::GetHandlerGroup(Class);
UWingServer::Print(TEXT("\n")); if (Group != PrevGroup)
PrevGroup = Group; {
if (!PrevGroup.IsEmpty())
UWingServer::Print(TEXT("\n"));
PrevGroup = Group;
}
} }
EmitCommand(Class); if (Verbose)
{
WingManual::PrintHandlerHelp(Class);
}
else
{
WingManual::PrintHandlerPrototype(Class);
}
} }
} }
virtual void Handle() override
{
UWingServer::Printf(TEXT("\n"));
EmitCommandList(false);
// UWingServer::Print(TEXT("\n--- Half-Baked (may have issues) ---\n\n"));
// EmitCommandList(true);
UWingServer::Printf(TEXT("\n"));
}
}; };

View File

@@ -2,8 +2,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "WingHandler.h" #include "WingHandler.h"
#include "WingServer.h" #include "WingManual.h"
#include "WingFetcher.h"
#include "UserManual.generated.h" #include "UserManual.generated.h"
UCLASS() UCLASS()
@@ -19,78 +18,6 @@ public:
virtual void Handle() override virtual void Handle() override
{ {
WingFetcher::PrintPathExplanation(); WingManual::PrintManual(WingManual::AllSections(), nullptr, false);
UWingServer::Print(TEXT(
"\n TYPES:"
"\n"
"\n To change variable types, or function prototypes, you will"
"\n use our syntax for types. Here are some simple examples:"
"\n"
"\n boolean, int64, double, string, etc."
"\n vector, rotator, hitresult, etc."
"\n actor, character, playercontroller, etc."
"\n eblendmode, emovementmode, etc."
"\n"
"\n Notice that it's 'actor', not 'AActor'."
"\n You can use the following notations for complex types:"
"\n"
"\n Soft<abp_manny>, Class<pawn>, SoftClass<pawn>"
"\n Array<int>, Set<string>, Map<int,string>"
"\n"
"\n FUNCTION ARGUMENTS AND RETURN VALUES:"
"\n"
"\n Function argument lists are expressed as comma-separated"
"\n lists of type-name pairs:"
"\n"
"\n double D,PlayerController P,Array<int> A"
"\n"
"\n To change the arguments or return values of a function, edit the"
"\n entry or exit node of the graph using GraphNode_SetArgs."
"\n You can view the arguments using GraphNode_Dump. If a return "
"\n node doesn't exist, you may have to create it using GraphNode_Create"
"\n before you can set return values. Custom event nodes also have"
"\n editable arguments."
"\n"
"\n IDENTIFIER SANITIZATION:\n"
"\n"
"\n Identifiers in Unreal can contain spaces and punctuation marks.\n"
"\n Those punctuation marks could confuse our parsers. For example,"
"\n How would we parse Array<X> if the typename X contained a less-than?"
"\n So, we automatically translate these characters on output:"
"\n"
"\n space -> ·"
"\n < -> ◁"
"\n > -> ▷"
"\n , -> ▾"
"\n "
"\n We do the reverse translation on input. Therefore, you will always"
"\n see sanitized versions of identifiers, and you must always use"
"\n sanitized versions of identifiers:"
"\n"
"\n Correct: /Game/Testing/BP_Test,graph:Get·Cursor·Location"
"\n Wrong: /Game/Testing/BP_Test,graph:Get Cursor Location"
"\n"
"\n ABOUT WHITESPACE:"
"\n"
"\n Do not put excess whitespace into paths, typenames, or"
"\n function prototypes, only use whitespace where it is required"
"\n by the syntax."
"\n"
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"
"\n will be editing the material graph. However, if you Graph_Dump"
"\n a material graph, you will see that the nodes contain mxprop"
"\n properties, which actually come from the material expressions."
"\n You can edit these using Property_Set on the node."
"\n"
"\n COMMANDS YOU SHOULD KNOW ABOUT AND REMEMBER:"
"\n"
"\n UserManual: this explanation"
"\n ShowCommands: a full list of all the commands"
"\n Graph_Dump: a detailed listing of any UEdGraph"
"\n Property_Dump: show information on many objects"
"\n"
));
} }
}; };

View File

@@ -18,38 +18,8 @@
#include "Blueprint/WidgetTree.h" #include "Blueprint/WidgetTree.h"
#include "Components/Widget.h" #include "Components/Widget.h"
#include "Subsystems/AssetEditorSubsystem.h" #include "Subsystems/AssetEditorSubsystem.h"
#include "WingServer.h"
void WingFetcher::PrintPathExplanation() #include "WingManual.h"
{
UWingServer::Print(TEXT(
"\n PATHS:"
"\n"
"\n Most commands require you to specify a path. A path starts"
"\n with an asset name, followed by steps separated by ,"
"\n that navigate into the asset. Some Examples:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result"
"\n /Game/Testing/BP_Test,graph:Clear·Action·Grid,node:K2Node_CallFunction_0"
"\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"
"\n Notice that paths use sanitized identifiers. See the UserManual"
"\n for more information on name sanitization."
"\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"
));
}
WingFetcher::WalkFunc WingFetcher::GetWalker(const FString& Step) WingFetcher::WalkFunc WingFetcher::GetWalker(const FString& Step)
{ {
@@ -94,7 +64,7 @@ void WingFetcher::PathFailed(const TCHAR* Expected)
UWingServer::Printf(TEXT("ERROR: Path specifies a %s, but expected %s\n"), *Obj->GetClass()->GetName(), Expected); UWingServer::Printf(TEXT("ERROR: Path specifies a %s, but expected %s\n"), *Obj->GetClass()->GetName(), Expected);
else else
UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n")); UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n"));
PrintPathExplanation(); UWingServer::SuggestManual(WingManual::Section::Paths);
SetError(); SetError();
} }
@@ -106,7 +76,7 @@ WingFetcher& WingFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expecte
UWingServer::Printf(TEXT("ERROR: Input to '%s' is %s, but expected %s\n"), Walker, *Obj->GetClass()->GetName(), Expected); UWingServer::Printf(TEXT("ERROR: Input to '%s' is %s, but expected %s\n"), Walker, *Obj->GetClass()->GetName(), Expected);
else else
UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n")); UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n"));
PrintPathExplanation(); UWingServer::SuggestManual(WingManual::Section::Paths);
SetError(); SetError();
return *this; return *this;
} }
@@ -115,11 +85,19 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
{ {
if (bError) return *this; if (bError) return *this;
if (Path.Contains(TEXT(" ")))
{
UWingServer::Printf(TEXT("ERROR: Paths may not contain whitespace."));
UWingServer::SuggestManual(WingManual::Section::Paths);
UWingServer::SuggestManual(WingManual::Section::IdentifierSanitization);
return SetError();
}
TArray<FString> Segments; TArray<FString> Segments;
Path.ParseIntoArray(Segments, TEXT(",")); Path.ParseIntoArray(Segments, TEXT(","));
if (Segments.Num() == 0) if (Segments.Num() == 0)
{ {
UWingServer::Print(TEXT("ERROR: Empty path\n")); UWingServer::Print(TEXT("ERROR: Empty path\n"));
UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError(); return SetError();
} }
@@ -140,6 +118,7 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
if (!Func) if (!Func)
{ {
UWingServer::Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key); UWingServer::Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key);
UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError(); return SetError();
} }
(this->*Func)(Value); (this->*Func)(Value);
@@ -155,8 +134,8 @@ WingFetcher& WingFetcher::Asset(const FString& PackagePath)
if (!PackagePath.StartsWith(TEXT("/"))) if (!PackagePath.StartsWith(TEXT("/")))
{ {
UWingServer::Printf(TEXT("ERROR: Asset path must start with '/', got '%s'\n"), *PackagePath); UWingServer::Printf(TEXT("ERROR: Path must start with '/', got '%s'\n"), *PackagePath);
PrintPathExplanation(); UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError(); return SetError();
} }
@@ -224,7 +203,7 @@ WingFetcher& WingFetcher::Graph(const FString& Value)
if (!Value.IsEmpty()) if (!Value.IsEmpty())
{ {
UWingServer::Printf(TEXT("ERROR: Materials have only one graph, with a blank name.\n\n")); UWingServer::Printf(TEXT("ERROR: Materials have only one graph, with a blank name.\n\n"));
PrintPathExplanation(); UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError(); return SetError();
} }
WingUtils::EnsureMaterialGraph(Mat); WingUtils::EnsureMaterialGraph(Mat);

View File

@@ -341,7 +341,6 @@ void WingGraphExport::EmitComments()
// Emit wrapped, indented body. // Emit wrapped, indented body.
Output.Append(WingUtils::WrapText(CommentNode->NodeComment, 70, TEXT(" - "))); Output.Append(WingUtils::WrapText(CommentNode->NodeComment, 70, TEXT(" - ")));
Output.Append(TEXT("\n"));
// Find contained nodes. // Find contained nodes.
TArray<FString> ContainedNames; TArray<FString> ContainedNames;

View File

@@ -0,0 +1,305 @@
#include "WingManual.h"
#include "WingServer.h"
#include "WingHandler.h"
#include "WingTypes.h"
TSet<WingManual::Section> WingManual::AllSections()
{
return {
Section::Paths,
Section::Types,
Section::FunctionArguments,
Section::IdentifierSanitization,
Section::Whitespace,
Section::MaterialEditing,
Section::ImportantCommands,
};
}
void WingManual::PrintHandlerPrototype(UClass *HandlerClass)
{
UWingServer::Print(WingUtils::GetHandlerName(HandlerClass));
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"));
}
void WingManual::PrintHandlerArguments(UClass *HandlerClass)
{
// 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"));
if (bOptional)
{
UWingServer::Printf(TEXT(" %s (optional %s)"), *Name, *Type);
}
else
{
UWingServer::Printf(TEXT(" %s (%s)"), *Name, *Type);
}
if (!Desc.IsEmpty()) UWingServer::Printf(TEXT(" — %s"), *Desc);
UWingServer::Print(TEXT("\n"));
}
}
void WingManual::PrintHandlerDescription(UClass *HandlerClass)
{
const IWingHandler* Handler = Cast<IWingHandler>(HandlerClass->GetDefaultObject());
if (!Handler) return;
UWingServer::Print(WingUtils::WrapText(Handler->GetDescription(), 80, TEXT(" // ")));
}
void WingManual::PrintHandlerHelp(UClass* HandlerClass)
{
UWingServer::Print(TEXT("\n"));
PrintHandlerPrototype(HandlerClass);
PrintHandlerArguments(HandlerClass);
PrintHandlerDescription(HandlerClass);
UWingServer::Print(TEXT("\n"));
}
void WingManual::PrintManual(TSet<Section> Sections, UClass *Handler, bool Abridged)
{
if (Handler == nullptr)
{
Sections.Remove(Section::HandlerHelp);
}
if (Sections.IsEmpty()) return;
if (Abridged)
{
UWingServer::Printf(TEXT("\n--- AUTOMATIC DOCUMENTATION ---\n"));
}
if (Sections.Contains(Section::HandlerHelp))
{
PrintHandlerHelp(Handler);
}
if (Sections.Contains(Section::Paths))
{
if (Abridged)
{
UWingServer::Print(TEXT(
"\n PATHS: Here are some example paths:"
"\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"
));
}
else
{
UWingServer::Print(TEXT(
"\n PATHS:"
"\n"
"\n Most commands require you to specify a path. A path starts"
"\n with an asset name, followed by steps separated by ,"
"\n 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"
"\n Notice that paths use sanitized identifiers. See the section"
"\n on identifier sanitization below for 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"
));
}
}
if (Sections.Contains(Section::Types))
{
if (Abridged)
{
UWingServer::Print(TEXT(
"\n TYPES: Here are some examples of valid types:"
"\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"
));
}
else
{
UWingServer::Print(TEXT(
"\n TYPES:"
"\n"
"\n To change variable types, or to express function prototypes, you will"
"\n 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 BP_Foo."
"\n"
));
}
}
if (Sections.Contains(Section::FunctionArguments))
{
if (Abridged)
{
UWingServer::Print(TEXT(
"\n FUNCTION ARGUMENTS: Here is an example argument list:"
"\n double D,PlayerController P,Array<Int> A"
"\n"
));
}
else
{
UWingServer::Print(TEXT(
"\n FUNCTION ARGUMENTS AND RETURN VALUES:"
"\n"
"\n Function argument lists are expressed as comma-separated"
"\n lists of type-name pairs:"
"\n"
"\n Double D,PlayerController P,Array<Int> A"
"\n"
"\n To change the arguments or return values of a function, edit the"
"\n entry or exit node of the graph using GraphNode_SetArgs."
"\n You can view the arguments using GraphNode_Dump. If a return "
"\n node doesn't exist, you may have to create it using GraphNode_Create"
"\n before you can set return values. Custom event nodes also have"
"\n editable arguments."
"\n"
));
}
}
if (Sections.Contains(Section::IdentifierSanitization))
{
if (Abridged)
{
UWingServer::Print(TEXT(
"\n IDENTIFIER SANITIZATION:\n"
"\n Identifiers in unreal can contain whitespace and punctuation.\n"
"\n We sanitize these characters on output:\n"
"\n"
"\n space → ·"
"\n < → ◁"
"\n > → ▷"
"\n , → ▾"
"\n "
"\n We do the reverse translation on input. Therefore, you will always"
"\n see sanitized versions of identifiers, and you must always use"
"\n sanitized versions of identifiers:"
"\n"
));
}
else
{
UWingServer::Print(TEXT(
"\n IDENTIFIER SANITIZATION:\n"
"\n"
"\n Identifiers in Unreal can contain spaces and punctuation marks.\n"
"\n Those punctuation marks could confuse our parsers. For example,"
"\n How would we parse Array<X> if the typename X contained a less-than?"
"\n So, we automatically translate these characters on output:"
"\n"
"\n space → ·"
"\n < → ◁"
"\n > → ▷"
"\n , → ▾"
"\n "
"\n We do the reverse translation on input. Therefore, you will always"
"\n see sanitized versions of identifiers, and you must always use"
"\n sanitized versions of identifiers:"
"\n"
"\n Correct: /Game/Testing/BP_Test,graph:Get·Cursor·Location"
"\n Wrong: /Game/Testing/BP_Test,graph:Get Cursor Location"
"\n"
));
}
}
if (Sections.Contains(Section::Whitespace))
{
UWingServer::Print(TEXT(
"\n ABOUT WHITESPACE:"
"\n Do not put excess whitespace into paths, typenames, or"
"\n function prototypes, only use whitespace where it is required"
"\n by the syntax."
"\n"
));
}
if (Sections.Contains(Section::MaterialEditing))
{
if (Abridged)
{
UWingServer::Print(TEXT(
"\n MATERIAL EDITING:"
"\n We do not expose material expressions directly. Instead, use"
"\n Property_Dump and Property_Set on the material graph nodes to"
"\n edit material expression properties."
"\n"
));
}
else
{
UWingServer::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 mxprop"
"\n properties, which actually come from the material expressions."
"\n You can edit these using Property_Set on the node."
"\n"
));
}
}
if (Sections.Contains(Section::ImportantCommands))
{
UWingServer::Print(TEXT(
"\n COMMANDS YOU SHOULD KNOW ABOUT AND REMEMBER:"
"\n UserManual: this explanation"
"\n ShowCommands: a full list of all the commands"
"\n Blueprint_Dump: a summary of any blueprint"
"\n Graph_Dump: a fairly detailed listing of any Graph"
"\n Property_Dump: show information on many objects"
"\n"
));
}
if (Abridged)
{
UWingServer::Printf(TEXT("\nUse command 'UserManual' to see the full manual.\n"));
}
}

View File

@@ -1,5 +1,6 @@
#include "WingServer.h" #include "WingServer.h"
#include "WingHandler.h" #include "WingHandler.h"
#include "WingManual.h"
#include "WingJson.h" #include "WingJson.h"
#include "WingLogCapture.h" #include "WingLogCapture.h"
#include "WingUtils.h" #include "WingUtils.h"
@@ -263,6 +264,8 @@ FString UWingServer::HandleRequest(const FString& Line)
LogCapture.CapturedErrors.Empty(); LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true; LogCapture.bEnabled = true;
HandlerOutput.Reset(); HandlerOutput.Reset();
SuggestedManualSections.Empty();
LastHandlerClass = nullptr;
TryCallHandler(Line); TryCallHandler(Line);
@@ -273,6 +276,11 @@ FString UWingServer::HandleRequest(const FString& Line)
UWingServer::Printf(TEXT("UE_LOG: %s\n"), *Msg); UWingServer::Printf(TEXT("UE_LOG: %s\n"), *Msg);
} }
LogCapture.CapturedErrors.Empty(); LogCapture.CapturedErrors.Empty();
if (!SuggestedManualSections.IsEmpty())
{
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
WingManual::PrintManual(SuggestedManualSections, LastHandlerClass, true);
}
FString Result = HandlerOutput.ToString(); FString Result = HandlerOutput.ToString();
HandlerOutput.Reset(); HandlerOutput.Reset();
for (int32 i = 0; i < Result.Len(); ++i) for (int32 i = 0; i < Result.Len(); ++i)
@@ -299,6 +307,7 @@ void UWingServer::TryCallHandler(const FString &Line)
if (!Request->TryGetStringField(TEXT("command"), Command)) if (!Request->TryGetStringField(TEXT("command"), Command))
{ {
UWingServer::Printf(TEXT("Request does not contain 'command' parameter")); UWingServer::Printf(TEXT("Request does not contain 'command' parameter"));
UWingServer::Printf(TEXT("We recommend sending command='UserManual'."));
return; return;
} }
Request->RemoveField(TEXT("command")); Request->RemoveField(TEXT("command"));
@@ -308,8 +317,10 @@ void UWingServer::TryCallHandler(const FString &Line)
if (!HandlerClass) if (!HandlerClass)
{ {
UWingServer::Printf(TEXT("Unknown command: %s"), *Command); UWingServer::Printf(TEXT("Unknown command: %s"), *Command);
UWingServer::SuggestManual(WingManual::Section::ImportantCommands);
return; return;
} }
LastHandlerClass = *HandlerClass;
// Make an object of the handler class. // Make an object of the handler class.
TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass)); TStrongObjectPtr<UObject> HandlerObj(NewObject<UObject>(GetTransientPackage(), *HandlerClass));
@@ -318,8 +329,7 @@ void UWingServer::TryCallHandler(const FString &Line)
// Populate the handler object with the request parameters. // Populate the handler object with the request parameters.
if (!WingJson::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request)) if (!WingJson::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request))
{ {
UWingServer::Printf(TEXT("\nUsage:\n\n")); UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
WingUtils::PrintHandlerHelp(*HandlerClass);
return; return;
} }

View File

@@ -302,16 +302,14 @@ FString WingUtils::FormatNodeTitle(const UEdGraphNode *Node)
FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix) FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString& Prefix)
{ {
FString Clean = Text;
Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n"));
TArray<FString> Words; TArray<FString> Words;
Clean.ParseIntoArrayWS(Words); Text.ParseIntoArrayWS(Words);
TStringBuilder<1024> Result; TStringBuilder<1024> Result;
int32 Col = 0; int32 Col = 0;
for (const FString& Word : Words) for (const FString& Word : Words)
{ {
if (Col > 0 && Col + 1 + Word.Len() > ColLimit) if ((Col > 0) && (Col + 1 + Word.Len() > ColLimit))
{ {
Result.Append(TEXT("\n")); Result.Append(TEXT("\n"));
Col = 0; Col = 0;
@@ -319,16 +317,17 @@ FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString&
if (Col == 0) if (Col == 0)
{ {
Result.Append(Prefix); Result.Append(Prefix);
Col = Prefix.Len(); Result.Append(Word);
Col = Prefix.Len() + Word.Len();
} }
else else
{ {
Result.Append(TEXT(" ")); Result.Append(TEXT(" "));
Col += 1; Result.Append(Word);
Col = 1 + Word.Len();
} }
Result.Append(Word);
Col += Word.Len();
} }
if (Col > 0) Result.Append(TEXT("\n"));
return Result.ToString(); return Result.ToString();
} }
@@ -616,48 +615,3 @@ FString WingUtils::GetHandlerGroup(UClass* HandlerClass)
return Name.Left(UnderscoreIdx); return Name.Left(UnderscoreIdx);
return Name; return Name;
} }
// ============================================================
// PrintHandlerHelp — verbose description of one handler command
// ============================================================
void WingUtils::PrintHandlerHelp(UClass* HandlerClass)
{
const IWingHandler* Handler = Cast<IWingHandler>(HandlerClass->GetDefaultObject());
if (!Handler) return;
FString ToolName = GetHandlerName(HandlerClass);
UWingServer::Print(TEXT("\n"));
UWingServer::Print(WrapText(Handler->GetDescription(), 80, TEXT("// ")));
UWingServer::Print(TEXT("\n"));
// Command signature line
UWingServer::Print(ToolName);
UWingServer::Print(TEXT("("));
bool bFirst = true;
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
{
if (!bFirst) UWingServer::Print(TEXT(","));
bFirst = false;
if (PropIt->HasMetaData(TEXT("Optional"))) UWingServer::Print(TEXT("?"));
UWingServer::Print(PropIt->GetName());
}
UWingServer::Print(TEXT(")\n"));
// parameter details
for (TFieldIterator<FProperty> PropIt(HandlerClass, EFieldIterationFlags::None); PropIt; ++PropIt)
{
FProperty* Prop = *PropIt;
FString Name = Prop->GetName();
FString Type = UWingTypes::TypeToText(Prop);
bool bOptional = Prop->HasMetaData(TEXT("Optional"));
const FString& Desc = Prop->GetMetaData(TEXT("Description"));
UWingServer::Printf(TEXT(" %s %s%s"),
*Type, *Name, bOptional ? TEXT(" (optional)") : TEXT(""));
if (!Desc.IsEmpty())
UWingServer::Printf(TEXT(" — %s"), *Desc);
UWingServer::Print(TEXT("\n"));
}
}

View File

@@ -36,10 +36,7 @@ struct FWalker;
class WingFetcher class WingFetcher
{ {
public: public:
// Print a general explanation of what paths look like.
static void PrintPathExplanation();
// Walk a path from an asset to an object // Walk a path from an asset to an object
// within that asset. If you call walk a // within that asset. If you call walk a
// second time, it will walk additional steps. // second time, it will walk additional steps.

View File

@@ -0,0 +1,25 @@
#pragma once
#include "Containers/Set.h"
class WingManual
{
public:
enum class Section
{
HandlerHelp,
Paths,
Types,
FunctionArguments,
IdentifierSanitization,
Whitespace,
MaterialEditing,
ImportantCommands,
};
static TSet<Section> AllSections();
static void PrintHandlerPrototype(UClass *Handler);
static void PrintHandlerArguments(UClass *Handler);
static void PrintHandlerDescription(UClass *Handler);
static void PrintHandlerHelp(UClass *Handler);
static void PrintManual(TSet<Section> Sections, UClass *Handler, bool Abridged);
};

View File

@@ -9,6 +9,7 @@
#include "WingUtils.h" #include "WingUtils.h"
#include "WingNotifier.h" #include "WingNotifier.h"
#include "WingLogCapture.h" #include "WingLogCapture.h"
#include "WingManual.h"
#include "WingServer.generated.h" #include "WingServer.generated.h"
class FSocket; class FSocket;
@@ -59,6 +60,9 @@ public:
GWingServer->HandlerOutput.Appendf(Fmt, Forward<ArgTypes>(Args)...); GWingServer->HandlerOutput.Appendf(Fmt, Forward<ArgTypes>(Args)...);
} }
/** Suggest that a manual section be printed after the handler finishes. */
static void SuggestManual(WingManual::Section Section) { GWingServer->SuggestedManualSections.Add(Section); }
/** Whether the server is currently listening. */ /** Whether the server is currently listening. */
bool IsRunning() const { return bRunning; } bool IsRunning() const { return bRunning; }
@@ -71,7 +75,9 @@ private:
// ----- Tool dispatch ----- // ----- Tool dispatch -----
UPROPERTY() UPROPERTY()
FWingNotifier Notifier; FWingNotifier Notifier;
UClass* LastHandlerClass;
TStringBuilder<16384> HandlerOutput; TStringBuilder<16384> HandlerOutput;
TSet<WingManual::Section> SuggestedManualSections;
FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request
UPROPERTY() UPROPERTY()
TMap<FString, TObjectPtr<UClass>> WingHandlerRegistry; // tool name -> UWingHandler subclass TMap<FString, TObjectPtr<UClass>> WingHandlerRegistry; // tool name -> UWingHandler subclass

View File

@@ -230,7 +230,6 @@ public:
static TArray<UClass*> CollectHandlerClasses(); static TArray<UClass*> CollectHandlerClasses();
static FString GetHandlerName(UClass* HandlerClass); static FString GetHandlerName(UClass* HandlerClass);
static FString GetHandlerGroup(UClass* HandlerClass); static FString GetHandlerGroup(UClass* HandlerClass);
static void PrintHandlerHelp(UClass* HandlerClass);
// ----- Reparent validation ----- // ----- Reparent validation -----
static bool CanReparentBlueprint(UClass* CurrentGenerated, UClass* Proposed); static bool CanReparentBlueprint(UClass* CurrentGenerated, UClass* Proposed);