Better error reporting in WingFetcher

This commit is contained in:
2026-03-25 17:13:00 -04:00
parent 89231987f2
commit bcb79ad0ab
5 changed files with 162 additions and 117 deletions

View File

@@ -3,6 +3,7 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "WingHandler.h" #include "WingHandler.h"
#include "WingServer.h" #include "WingServer.h"
#include "WingFetcher.h"
#include "UserManual.generated.h" #include "UserManual.generated.h"
UCLASS() UCLASS()
@@ -18,28 +19,8 @@ public:
virtual void Handle() override virtual void Handle() override
{ {
WingFetcher::PrintPathExplanation();
UWingServer::Print(TEXT( 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. Example:"
"\n"
"\n /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result"
"\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"
"\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"
"\n TYPES:" "\n TYPES:"
"\n" "\n"
"\n To change variable types, or function prototypes, you will" "\n To change variable types, or function prototypes, you will"
@@ -70,6 +51,25 @@ public:
"\n before you can set return values. Custom event nodes also have" "\n before you can set return values. Custom event nodes also have"
"\n editable arguments." "\n editable arguments."
"\n" "\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 ABOUT WHITESPACE:"
"\n" "\n"
"\n Do not put excess whitespace into paths, typenames, or" "\n Do not put excess whitespace into paths, typenames, or"

View File

@@ -19,6 +19,38 @@
#include "Components/Widget.h" #include "Components/Widget.h"
#include "Subsystems/AssetEditorSubsystem.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"
));
}
WingFetcher::WalkFunc WingFetcher::GetWalker(const FString& Step) WingFetcher::WalkFunc WingFetcher::GetWalker(const FString& Step)
{ {
if (Step.Equals(TEXT("graph"), ESearchCase::IgnoreCase)) return &WingFetcher::Graph; if (Step.Equals(TEXT("graph"), ESearchCase::IgnoreCase)) return &WingFetcher::Graph;
@@ -56,24 +88,24 @@ WingFetcher& WingFetcher::SetError()
void WingFetcher::PathFailed(const TCHAR* Expected) void WingFetcher::PathFailed(const TCHAR* Expected)
{ {
SetError();
if (ResultPin) if (ResultPin)
UWingServer::Printf(TEXT("ERROR: Path specifies a pin, but expected %s\n"), Expected); UWingServer::Printf(TEXT("ERROR: Path specifies a pin, but expected %s\n"), Expected);
else if (Obj) else if (Obj)
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"));
SetError();
} }
WingFetcher& WingFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expected) WingFetcher& WingFetcher::TypeMismatch(const TCHAR* Walker, const TCHAR* Expected)
{ {
SetError();
if (ResultPin) if (ResultPin)
UWingServer::Printf(TEXT("ERROR: Input to '%s' is a pin, but expected %s\n"), Walker, Expected); UWingServer::Printf(TEXT("ERROR: Input to '%s' is a pin, but expected %s\n"), Walker, Expected);
else if (Obj) else if (Obj)
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"));
SetError();
return *this; return *this;
} }
@@ -119,6 +151,13 @@ WingFetcher& WingFetcher::Asset(const FString& PackagePath)
{ {
if (bError) return *this; if (bError) return *this;
if (!PackagePath.StartsWith(TEXT("/")))
{
UWingServer::Printf(TEXT("ERROR: Asset path must start with '/', got '%s'\n"), *PackagePath);
PrintPathExplanation();
return SetError();
}
// Check if the package exists before calling LoadObject, because // Check if the package exists before calling LoadObject, because
// LoadObject logs its own errors when the package doesn't exist. // LoadObject logs its own errors when the package doesn't exist.
FString PackageName = FPackageName::ObjectPathToPackageName(PackagePath); FString PackageName = FPackageName::ObjectPathToPackageName(PackagePath);
@@ -182,7 +221,8 @@ WingFetcher& WingFetcher::Graph(const FString& Value)
{ {
if (!Value.IsEmpty()) if (!Value.IsEmpty())
{ {
UWingServer::Printf(TEXT("ERROR: Materials do not have named graphs (got '%s')\n"), *Value); UWingServer::Printf(TEXT("ERROR: Materials have only one graph, with a blank name.\n\n"));
PrintPathExplanation();
return SetError(); return SetError();
} }
WingUtils::EnsureMaterialGraph(Mat); WingUtils::EnsureMaterialGraph(Mat);
@@ -197,12 +237,26 @@ WingFetcher& WingFetcher::Graph(const FString& Value)
UBlueprint* BP = ::Cast<UBlueprint>(Obj); UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP) if (!BP)
return TypeMismatch(TEXT("graph"), TEXT("Blueprint or Material")); {
TypeMismatch(TEXT("graph"), TEXT("Blueprint or Material"));
PrintPathExplanation();
return SetError();
}
UEdGraph* Graph = WingUtils::FindExactlyOneNamed(Value, WingUtils::AllGraphs(BP), TEXT("Graph")); TArray<UEdGraph*> Graphs = WingUtils::AllGraphs(BP);
if (!Graph) return SetError(); UEdGraph* Found = WingUtils::FindExactlyOneNamed(Value, Graphs, TEXT("graph"));
if (!Found)
{
UWingServer::Printf(TEXT("Graphs that exist in blueprint:"));
for (const UEdGraph *G : Graphs)
{
UWingServer::Printf(TEXT(" %s"), *WingUtils::FormatName(G));
}
UWingServer::Printf(TEXT("\n"));
return SetError();
}
SetObj(Graph); SetObj(Found);
return *this; return *this;
} }
@@ -210,58 +264,30 @@ WingFetcher& WingFetcher::Node(const FString& Value)
{ {
if (bError) return *this; if (bError) return *this;
// If current object is a graph, search that graph // If current object is not a graph, refuse.
if (UEdGraph* G = ::Cast<UEdGraph>(Obj)) UEdGraph *Graph = ::Cast<UEdGraph>(Obj);
if (Graph == nullptr)
{ {
UEdGraphNode* Found = nullptr; TypeMismatch(TEXT("node"), TEXT("Graph"));
for (UEdGraphNode* N : G->Nodes) PrintPathExplanation();
{ return SetError();
if (!N || !WingUtils::Identifies(Value, N))
continue;
if (Found)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous node '%s' in graph %s\n"), *Value, *G->GetName());
return SetError();
}
Found = N;
}
if (!Found)
{
UWingServer::Printf(TEXT("ERROR: Node '%s' not found in graph %s\n"), *Value, *G->GetName());
return SetError();
}
SetObj(Found);
return *this;
} }
// If current object is a blueprint, search all graphs // Get the nodes from the graph.
if (UBlueprint* BP = ::Cast<UBlueprint>(Obj)) TArray<UEdGraphNode *> AllNodes = WingUtils::AllNodes(Graph);
UEdGraphNode *Node = WingUtils::FindExactlyOneNamed(Value, AllNodes, TEXT("node"));
if (Node == nullptr)
{ {
UEdGraphNode* Found = nullptr; UWingServer::Printf(TEXT("Nodes that exist in graph:"));
for (UEdGraph* G : WingUtils::AllGraphs(BP)) for (const UEdGraphNode *N : AllNodes)
{ {
for (UEdGraphNode* N : G->Nodes) UWingServer::Printf(TEXT(" %s"), *WingUtils::FormatName(N));
{
if (!N || !WingUtils::Identifies(Value, N))
continue;
if (Found)
{
UWingServer::Printf(TEXT("ERROR: Ambiguous node '%s' in %s\n"), *Value, *BP->GetName());
return SetError();
}
Found = N;
}
} }
if (!Found) UWingServer::Printf(TEXT("\n"));
{ return SetError();
UWingServer::Printf(TEXT("ERROR: Node '%s' not found in %s\n"), *Value, *BP->GetName());
return SetError();
}
SetObj(Found);
return *this;
} }
SetObj(Node);
return TypeMismatch(TEXT("node"), TEXT("graph or Blueprint")); return *this;
} }
WingFetcher& WingFetcher::Pin(const FString& Value) WingFetcher& WingFetcher::Pin(const FString& Value)
@@ -270,27 +296,22 @@ WingFetcher& WingFetcher::Pin(const FString& Value)
UEdGraphNode* N = ::Cast<UEdGraphNode>(Obj); UEdGraphNode* N = ::Cast<UEdGraphNode>(Obj);
if (!N) if (!N)
return TypeMismatch(TEXT("pin"), TEXT("node"));
UEdGraphPin* Found = nullptr;
for (UEdGraphPin *P : N->Pins)
{ {
if (!WingUtils::Identifies(Value, P)) TypeMismatch(TEXT("pin"), TEXT("node"));
continue; PrintPathExplanation();
if (Found) return SetError();
{ }
UWingServer::Printf(TEXT("ERROR: Ambiguous pin '%s' on node %s\n"), UEdGraphPin *Found = WingUtils::FindExactlyOneNamed(Value, N->Pins, TEXT("pin"));
*Value, *WingUtils::FormatName(N)); if (!Found)
return SetError(); {
} UWingServer::Printf(TEXT("Pins that exist in the node:"));
Found = P; for (const UEdGraphPin *P : N->Pins)
} {
if (!Found) UWingServer::Printf(TEXT(" %s"), *WingUtils::FormatName(P));
{ }
UWingServer::Printf(TEXT("ERROR: Pin '%s' not found on node %s\n"), UWingServer::Printf(TEXT("\n"));
*Value, *WingUtils::FormatName(N));
return SetError(); return SetError();
} }
SetPin(Found); SetPin(Found);
return *this; return *this;
} }
@@ -301,11 +322,25 @@ WingFetcher& WingFetcher::Component(const FString& Value)
UBlueprint* BP = ::Cast<UBlueprint>(Obj); UBlueprint* BP = ::Cast<UBlueprint>(Obj);
if (!BP) if (!BP)
return TypeMismatch(TEXT("component"), TEXT("Blueprint")); {
TypeMismatch(TEXT("component"), TEXT("Blueprint"));
PrintPathExplanation();
return SetError();
}
TArray<FWingActorComponent> AllComponents = FWingActorComponent::GetAll(BP); TArray<FWingActorComponent> AllComponents = FWingActorComponent::GetAll(BP);
FWingActorComponent* Comp = WingUtils::FindExactlyOneNamed(Value, AllComponents, TEXT("Component")); FWingActorComponent* Comp = WingUtils::FindExactlyOneNamed(Value, AllComponents, TEXT("component"));
if (!Comp) return SetError(); if (!Comp)
{
UWingServer::Printf(TEXT("Components that exist in the blueprint:"));
for (const FWingActorComponent &C : AllComponents)
{
UWingServer::Printf(TEXT(" %s"), *WingUtils::FormatName(C));
}
UWingServer::Printf(TEXT("\n"));
return SetError();
}
if (!Comp->IsOwnedBy(BP)) if (!Comp->IsOwnedBy(BP))
{ {
UWingServer::Printf(TEXT("ERROR: Component '%s' belongs to %s, to edit it, you must go through that blueprint.\n"), UWingServer::Printf(TEXT("ERROR: Component '%s' belongs to %s, to edit it, you must go through that blueprint.\n"),
@@ -322,13 +357,25 @@ WingFetcher& WingFetcher::Widget(const FString& Value)
UWidgetBlueprint* WidgetBP = ::Cast<UWidgetBlueprint>(Obj); UWidgetBlueprint* WidgetBP = ::Cast<UWidgetBlueprint>(Obj);
if (!WidgetBP) if (!WidgetBP)
return TypeMismatch(TEXT("widget"), TEXT("WidgetBlueprint")); {
TypeMismatch(TEXT("widget"), TEXT("WidgetBlueprint"));
PrintPathExplanation();
return SetError();
}
TArray<UWidget*> AllWidgets; TArray<UWidget*> AllWidgets;
WidgetBP->WidgetTree->GetAllWidgets(AllWidgets); WidgetBP->WidgetTree->GetAllWidgets(AllWidgets);
UWidget* Found = WingUtils::FindExactlyOneNamed(Value, AllWidgets, TEXT("Widget")); UWidget* Found = WingUtils::FindExactlyOneNamed(Value, AllWidgets, TEXT("widget"));
if (!Found) return SetError(); if (!Found)
{
UWingServer::Printf(TEXT("Widgets that exist in the blueprint:"));
for (const UWidget *W : AllWidgets)
{
UWingServer::Printf(TEXT(" %s"), *WingUtils::FormatName(W));
}
UWingServer::Printf(TEXT("\n"));
return SetError();
}
SetObj(Found); SetObj(Found);
return *this; return *this;
} }
@@ -339,7 +386,11 @@ WingFetcher& WingFetcher::LevelBlueprint(const FString& Value)
UWorld* World = ::Cast<UWorld>(Obj); UWorld* World = ::Cast<UWorld>(Obj);
if (!World) if (!World)
return TypeMismatch(TEXT("levelblueprint"), TEXT("World")); {
TypeMismatch(TEXT("levelblueprint"), TEXT("world"));
PrintPathExplanation();
return SetError();
}
if (!World->PersistentLevel) if (!World->PersistentLevel)
{ {

View File

@@ -459,7 +459,6 @@ TArray<UEdGraph*> WingUtils::AllGraphs(UBlueprint* BP)
return Graphs; return Graphs;
} }
TArray<UEdGraphNode*> WingUtils::AllNodes(UBlueprint* BP) TArray<UEdGraphNode*> WingUtils::AllNodes(UBlueprint* BP)
{ {
TArray<UEdGraphNode*> Nodes; TArray<UEdGraphNode*> Nodes;
@@ -468,6 +467,13 @@ TArray<UEdGraphNode*> WingUtils::AllNodes(UBlueprint* BP)
return Nodes; return Nodes;
} }
TArray<UEdGraphNode*> WingUtils::AllNodes(UEdGraph *Graph)
{
TArray<UEdGraphNode*> Result;
Result.Append(Graph->Nodes);
return Result;
}
// ============================================================ // ============================================================
// Material helpers // Material helpers
// ============================================================ // ============================================================

View File

@@ -37,6 +37,9 @@ 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

@@ -207,23 +207,8 @@ public:
// ----- Blueprint helpers ----- // ----- Blueprint helpers -----
static TArray<UEdGraph*> AllGraphs(UBlueprint* BP); static TArray<UEdGraph*> AllGraphs(UBlueprint* BP);
static TArray<UEdGraphNode*> AllNodes(UBlueprint* BP); static TArray<UEdGraphNode*> AllNodes(UBlueprint* BP);
template<class T> static TArray<T*> AllNodes(UBlueprint* BP) static TArray<UEdGraphNode*> AllNodes(UEdGraph *Graph);
{
TArray<T*> Result;
for (UEdGraph* Graph : AllGraphs(BP))
for (UEdGraphNode* Node : Graph->Nodes)
if (T* Typed = Cast<T>(Node))
Result.Add(Typed);
return Result;
}
template<class T> static TArray<T*> AllNodes(UEdGraph* Graph)
{
TArray<T*> Result;
for (UEdGraphNode* Node : Graph->Nodes)
if (T* Typed = Cast<T>(Node))
Result.Add(Typed);
return Result;
}
// ----- Material helpers ----- // ----- Material helpers -----
static void EnsureMaterialGraph(UMaterial* Material); static void EnsureMaterialGraph(UMaterial* Material);