diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h index 92e82705..792fee44 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Graph_Dump.h @@ -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()); } diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/ShowCommands.h b/Plugins/UEWingman/Source/UEWingman/Handlers/ShowCommands.h index b53c8886..3aff0e63 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/ShowCommands.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/ShowCommands.h @@ -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 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")); - } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h b/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h index bd629afe..675671a8 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h @@ -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, Class, SoftClass" - "\n Array, Set, Map" - "\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 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 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); } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp index 3ef2554b..6f24d9af 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp @@ -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 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); diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp index eda908f6..e51e09d7 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingGraphExport.cpp @@ -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 ContainedNames; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp new file mode 100644 index 00000000..1e6a2339 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp @@ -0,0 +1,305 @@ +#include "WingManual.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingTypes.h" + +TSet 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 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 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(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
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, Set, Map" + "\n Soft, Class, SoftClass" + "\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, Set, Map" + "\n Soft, Class, SoftClass" + "\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 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 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 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")); + } +} diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp index cd066873..af6fca35 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingServer.cpp @@ -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 HandlerObj(NewObject(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; } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp index d1571f6d..39229024 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -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 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(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 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 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")); - } -} diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingFetcher.h b/Plugins/UEWingman/Source/UEWingman/Public/WingFetcher.h index 18ceb4f9..b98d906a 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingFetcher.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingFetcher.h @@ -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. diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingManual.h b/Plugins/UEWingman/Source/UEWingman/Public/WingManual.h new file mode 100644 index 00000000..b0459f11 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingManual.h @@ -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
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
Sections, UClass *Handler, bool Abridged); +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h b/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h index 2bfb254f..9ce57f3a 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingServer.h @@ -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(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 SuggestedManualSections; FLogCaptureOutputDevice LogCapture; // installed once at startup, enabled per-request UPROPERTY() TMap> WingHandlerRegistry; // tool name -> UWingHandler subclass diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h index a57c747d..a65879d4 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h @@ -230,7 +230,6 @@ public: static TArray 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);