diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetArgs.h b/Plugins/UEWingman/Source/UEWingman/Handlers/GraphNode_SetArgs.h index ffcd8085..1504ad71 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="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 c0dde3ae..fd41cef9 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 steps separated by │" + "\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,7 +38,7 @@ 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" @@ -53,15 +53,15 @@ public: "\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 Soft, Class, SoftClass" + "\n Array, Set, Map" "\n" "\n FUNCTION ARGUMENTS AND RETURN VALUES:" "\n" - "\n Function argument lists are expressed as │-separated" - "\n lists of type◦name pairs:" + "\n Function argument lists are expressed as comma-separated" + "\n lists of type-name pairs:" "\n" - "\n double◦D│PlayerController◦P│Array◂int▸◦A" + "\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." @@ -70,13 +70,11 @@ public: "\n before you can set return values. Custom event nodes also have" "\n editable arguments." "\n" - "\n ABOUT UNICODE AND WHITESPACE:" + "\n ABOUT 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 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" diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp index dcf05545..f028b9a5 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")); @@ -164,7 +164,7 @@ WingFetcher& WingFetcher::Graph(const FString& Value) { if (bError) return *this; - // Material with blank graph name → navigate to the material graph. + // Material with blank graph name if (UMaterial* Mat = ::Cast(Obj)) { if (!Value.IsEmpty()) diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp index 6179e040..b76025c9 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFunctionArgs.cpp @@ -3,6 +3,7 @@ #include "K2Node_FunctionResult.h" #include "K2Node_Tunnel.h" #include "WingTypes.h" +#include "WingUtils.h" #include "WingServer.h" bool WingFunctionArgs::HasArgs(UEdGraphNode* Node) @@ -20,8 +21,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(" ") << WingUtils::FormatName(*Pin); } return FString(SB); } @@ -42,18 +43,18 @@ 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 white bullet. + // Split "type name" FString TypeStr, NameStr; - if (!Token.Split(TEXT("◦"), &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; } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp index 9c78be5d..5c280fe1 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp @@ -104,13 +104,13 @@ void UWingTypes::ChooseShortName(const FAssetData &Data) { // Blueprint: the generated class is AssetName_C FString ObjectPath = FString::Printf(TEXT("%s.%s_C"), *PackageName, *AssetName); - ChooseShortName(AssetName, ObjectPath); + ChooseShortName(WingUtils::SanitizeName(AssetName), ObjectPath); } else if (ClassPath == UUserDefinedStruct::StaticClass()->GetClassPathName() || ClassPath == UUserDefinedEnum::StaticClass()->GetClassPathName()) { FString ObjectPath = FString::Printf(TEXT("%s.%s"), *PackageName, *AssetName); - ChooseShortName(AssetName, ObjectPath); + ChooseShortName(WingUtils::SanitizeName(AssetName), ObjectPath); } } @@ -159,11 +159,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; } @@ -181,9 +181,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( @@ -192,7 +192,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; @@ -447,29 +447,29 @@ bool UWingTypes::ParsePlainIdentifier(FEdGraphPinType& OutType) bool UWingTypes::ParseWrapped(FName Wrapper, FEdGraphPinType& OutType) { Cursor++; - if (!ParseChar(L'◂')) return false; + if (!ParseChar('<')) 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(L'▸')) return false; + if (!ParseChar('>')) return false; OutType.PinCategory = Wrapper; return true; } bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType) { - if (TokenIs(TEXT("Soft"), L'◂')) + if (TokenIs(TEXT("Soft"), '<')) { return ParseWrapped(UEdGraphSchema_K2::PC_SoftObject, OutType); } - else if (TokenIs(TEXT("Class"), L'◂')) + else if (TokenIs(TEXT("Class"), '<')) { return ParseWrapped(UEdGraphSchema_K2::PC_Class, OutType); } - else if (TokenIs(TEXT("SoftClass"), L'◂')) + else if (TokenIs(TEXT("SoftClass"), '<')) { return ParseWrapped(UEdGraphSchema_K2::PC_SoftClass, OutType); } @@ -479,40 +479,40 @@ bool UWingTypes::ParseMaybeWrapped(FEdGraphPinType& OutType) bool UWingTypes::ParseArrayOrSet(FEdGraphPinType& OutType) { Cursor++; - if (!ParseChar(L'◂')) return false; + if (!ParseChar('<')) return false; if (!ParseMaybeWrapped(OutType)) return false; - if (!ParseChar(L'▸')) return false; + if (!ParseChar('>')) return false; return true; } bool UWingTypes::ParseMap(FEdGraphPinType& OutType) { Cursor++; - if (!ParseChar(L'◂')) return false; + if (!ParseChar('<')) return false; if (!ParsePlainIdentifier(OutType)) return false; - if (!ParseChar(L'◆')) return false; + if (!ParseChar(',')) 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(L'▸')) return false; + if (!ParseChar('>')) return false; return true; } bool UWingTypes::ParseType(FEdGraphPinType& OutType) { - if (TokenIs(TEXT("Array"), L'◂')) + if (TokenIs(TEXT("Array"), '<')) { OutType.ContainerType = EPinContainerType::Array; if (!ParseArrayOrSet(OutType)) return false; } - else if (TokenIs(TEXT("Set"), L'◂')) + else if (TokenIs(TEXT("Set"), '<')) { OutType.ContainerType = EPinContainerType::Set; if (!ParseArrayOrSet(OutType)) return false; } - else if (TokenIs(TEXT("Map"), L'◂')) + else if (TokenIs(TEXT("Map"), '<')) { 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 c3915cde..586de583 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -11,6 +11,7 @@ #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" #include "EdGraph/EdGraphPin.h" +#include "K2Node_EditablePinBase.h" #include "EdGraph/EdGraphSchema.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" @@ -53,14 +54,11 @@ extern int32 TrySavePackageSEH( // ============================================================ // 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. +// Our parsers reserve certain punctuation marks for parsing +// types, paths, and the like. For example: Array. +// We therefore cannot allow those specific punctuation marks +// in names. We replace them with similar-looking unicode +// characters. // ============================================================ void WingUtils::SanitizeNameInPlace(FString &Name) @@ -70,14 +68,13 @@ void WingUtils::SanitizeNameInPlace(FString &Name) { TCHAR c = Name[Src]; if (c < 0x20 || c == 0x7F) continue; - if (c == L'◂') c='<'; - if (c == L'▸') c='>'; - if (c == L'◆') c='*'; - if (c == L'◦') c='.'; - if (c == L'│') c = '|'; + if (c == ' ') c=L'·'; + if (c == '<') c=L'◁'; + if (c == '>') c=L'▷'; + if (c == ',') c=L'·'; Name[Dst++] = c; } - if (Dst == 0) Name[Dst++] = '_'; + if (Dst == 0) Name[Dst++] = L'·'; Name.LeftInline(Dst); } @@ -216,6 +213,11 @@ FString WingUtils::FormatName(const FProperty *Prop) return SanitizeName(Prop->GetName()); } +FString WingUtils::FormatName(const FUserPinInfo &Pin) +{ + return SanitizeName(Pin.PinName); +} + // ============================================================ // Formatting other things // ============================================================ diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h b/Plugins/UEWingman/Source/UEWingman/Public/WingFunctionArgs.h index 59fe49fb..b55ec481 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); diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h index 2f9f3dcf..e30dd117 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h @@ -30,6 +30,7 @@ class UEnum; class USCS_Node; struct FMemberReference; struct FBPVariableDescription; +struct FUserPinInfo; // Stateless utility functions used by MCP handlers and the MCP server. // This is effectively a namespace — all methods are static. class WingUtils @@ -71,6 +72,7 @@ public: static FString FormatName(const UScriptStruct *Struct); static FString FormatName(const UEnum *Enum); static FString FormatName(const FProperty *Prop); + static FString FormatName(const FUserPinInfo &Pin); //////////////////////////////////////////////////////// // diff --git a/find_nonascii.py b/find_nonascii.py new file mode 100644 index 00000000..27691750 --- /dev/null +++ b/find_nonascii.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python3 +"""Search UEWingman source files for non-ASCII characters.""" + +import os +import glob + +source_dir = "Plugins/UEWingman/Source" +patterns = ["**/*.cpp", "**/*.h"] +found_any = False + +for pattern in patterns: + for filepath in sorted(glob.glob(os.path.join(source_dir, pattern), recursive=True)): + with open(filepath, "r", encoding="utf-8") as f: + for lineno, line in enumerate(f, 1): + for col, ch in enumerate(line, 1): + if ord(ch) > 127: + if not found_any: + found_any = True + print(f"{filepath}:{lineno}:{col} U+{ord(ch):04X} {ch!r} ...{line[max(0,col-10):col+10].rstrip()}...") + +if not found_any: + print("No non-ASCII characters found.")