diff --git a/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h b/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h index d191c6fd..c3deaac3 100644 --- a/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h +++ b/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h @@ -56,17 +56,9 @@ public: } // Check for duplicate component names - const TArray& 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& 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)) diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetArgs.h b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetArgs.h index af1684cf..ffcd8085 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetArgs.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetArgs.h @@ -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│float◦y'")) FString Args; virtual FString GetDescription() const override diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h b/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h index bb3d0a53..c0dde3ae 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/UserManual.h @@ -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_Hotkeys│graph:EventGraph│node:Self03│pin: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/MyMaterial│graph" "\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, class, softclass" - "\n array, set, map" + "\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 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 A" + "\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." @@ -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" diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp index 18adaa90..dcf05545 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp @@ -77,7 +77,7 @@ WingFetcher& WingFetcher::Walk(const FString& Path) if (bError) return *this; TArray Segments; - Path.ParseIntoArray(Segments, TEXT(",")); + Path.ParseIntoArray(Segments, TEXT("│")); if (Segments.Num() == 0) { UWingServer::Print(TEXT("ERROR: Empty path\n")); diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp index 34e092e0..6179e040 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp @@ -20,8 +20,8 @@ FString WingFunctionArgs::GetArgs(UEdGraphNode* Node) TStringBuilder<256> SB; for (const TSharedPtr& 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& OutArg if (Trimmed.IsEmpty()) return true; TArray 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 "type◦name" 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 'type◦name' 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()) { diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp index adda93f9..3a277301 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp @@ -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(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; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp index 0d202ea3..a4a10910 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -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(); diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h b/Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h index 56d13886..59fe49fb 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h @@ -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│float◦y". 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│float◦y". // 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│float◦y" into an array of FParsedArg. // Returns false and prints an error on failure. static bool ParseArgs(const FString& Args, TArray& OutArgs); diff --git a/tools/mcp-test.py b/tools/mcp-test.py index e79db40b..b26e76d8 100755 --- a/tools/mcp-test.py +++ b/tools/mcp-test.py @@ -15,6 +15,15 @@ HOST = "localhost" PORT = 9847 TIMEOUT = 120 +# Map ASCII characters to the Unicode geometric delimiters used by the plugin. +DELIMITER_MAP = str.maketrans({ + '<': '◂', + '>': '▸', + '*': '◆', + '.': '◦', + '|': '│', +}) + def main(): args = sys.argv[1:] if not args: @@ -35,6 +44,11 @@ def main(): pass 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.settimeout(TIMEOUT) try: