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

View File

@@ -6,7 +6,7 @@
#include "WingServer.h"
#include "WingTypes.h"
#include "WingJson.h"
#include "WingUtils.h"
#include "WingManual.h"
#include "ShowCommands.generated.h"
UCLASS()
@@ -26,60 +26,37 @@ public:
return TEXT("List all available commands with their descriptions.");
}
void EmitCommand(UClass* Class)
{
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)
virtual void Handle() override
{
FString QueryLower = Query.ToLower();
FString PrevGroup;
for (UClass* Class : WingUtils::CollectHandlerClasses())
{
bool bIsHalfBaked = Class->GetMetaData(TEXT("ModuleRelativePath")).StartsWith(TEXT("HalfBaked/"));
if (bIsHalfBaked != bHalfBaked)
continue;
FString ToolName = WingUtils::GetHandlerName(Class);
if (!ToolName.ToLower().Contains(QueryLower))
continue;
// Blank line between groups
FString Group = WingUtils::GetHandlerGroup(Class);
if (Group != PrevGroup)
if (!Verbose)
{
if (!PrevGroup.IsEmpty())
UWingServer::Print(TEXT("\n"));
PrevGroup = Group;
FString Group = WingUtils::GetHandlerGroup(Class);
if (Group != PrevGroup)
{
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 "WingHandler.h"
#include "WingServer.h"
#include "WingFetcher.h"
#include "WingManual.h"
#include "UserManual.generated.h"
UCLASS()
@@ -19,78 +18,6 @@ public:
virtual void Handle() override
{
WingFetcher::PrintPathExplanation();
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"
));
WingManual::PrintManual(WingManual::AllSections(), nullptr, false);
}
};

View File

@@ -18,38 +18,8 @@
#include "Blueprint/WidgetTree.h"
#include "Components/Widget.h"
#include "Subsystems/AssetEditorSubsystem.h"
void WingFetcher::PrintPathExplanation()
{
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"
));
}
#include "WingServer.h"
#include "WingManual.h"
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);
else
UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n"));
PrintPathExplanation();
UWingServer::SuggestManual(WingManual::Section::Paths);
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);
else
UWingServer::Printf(TEXT("ERROR: Path led to a null pointer\n"));
PrintPathExplanation();
UWingServer::SuggestManual(WingManual::Section::Paths);
SetError();
return *this;
}
@@ -115,11 +85,19 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
{
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;
Path.ParseIntoArray(Segments, TEXT(","));
if (Segments.Num() == 0)
{
UWingServer::Print(TEXT("ERROR: Empty path\n"));
UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError();
}
@@ -140,6 +118,7 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
if (!Func)
{
UWingServer::Printf(TEXT("ERROR: Unknown path step '%s'\n"), *Key);
UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError();
}
(this->*Func)(Value);
@@ -155,8 +134,8 @@ WingFetcher& WingFetcher::Asset(const FString& PackagePath)
if (!PackagePath.StartsWith(TEXT("/")))
{
UWingServer::Printf(TEXT("ERROR: Asset path must start with '/', got '%s'\n"), *PackagePath);
PrintPathExplanation();
UWingServer::Printf(TEXT("ERROR: Path must start with '/', got '%s'\n"), *PackagePath);
UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError();
}
@@ -224,7 +203,7 @@ WingFetcher& WingFetcher::Graph(const FString& Value)
if (!Value.IsEmpty())
{
UWingServer::Printf(TEXT("ERROR: Materials have only one graph, with a blank name.\n\n"));
PrintPathExplanation();
UWingServer::SuggestManual(WingManual::Section::Paths);
return SetError();
}
WingUtils::EnsureMaterialGraph(Mat);

View File

@@ -341,7 +341,6 @@ void WingGraphExport::EmitComments()
// Emit wrapped, indented body.
Output.Append(WingUtils::WrapText(CommentNode->NodeComment, 70, TEXT(" - ")));
Output.Append(TEXT("\n"));
// Find contained nodes.
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 "WingHandler.h"
#include "WingManual.h"
#include "WingJson.h"
#include "WingLogCapture.h"
#include "WingUtils.h"
@@ -263,6 +264,8 @@ FString UWingServer::HandleRequest(const FString& Line)
LogCapture.CapturedErrors.Empty();
LogCapture.bEnabled = true;
HandlerOutput.Reset();
SuggestedManualSections.Empty();
LastHandlerClass = nullptr;
TryCallHandler(Line);
@@ -273,6 +276,11 @@ FString UWingServer::HandleRequest(const FString& Line)
UWingServer::Printf(TEXT("UE_LOG: %s\n"), *Msg);
}
LogCapture.CapturedErrors.Empty();
if (!SuggestedManualSections.IsEmpty())
{
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
WingManual::PrintManual(SuggestedManualSections, LastHandlerClass, true);
}
FString Result = HandlerOutput.ToString();
HandlerOutput.Reset();
for (int32 i = 0; i < Result.Len(); ++i)
@@ -299,6 +307,7 @@ void UWingServer::TryCallHandler(const FString &Line)
if (!Request->TryGetStringField(TEXT("command"), Command))
{
UWingServer::Printf(TEXT("Request does not contain 'command' parameter"));
UWingServer::Printf(TEXT("We recommend sending command='UserManual'."));
return;
}
Request->RemoveField(TEXT("command"));
@@ -308,8 +317,10 @@ void UWingServer::TryCallHandler(const FString &Line)
if (!HandlerClass)
{
UWingServer::Printf(TEXT("Unknown command: %s"), *Command);
UWingServer::SuggestManual(WingManual::Section::ImportantCommands);
return;
}
LastHandlerClass = *HandlerClass;
// Make an object of the handler class.
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.
if (!WingJson::PopulateFromJson(HandlerObj->GetClass(), HandlerObj.Get(), &*Request))
{
UWingServer::Printf(TEXT("\nUsage:\n\n"));
WingUtils::PrintHandlerHelp(*HandlerClass);
UWingServer::SuggestManual(WingManual::Section::HandlerHelp);
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 Clean = Text;
Clean.ReplaceInline(TEXT("\r\n"), TEXT("\n"));
TArray<FString> Words;
Clean.ParseIntoArrayWS(Words);
Text.ParseIntoArrayWS(Words);
TStringBuilder<1024> Result;
int32 Col = 0;
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"));
Col = 0;
@@ -319,16 +317,17 @@ FString WingUtils::WrapText(const FString& Text, int32 ColLimit, const FString&
if (Col == 0)
{
Result.Append(Prefix);
Col = Prefix.Len();
Result.Append(Word);
Col = Prefix.Len() + Word.Len();
}
else
{
{
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();
}
@@ -616,48 +615,3 @@ FString WingUtils::GetHandlerGroup(UClass* HandlerClass)
return Name.Left(UnderscoreIdx);
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
{
public:
// Print a general explanation of what paths look like.
static void PrintPathExplanation();
public:
// Walk a path from an asset to an object
// within that asset. If you call walk a
// second time, it will walk additional steps.

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 "WingNotifier.h"
#include "WingLogCapture.h"
#include "WingManual.h"
#include "WingServer.generated.h"
class FSocket;
@@ -59,6 +60,9 @@ public:
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. */
bool IsRunning() const { return bRunning; }
@@ -71,7 +75,9 @@ private:
// ----- Tool dispatch -----
UPROPERTY()
FWingNotifier Notifier;
UClass* LastHandlerClass;
TStringBuilder<16384> HandlerOutput;
TSet<WingManual::Section> SuggestedManualSections;
FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request
UPROPERTY()
TMap<FString, TObjectPtr<UClass>> WingHandlerRegistry; // tool name -> UWingHandler subclass

View File

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