Replace delimiters with unicode shapes

This commit is contained in:
2026-03-19 00:40:27 -04:00
parent 467c1464aa
commit e9ad41bbb3
9 changed files with 91 additions and 60 deletions

View File

@@ -56,17 +56,9 @@ public:
}
// Check for duplicate component names
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes();
for (USCS_Node* Existing : ExistingNodes)
{
if (Existing && Existing->ComponentTemplate &&
WingUtils::Identifies(Component, Existing->ComponentTemplate))
{
UWingServer::Printf(TEXT("ERROR: A component named '%s' already exists in Blueprint '%s'\n"),
*Component, *WingUtils::FormatName(BP));
return;
}
}
const TArray<USCS_Node*>& Existing = SCS->GetAllNodes();
if (!WingUtils::FindExactlyNoneNamed(Component, Existing))
return;
// Resolve the component class by name
UClass* ComponentClassObj = UWingTypes::TextToOneObjectType(ComponentClass);
@@ -81,7 +73,7 @@ public:
USCS_Node* ParentSCSNode = nullptr;
if (!ParentComponent.IsEmpty())
{
for (USCS_Node* Node : ExistingNodes)
for (USCS_Node* Node : Existing)
{
if (Node && Node->ComponentTemplate &&
WingUtils::Identifies(ParentComponent, Node->ComponentTemplate))

View File

@@ -17,7 +17,7 @@ public:
UPROPERTY(meta=(Description="Path to a graph node (function entry, function result, custom event, or tunnel)"))
FString Node;
UPROPERTY(meta=(Description="Comma-separated args, e.g. 'int x, float y'"))
UPROPERTY(meta=(Description="Args e.g. 'int◦x│floaty'"))
FString Args;
virtual FString GetDescription() const override

View File

@@ -22,10 +22,10 @@ public:
"\n PATHS:"
"\n"
"\n Most commands require you to specify a path. A path starts"
"\n with an asset name, followed by comma-separated steps that"
"\n navigate into the asset. Example:"
"\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 /Game/Widgets/WB_Hotkeysgraph:EventGraphnode:Self03pin:Result"
"\n"
"\n The navigation steps supported are:"
"\n"
@@ -38,30 +38,30 @@ public:
"\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 /Game/Materials/MyMaterialgraph"
"\n"
"\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 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 Softabp_manny, Classpawn, SoftClasspawn"
"\n Arrayint, Setstring, Mapintstring"
"\n"
"\n FUNCTION ARGUMENTS AND RETURN VALUES:"
"\n"
"\n Function argument lists are expressed as comma-separated"
"\n lists containing type and variable name:"
"\n Function argument lists are expressed as -separated"
"\n lists of type◦name pairs:"
"\n"
"\n double D, PlayerController P, array<int> A"
"\n double◦D│PlayerController◦P│Arrayint▸◦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."
@@ -70,6 +70,14 @@ public:
"\n before you can set return values. Custom event nodes also have"
"\n editable arguments."
"\n"
"\n ABOUT UNICODE AND WHITESPACE:"
"\n"
"\n We use unicode geometric shapes as delimiters in"
"\n some places, such as in typenames, function arguments, and"
"\n paths (see above). This is intentional, you really"
"\n do have to use those delimiters. Do not put excess"
"\n whitespace into paths or typenames."
"\n"
"\n MATERIAL EDITING:"
"\n"
"\n We do not expose material expressions directly. Instead, you"

View File

@@ -77,7 +77,7 @@ WingFetcher& WingFetcher::Walk(const FString& Path)
if (bError) return *this;
TArray<FString> Segments;
Path.ParseIntoArray(Segments, TEXT(","));
Path.ParseIntoArray(Segments, TEXT(""));
if (Segments.Num() == 0)
{
UWingServer::Print(TEXT("ERROR: Empty path\n"));

View File

@@ -20,8 +20,8 @@ FString WingFunctionArgs::GetArgs(UEdGraphNode* Node)
TStringBuilder<256> SB;
for (const TSharedPtr<FUserPinInfo>& Pin : Editable->UserDefinedPins)
{
if (SB.Len() > 0) SB << TEXT(", ");
SB << UWingTypes::TypeToText(Pin->PinType) << TEXT(" ") << Pin->PinName.ToString();
if (SB.Len() > 0) SB << TEXT("");
SB << UWingTypes::TypeToText(Pin->PinType) << TEXT("") << Pin->PinName.ToString();
}
return FString(SB);
}
@@ -42,23 +42,23 @@ bool WingFunctionArgs::ParseArgs(const FString& Args, TArray<FParsedArg>& OutArg
if (Trimmed.IsEmpty()) return true;
TArray<FString> Parts;
Trimmed.ParseIntoArray(Parts, TEXT(","));
Trimmed.ParseIntoArray(Parts, TEXT(""));
for (const FString& Part : Parts)
{
FString Token = Part.TrimStartAndEnd();
if (Token.IsEmpty()) continue;
// Split "type name" on the last space.
int32 LastSpace;
if (!Token.FindLastChar(TEXT(' '), LastSpace))
// Split "typename" on the white bullet.
FString TypeStr, NameStr;
if (!Token.Split(TEXT(""), &TypeStr, &NameStr))
{
UWingServer::Printf(TEXT("ERROR: Expected 'type name' but got '%s'\n"), *Token);
UWingServer::Printf(TEXT("ERROR: Expected 'typename' but got '%s'\n"), *Token);
return false;
}
FString TypeStr = Token.Left(LastSpace).TrimStartAndEnd();
FString NameStr = Token.Mid(LastSpace + 1).TrimStartAndEnd();
TypeStr.TrimStartAndEndInline();
NameStr.TrimStartAndEndInline();
if (TypeStr.IsEmpty() || NameStr.IsEmpty())
{

View File

@@ -149,11 +149,11 @@ FString UWingTypes::TypeToTextInner(FName Category, FName SubCategory, UObject*
if (Category == UEdGraphSchema_K2::PC_Object)
return Short;
if (Category == UEdGraphSchema_K2::PC_Class)
return FString::Printf(TEXT("Class<%s>"), *Short);
return FString::Printf(TEXT("Class%s"), *Short);
if (Category == UEdGraphSchema_K2::PC_SoftObject)
return FString::Printf(TEXT("Soft<%s>"), *Short);
return FString::Printf(TEXT("Soft%s"), *Short);
if (Category == UEdGraphSchema_K2::PC_SoftClass)
return FString::Printf(TEXT("SoftClass<%s>"), *Short);
return FString::Printf(TEXT("SoftClass%s"), *Short);
if (Category == UEdGraphSchema_K2::PC_Interface)
return Short;
}
@@ -171,9 +171,9 @@ FString UWingTypes::TypeToText(const FEdGraphPinType& PinType)
return FString();
if (PinType.IsArray())
return FString::Printf(TEXT("Array<%s>"), *Inner);
return FString::Printf(TEXT("Array%s"), *Inner);
if (PinType.IsSet())
return FString::Printf(TEXT("Set<%s>"), *Inner);
return FString::Printf(TEXT("Set%s"), *Inner);
if (PinType.IsMap())
{
FString ValueInner = Types->TypeToTextInner(
@@ -182,7 +182,7 @@ FString UWingTypes::TypeToText(const FEdGraphPinType& PinType)
PinType.PinValueType.TerminalSubCategoryObject.Get());
if (ValueInner.IsEmpty())
return FString();
return FString::Printf(TEXT("Map<%s, %s>"), *Inner, *ValueInner);
return FString::Printf(TEXT("Map%s%s"), *Inner, *ValueInner);
}
return Inner;
@@ -428,29 +428,29 @@ bool UWingTypes::ParsePlainIdentifier(FEdGraphPinType& OutType)
bool UWingTypes::ParseWrapped(FName Wrapper, FEdGraphPinType& OutType)
{
Cursor++;
if (!ParseChar('<')) return false;
if (!ParseChar(L'')) return false;
if (!ParsePlainIdentifier(OutType)) return false;
if (!Cast<UClass>(OutType.PinSubCategoryObject))
{
Error = FString::Printf(TEXT("%s is not a Class"), *OutType.PinSubCategoryObject->GetName());
return false;
}
if (!ParseChar('>')) return false;
if (!ParseChar(L'')) return false;
OutType.PinCategory = Wrapper;
return true;
}
bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType)
{
if (TokenIs(TEXT("Soft"), '<'))
if (TokenIs(TEXT("Soft"), L''))
{
return ParseWrapped(UEdGraphSchema_K2::PC_SoftObject, OutType);
}
else if (TokenIs(TEXT("Class"), '<'))
else if (TokenIs(TEXT("Class"), L''))
{
return ParseWrapped(UEdGraphSchema_K2::PC_Class, OutType);
}
else if (TokenIs(TEXT("SoftClass"), '<'))
else if (TokenIs(TEXT("SoftClass"), L''))
{
return ParseWrapped(UEdGraphSchema_K2::PC_SoftClass, OutType);
}
@@ -460,40 +460,40 @@ bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType)
bool UWingTypes::ParseArrayOrSet(FEdGraphPinType& OutType)
{
Cursor++;
if (!ParseChar('<')) return false;
if (!ParseChar(L'')) return false;
if (!ParseMaybeWrapped(OutType)) return false;
if (!ParseChar('>')) return false;
if (!ParseChar(L'')) return false;
return true;
}
bool UWingTypes::ParseMap(FEdGraphPinType& OutType)
{
Cursor++;
if (!ParseChar('<')) return false;
if (!ParseChar(L'')) return false;
if (!ParsePlainIdentifier(OutType)) return false;
if (!ParseChar(',')) return false;
if (!ParseChar(L'')) return false;
FEdGraphPinType ValueType;
if (!ParseMaybeWrapped(ValueType)) return false;
OutType.PinValueType.TerminalCategory = ValueType.PinCategory;
OutType.PinValueType.TerminalSubCategory = ValueType.PinSubCategory;
OutType.PinValueType.TerminalSubCategoryObject = ValueType.PinSubCategoryObject;
if (!ParseChar('>')) return false;
if (!ParseChar(L'')) return false;
return true;
}
bool UWingTypes::ParseType(FEdGraphPinType& OutType)
{
if (TokenIs(TEXT("Array"), '<'))
if (TokenIs(TEXT("Array"), L''))
{
OutType.ContainerType = EPinContainerType::Array;
if (!ParseArrayOrSet(OutType)) return false;
}
else if (TokenIs(TEXT("Set"), '<'))
else if (TokenIs(TEXT("Set"), L''))
{
OutType.ContainerType = EPinContainerType::Set;
if (!ParseArrayOrSet(OutType)) return false;
}
else if (TokenIs(TEXT("Map"), '<'))
else if (TokenIs(TEXT("Map"), L''))
{
OutType.ContainerType = EPinContainerType::Map;
if (!ParseMap(OutType)) return false;

View File

@@ -51,7 +51,16 @@ extern int32 TrySavePackageSEH(
#endif
// ============================================================
// Name Formatting
// Name sanitization
//
// Our parsers use Unicode geometric shape delimiters that
// are unlikely to appear in names: ◂▸ for type brackets, ◆
// for map key◆value, ◦ for type◦name in prototypes, │ for
// list/path separation. If any of these characters appear
// in a name, we replace them with safe ASCII equivalents.
// One consequence of name sanitization is that we can't use
// Unreal's name-lookup routines — the LLM only sees
// sanitized names. So we do our own name lookups.
// ============================================================
void WingUtils::SanitizeNameInPlace(FString &Name)
@@ -61,7 +70,11 @@ void WingUtils::SanitizeNameInPlace(FString &Name)
{
TCHAR c = Name[Src];
if (c < 0x20 || c == 0x7F) continue;
if ((c == ' ') || (c == ',') || (c == ':')) c = '_';
if (c == L'') c='<';
if (c == L'') c='>';
if (c == L'') c='*';
if (c == L'') c='.';
if (c == L'') c = '|';
Name[Dst++] = c;
}
if (Dst == 0) Name[Dst++] = '_';
@@ -82,6 +95,10 @@ FString WingUtils::SanitizeName(FName Name)
return Result;
}
// ============================================================
// Name Lookup
// ============================================================
FString WingUtils::FormatName(const UWorld *World)
{
return World->GetPathName();

View File

@@ -13,10 +13,10 @@ struct WingFunctionArgs
// CustomEvent, or Tunnel).
static bool HasArgs(UEdGraphNode* Node);
// Returns the user-defined pins as a string like "int x, float y".
// Returns the user-defined pins as a string like "int◦x│floaty".
static FString GetArgs(UEdGraphNode* Node);
// Sets the user-defined pins from a string like "int x, float y".
// Sets the user-defined pins from a string like "int◦x│floaty".
// Returns true on success.
static bool SetArgs(UEdGraphNode* Node, const FString& Args);
@@ -28,7 +28,7 @@ private:
FName PinName;
};
// Parse "int x, float y" into an array of FParsedArg.
// Parse "int◦x│floaty" into an array of FParsedArg.
// Returns false and prints an error on failure.
static bool ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs);