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 // Check for duplicate component names
const TArray<USCS_Node*>& ExistingNodes = SCS->GetAllNodes(); const TArray<USCS_Node*>& Existing = SCS->GetAllNodes();
for (USCS_Node* Existing : ExistingNodes) if (!WingUtils::FindExactlyNoneNamed(Component, Existing))
{ return;
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;
}
}
// Resolve the component class by name // Resolve the component class by name
UClass* ComponentClassObj = UWingTypes::TextToOneObjectType(ComponentClass); UClass* ComponentClassObj = UWingTypes::TextToOneObjectType(ComponentClass);
@@ -81,7 +73,7 @@ public:
USCS_Node* ParentSCSNode = nullptr; USCS_Node* ParentSCSNode = nullptr;
if (!ParentComponent.IsEmpty()) if (!ParentComponent.IsEmpty())
{ {
for (USCS_Node* Node : ExistingNodes) for (USCS_Node* Node : Existing)
{ {
if (Node && Node->ComponentTemplate && if (Node && Node->ComponentTemplate &&
WingUtils::Identifies(ParentComponent, 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)")) UPROPERTY(meta=(Description="Path to a graph node (function entry, function result, custom event, or tunnel)"))
FString Node; 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; FString Args;
virtual FString GetDescription() const override virtual FString GetDescription() const override

View File

@@ -22,10 +22,10 @@ public:
"\n PATHS:" "\n PATHS:"
"\n" "\n"
"\n Most commands require you to specify a path. A path starts" "\n Most commands require you to specify a path. A path starts"
"\n with an asset name, followed by comma-separated steps that" "\n with an asset name, followed by steps separated by │"
"\n navigate into the asset. Example:" "\n that navigate into the asset. Example:"
"\n" "\n"
"\n /Game/Widgets/WB_Hotkeys,graph:EventGraph,node:Self03,pin:Result" "\n /Game/Widgets/WB_Hotkeysgraph:EventGraphnode:Self03pin:Result"
"\n" "\n"
"\n The navigation steps supported are:" "\n The navigation steps supported are:"
"\n" "\n"
@@ -38,30 +38,30 @@ public:
"\n Steps do not always require a parameter. For example, materials" "\n Steps do not always require a parameter. For example, materials"
"\n only have one graph, so you can just say:" "\n only have one graph, so you can just say:"
"\n" "\n"
"\n /Game/Materials/MyMaterial,graph" "\n /Game/Materials/MyMaterialgraph"
"\n" "\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"
"\n use our syntax for types. Here are some simple examples:" "\n use our syntax for types. Here are some simple examples:"
"\n" "\n"
"\n boolean, int64, double, string, etc" "\n boolean, int64, double, string, etc."
"\n vector, rotator, hitresult, etc" "\n vector, rotator, hitresult, etc."
"\n actor, character, playercontroller, etc" "\n actor, character, playercontroller, etc."
"\n eblendmode, emovementmode, etc" "\n eblendmode, emovementmode, etc."
"\n" "\n"
"\n Notice that it's 'actor', not 'AActor'." "\n Notice that it's 'actor', not 'AActor'."
"\n You can use the following notations for complex types:" "\n You can use the following notations for complex types:"
"\n" "\n"
"\n soft<abp_manny>, class<pawn>, softclass<pawn>" "\n Softabp_manny, Classpawn, SoftClasspawn"
"\n array<int>, set<string>, map<int, string>" "\n Arrayint, Setstring, Mapintstring"
"\n" "\n"
"\n FUNCTION ARGUMENTS AND RETURN VALUES:" "\n FUNCTION ARGUMENTS AND RETURN VALUES:"
"\n" "\n"
"\n Function argument lists are expressed as comma-separated" "\n Function argument lists are expressed as -separated"
"\n lists containing type and variable name:" "\n lists of type◦name pairs:"
"\n" "\n"
"\n double D, PlayerController P, array<int> A" "\n double◦D│PlayerController◦P│Arrayint▸◦A"
"\n" "\n"
"\n To change the arguments or return values of a function, edit the" "\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 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 before you can set return values. Custom event nodes also have"
"\n editable arguments." "\n editable arguments."
"\n" "\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 MATERIAL EDITING:"
"\n" "\n"
"\n We do not expose material expressions directly. Instead, you" "\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; if (bError) return *this;
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"));

View File

@@ -20,8 +20,8 @@ FString WingFunctionArgs::GetArgs(UEdGraphNode* Node)
TStringBuilder<256> SB; TStringBuilder<256> SB;
for (const TSharedPtr<FUserPinInfo>& Pin : Editable->UserDefinedPins) for (const TSharedPtr<FUserPinInfo>& Pin : Editable->UserDefinedPins)
{ {
if (SB.Len() > 0) SB << TEXT(", "); if (SB.Len() > 0) SB << TEXT("");
SB << UWingTypes::TypeToText(Pin->PinType) << TEXT(" ") << Pin->PinName.ToString(); SB << UWingTypes::TypeToText(Pin->PinType) << TEXT("") << Pin->PinName.ToString();
} }
return FString(SB); return FString(SB);
} }
@@ -42,23 +42,23 @@ bool WingFunctionArgs::ParseArgs(const FString& Args, TArray<FParsedArg>& OutArg
if (Trimmed.IsEmpty()) return true; if (Trimmed.IsEmpty()) return true;
TArray<FString> Parts; TArray<FString> Parts;
Trimmed.ParseIntoArray(Parts, TEXT(",")); Trimmed.ParseIntoArray(Parts, TEXT(""));
for (const FString& Part : Parts) for (const FString& Part : Parts)
{ {
FString Token = Part.TrimStartAndEnd(); FString Token = Part.TrimStartAndEnd();
if (Token.IsEmpty()) continue; if (Token.IsEmpty()) continue;
// Split "type name" on the last space. // Split "typename" on the white bullet.
int32 LastSpace; FString TypeStr, NameStr;
if (!Token.FindLastChar(TEXT(' '), LastSpace)) 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; return false;
} }
FString TypeStr = Token.Left(LastSpace).TrimStartAndEnd(); TypeStr.TrimStartAndEndInline();
FString NameStr = Token.Mid(LastSpace + 1).TrimStartAndEnd(); NameStr.TrimStartAndEndInline();
if (TypeStr.IsEmpty() || NameStr.IsEmpty()) 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) if (Category == UEdGraphSchema_K2::PC_Object)
return Short; return Short;
if (Category == UEdGraphSchema_K2::PC_Class) 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) 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) 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) if (Category == UEdGraphSchema_K2::PC_Interface)
return Short; return Short;
} }
@@ -171,9 +171,9 @@ FString UWingTypes::TypeToText(const FEdGraphPinType& PinType)
return FString(); return FString();
if (PinType.IsArray()) if (PinType.IsArray())
return FString::Printf(TEXT("Array<%s>"), *Inner); return FString::Printf(TEXT("Array%s"), *Inner);
if (PinType.IsSet()) if (PinType.IsSet())
return FString::Printf(TEXT("Set<%s>"), *Inner); return FString::Printf(TEXT("Set%s"), *Inner);
if (PinType.IsMap()) if (PinType.IsMap())
{ {
FString ValueInner = Types->TypeToTextInner( FString ValueInner = Types->TypeToTextInner(
@@ -182,7 +182,7 @@ FString UWingTypes::TypeToText(const FEdGraphPinType& PinType)
PinType.PinValueType.TerminalSubCategoryObject.Get()); PinType.PinValueType.TerminalSubCategoryObject.Get());
if (ValueInner.IsEmpty()) if (ValueInner.IsEmpty())
return FString(); return FString();
return FString::Printf(TEXT("Map<%s, %s>"), *Inner, *ValueInner); return FString::Printf(TEXT("Map%s%s"), *Inner, *ValueInner);
} }
return Inner; return Inner;
@@ -428,29 +428,29 @@ bool UWingTypes::ParsePlainIdentifier(FEdGraphPinType& OutType)
bool UWingTypes::ParseWrapped(FName Wrapper, FEdGraphPinType& OutType) bool UWingTypes::ParseWrapped(FName Wrapper, FEdGraphPinType& OutType)
{ {
Cursor++; Cursor++;
if (!ParseChar('<')) return false; if (!ParseChar(L'')) return false;
if (!ParsePlainIdentifier(OutType)) return false; if (!ParsePlainIdentifier(OutType)) return false;
if (!Cast<UClass>(OutType.PinSubCategoryObject)) if (!Cast<UClass>(OutType.PinSubCategoryObject))
{ {
Error = FString::Printf(TEXT("%s is not a Class"), *OutType.PinSubCategoryObject->GetName()); Error = FString::Printf(TEXT("%s is not a Class"), *OutType.PinSubCategoryObject->GetName());
return false; return false;
} }
if (!ParseChar('>')) return false; if (!ParseChar(L'')) return false;
OutType.PinCategory = Wrapper; OutType.PinCategory = Wrapper;
return true; return true;
} }
bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType) bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType)
{ {
if (TokenIs(TEXT("Soft"), '<')) if (TokenIs(TEXT("Soft"), L''))
{ {
return ParseWrapped(UEdGraphSchema_K2::PC_SoftObject, OutType); 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); 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); return ParseWrapped(UEdGraphSchema_K2::PC_SoftClass, OutType);
} }
@@ -460,40 +460,40 @@ bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType)
bool UWingTypes::ParseArrayOrSet(FEdGraphPinType& OutType) bool UWingTypes::ParseArrayOrSet(FEdGraphPinType& OutType)
{ {
Cursor++; Cursor++;
if (!ParseChar('<')) return false; if (!ParseChar(L'')) return false;
if (!ParseMaybeWrapped(OutType)) return false; if (!ParseMaybeWrapped(OutType)) return false;
if (!ParseChar('>')) return false; if (!ParseChar(L'')) return false;
return true; return true;
} }
bool UWingTypes::ParseMap(FEdGraphPinType& OutType) bool UWingTypes::ParseMap(FEdGraphPinType& OutType)
{ {
Cursor++; Cursor++;
if (!ParseChar('<')) return false; if (!ParseChar(L'')) return false;
if (!ParsePlainIdentifier(OutType)) return false; if (!ParsePlainIdentifier(OutType)) return false;
if (!ParseChar(',')) return false; if (!ParseChar(L'')) return false;
FEdGraphPinType ValueType; FEdGraphPinType ValueType;
if (!ParseMaybeWrapped(ValueType)) return false; if (!ParseMaybeWrapped(ValueType)) return false;
OutType.PinValueType.TerminalCategory = ValueType.PinCategory; OutType.PinValueType.TerminalCategory = ValueType.PinCategory;
OutType.PinValueType.TerminalSubCategory = ValueType.PinSubCategory; OutType.PinValueType.TerminalSubCategory = ValueType.PinSubCategory;
OutType.PinValueType.TerminalSubCategoryObject = ValueType.PinSubCategoryObject; OutType.PinValueType.TerminalSubCategoryObject = ValueType.PinSubCategoryObject;
if (!ParseChar('>')) return false; if (!ParseChar(L'')) return false;
return true; return true;
} }
bool UWingTypes::ParseType(FEdGraphPinType& OutType) bool UWingTypes::ParseType(FEdGraphPinType& OutType)
{ {
if (TokenIs(TEXT("Array"), '<')) if (TokenIs(TEXT("Array"), L''))
{ {
OutType.ContainerType = EPinContainerType::Array; OutType.ContainerType = EPinContainerType::Array;
if (!ParseArrayOrSet(OutType)) return false; if (!ParseArrayOrSet(OutType)) return false;
} }
else if (TokenIs(TEXT("Set"), '<')) else if (TokenIs(TEXT("Set"), L''))
{ {
OutType.ContainerType = EPinContainerType::Set; OutType.ContainerType = EPinContainerType::Set;
if (!ParseArrayOrSet(OutType)) return false; if (!ParseArrayOrSet(OutType)) return false;
} }
else if (TokenIs(TEXT("Map"), '<')) else if (TokenIs(TEXT("Map"), L''))
{ {
OutType.ContainerType = EPinContainerType::Map; OutType.ContainerType = EPinContainerType::Map;
if (!ParseMap(OutType)) return false; if (!ParseMap(OutType)) return false;

View File

@@ -51,7 +51,16 @@ extern int32 TrySavePackageSEH(
#endif #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) void WingUtils::SanitizeNameInPlace(FString &Name)
@@ -61,7 +70,11 @@ void WingUtils::SanitizeNameInPlace(FString &Name)
{ {
TCHAR c = Name[Src]; TCHAR c = Name[Src];
if (c < 0x20 || c == 0x7F) continue; 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; Name[Dst++] = c;
} }
if (Dst == 0) Name[Dst++] = '_'; if (Dst == 0) Name[Dst++] = '_';
@@ -82,6 +95,10 @@ FString WingUtils::SanitizeName(FName Name)
return Result; return Result;
} }
// ============================================================
// Name Lookup
// ============================================================
FString WingUtils::FormatName(const UWorld *World) FString WingUtils::FormatName(const UWorld *World)
{ {
return World->GetPathName(); return World->GetPathName();

View File

@@ -13,10 +13,10 @@ struct WingFunctionArgs
// CustomEvent, or Tunnel). // CustomEvent, or Tunnel).
static bool HasArgs(UEdGraphNode* Node); 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); 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. // Returns true on success.
static bool SetArgs(UEdGraphNode* Node, const FString& Args); static bool SetArgs(UEdGraphNode* Node, const FString& Args);
@@ -28,7 +28,7 @@ private:
FName PinName; 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. // Returns false and prints an error on failure.
static bool ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs); static bool ParseArgs(const FString& Args, TArray<FParsedArg>& OutArgs);

View File

@@ -15,6 +15,15 @@ HOST = "localhost"
PORT = 9847 PORT = 9847
TIMEOUT = 120 TIMEOUT = 120
# Map ASCII characters to the Unicode geometric delimiters used by the plugin.
DELIMITER_MAP = str.maketrans({
'<': '',
'>': '',
'*': '',
'.': '',
'|': '',
})
def main(): def main():
args = sys.argv[1:] args = sys.argv[1:]
if not args: if not args:
@@ -35,6 +44,11 @@ def main():
pass pass
msg[key] = value msg[key] = value
# Translate ASCII delimiter shortcuts to Unicode geometric shapes.
for key, value in msg.items():
if isinstance(value, str):
msg[key] = value.translate(DELIMITER_MAP)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(TIMEOUT) sock.settimeout(TIMEOUT)
try: try: