diff --git a/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_RemoveComponent.h b/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_RemoveComponent.h deleted file mode 100644 index c3466362..00000000 --- a/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_RemoveComponent.h +++ /dev/null @@ -1,91 +0,0 @@ -#pragma once - -#include "CoreMinimal.h" -#include "WingServer.h" -#include "WingHandler.h" -#include "WingFetcher.h" -#include "WingUtils.h" -#include "Engine/Blueprint.h" -#include "Engine/SimpleConstructionScript.h" -#include "Engine/SCS_Node.h" -#include "Blueprint_RemoveComponent.generated.h" - - -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- -// --------------------------------------------------------------------------- - -UCLASS() -class UWing_Blueprint_RemoveComponent : public UObject, public IWingHandler -{ - GENERATED_BODY() - -public: - UPROPERTY(meta=(Description="Blueprint name or package path")) - FString Blueprint; - - UPROPERTY(meta=(Description="Component to remove")) - FString Component; - - virtual FString GetDescription() const override - { - return TEXT("Remove a component from a Blueprint's SimpleConstructionScript."); - } - - virtual void Handle() override - { - WingFetcher F; - UBlueprint* BP = F.Walk(Blueprint).Cast(); - if (!BP) return; - - USimpleConstructionScript* SCS = BP->SimpleConstructionScript; - if (!SCS) - { - UWingServer::Print(TEXT("ERROR: Not an Actor Blueprint (no SimpleConstructionScript).\n")); - return; - } - - // Find the node to remove using Identifies for consistent name matching - USCS_Node* NodeToRemove = nullptr; - const TArray& AllNodes = SCS->GetAllNodes(); - for (USCS_Node* Node : AllNodes) - { - if (Node && Node->ComponentTemplate && - WingUtils::Identifies(Component, Node->ComponentTemplate)) - { - NodeToRemove = Node; - break; - } - } - - if (!NodeToRemove) - { - UWingServer::Printf(TEXT("ERROR: Component '%s' not found.\nAvailable components:\n"), - *Component); - for (USCS_Node* Node : AllNodes) - { - if (Node && Node->ComponentTemplate) - UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(Node->ComponentTemplate)); - } - return; - } - - // Prevent removing the root scene component if it has children - const TArray& RootNodes = SCS->GetRootNodes(); - if (RootNodes.Contains(NodeToRemove) && NodeToRemove->GetChildNodes().Num() > 0) - { - UWingServer::Printf(TEXT("ERROR: Cannot remove '%s' — it is a root component with %d child(ren). " - "Remove or re-parent the children first.\n"), - *WingUtils::FormatName(NodeToRemove->ComponentTemplate), - NodeToRemove->GetChildNodes().Num()); - return; - } - - FString RemovedName = WingUtils::FormatName(NodeToRemove->ComponentTemplate); - - // Remove the node (promotes children to parent if it has any — but we've guarded root above) - SCS->RemoveNodeAndPromoteChildren(NodeToRemove); - - UWingServer::Printf(TEXT("Removed component %s.\n"), *RemovedName); - } -}; diff --git a/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Add.h similarity index 76% rename from Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h rename to Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Add.h index 4302ece7..499c5c4e 100644 --- a/Plugins/UEWingman/Source/UEWingman/HalfBaked/Blueprint_AddComponent.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Add.h @@ -6,11 +6,12 @@ #include "WingUtils.h" #include "WingTypes.h" #include "WingServer.h" +#include "WingActorComponent.h" #include "Engine/Blueprint.h" #include "Engine/SimpleConstructionScript.h" #include "Engine/SCS_Node.h" #include "Components/ActorComponent.h" -#include "Blueprint_AddComponent.generated.h" +#include "BlueprintComponent_Add.generated.h" // --------------------------------------------------------------------------- @@ -18,7 +19,7 @@ // --------------------------------------------------------------------------- UCLASS() -class UWing_Blueprint_AddComponent : public UObject, public IWingHandler +class UWing_BlueprintComponent_Add : public UObject, public IWingHandler { GENERATED_BODY() @@ -55,9 +56,9 @@ public: return; } - // Check for duplicate component names - const TArray& Existing = SCS->GetAllNodes(); - if (!WingUtils::FindExactlyNoneNamed(Component, Existing)) + // Check for duplicate component names (including native/inherited) + TArray AllComponents = FWingActorComponent::GetAll(BP); + if (!WingUtils::FindExactlyNoneNamed(Component, AllComponents)) return; // Resolve the component class by name @@ -69,12 +70,12 @@ public: return; } - // If parent component specified, find its SCS node - USCS_Node* ParentSCSNode = nullptr; + // If parent component specified, find it among all components + FWingActorComponent* ParentComp = nullptr; if (!Parent.IsEmpty()) { - ParentSCSNode = WingUtils::FindExactlyOneNamed(Parent, Existing); - if (!ParentSCSNode) return; + ParentComp = WingUtils::FindExactlyOneNamed(Parent, AllComponents); + if (!ParentComp) return; } // Create the SCS node @@ -87,22 +88,15 @@ public: } // Add to the hierarchy - if (ParentSCSNode) - { - ParentSCSNode->AddChildNode(NewNode); - } - else - { - SCS->AddNode(NewNode); - } + if (!FWingActorComponent::SetParent(NewNode, ParentComp)) + return; + SCS->AddNode(NewNode); UWingServer::Printf(TEXT("Added component %s (%s)"), *WingUtils::FormatName(NewNode->ComponentTemplate), *WingUtils::FormatName(ComponentClassObj)); - if (ParentSCSNode) - { - UWingServer::Printf(TEXT(" under %s"), *WingUtils::FormatName(ParentSCSNode->ComponentTemplate)); - } + if (ParentComp) + UWingServer::Printf(TEXT(" under %s"), *ParentComp->GetName()); UWingServer::Print(TEXT("\n")); } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Dump.h b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Dump.h new file mode 100644 index 00000000..f1506447 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Dump.h @@ -0,0 +1,82 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingFetcher.h" +#include "WingProperty.h" +#include "WingTypes.h" +#include "WingUtils.h" +#include "WingActorComponent.h" +#include "Engine/SCS_Node.h" +#include "Engine/SimpleConstructionScript.h" +#include "BlueprintComponent_Dump.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UWing_BlueprintComponent_Dump : public UObject, public IWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Path to the component (e.g. '/Game/MyBP,component:MyComp')")) + FString Component; + + virtual FString GetDescription() const override + { + return TEXT("Dump a component's class, parent, and editable properties."); + } + + virtual void Handle() override + { + WingFetcher F; + USCS_Node* Node = F.Walk(Component).Cast(); + if (!Node) return; + + UActorComponent* Template = Node->ComponentTemplate; + if (!Template) + { + UWingServer::Print(TEXT("ERROR: SCS node has no component template.\n")); + return; + } + + // Header + UWingServer::Printf(TEXT("Component: %s\n"), *WingUtils::FormatName(Node)); + UWingServer::Printf(TEXT("Class: %s\n"), *WingUtils::FormatName(Node->ComponentClass)); + + // Parent + if (Node->ParentComponentOrVariableName != NAME_None) + UWingServer::Printf(TEXT("Parent: %s\n"), *WingUtils::SanitizeName(Node->ParentComponentOrVariableName)); + + // Properties (already sorted and grouped by category) + TArray Props = FWingProperty::GetAll(Template, CPF_Edit); + + if (Props.IsEmpty()) + { + UWingServer::Print(TEXT("\n (no editable properties found)\n")); + return; + } + + FString CurrentCategory; + for (FWingProperty& P : Props) + { + FString Category = P.GetCategory(); + if (Category != CurrentCategory) + { + CurrentCategory = Category; + UWingServer::Printf(TEXT("\n%s:\n"), *CurrentCategory); + } + + bool bEditable = !P->HasAnyPropertyFlags(CPF_EditConst); + UWingServer::Printf(TEXT(" %s %s %s = %s\n"), + bEditable ? TEXT("editable") : TEXT("readonly"), + *UWingTypes::TypeToText(P.Prop), + *WingUtils::FormatName(P.Prop), + *P.GetTruncatedText(100)); + } + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Remove.h b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Remove.h new file mode 100644 index 00000000..e32a55ad --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Remove.h @@ -0,0 +1,55 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingFetcher.h" +#include "WingUtils.h" +#include "Engine/SCS_Node.h" +#include "Engine/SimpleConstructionScript.h" +#include "BlueprintComponent_Remove.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UWing_BlueprintComponent_Remove : public UObject, public IWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Path to the component (e.g. '/Game/MyBP,component:MyComp')")) + FString Component; + + virtual FString GetDescription() const override + { + return TEXT("Remove a component from a Blueprint's SimpleConstructionScript."); + } + + virtual void Handle() override + { + WingFetcher F; + USCS_Node* Node = F.Walk(Component).Cast(); + if (!Node) return; + + USimpleConstructionScript* SCS = Node->GetSCS(); + if (!SCS) return; + + // Prevent removing a root component that has children + const TArray& RootNodes = SCS->GetRootNodes(); + if (RootNodes.Contains(Node) && Node->GetChildNodes().Num() > 0) + { + UWingServer::Printf(TEXT("ERROR: Cannot remove '%s' — it is a root component with %d children. " + "Remove or re-parent the children first.\n"), + *WingUtils::FormatName(Node), + Node->GetChildNodes().Num()); + return; + } + + FString RemovedName = WingUtils::FormatName(Node); + SCS->RemoveNodeAndPromoteChildren(Node); + UWingServer::Printf(TEXT("Removed component %s.\n"), *RemovedName); + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Reparent.h b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Reparent.h new file mode 100644 index 00000000..5af3a789 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/BlueprintComponent_Reparent.h @@ -0,0 +1,64 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WingHandler.h" +#include "WingFetcher.h" +#include "WingUtils.h" +#include "WingServer.h" +#include "WingActorComponent.h" +#include "Engine/Blueprint.h" +#include "Engine/SimpleConstructionScript.h" +#include "Engine/SCS_Node.h" +#include "BlueprintComponent_Reparent.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UWing_BlueprintComponent_Reparent : public UObject, public IWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Path to the component (e.g. '/Game/MyBP,component:MyComp')")) + FString Component; + + UPROPERTY(meta=(Optional, Description="New parent component name. If empty, detaches to root.")) + FString Parent; + + virtual FString GetDescription() const override + { + return TEXT("Change the parent of a component in a Blueprint's SimpleConstructionScript."); + } + + virtual void Handle() override + { + WingFetcher F; + USCS_Node* Node = F.Walk(Component).Cast(); + if (!Node) return; + + USimpleConstructionScript* SCS = Node->GetSCS(); + if (!SCS) return; + + // Find the new parent among all components (if specified) + FWingActorComponent* NewParent = nullptr; + TArray AllComponents; + if (!Parent.IsEmpty()) + { + UBlueprint* BP = SCS->GetBlueprint(); + AllComponents = FWingActorComponent::GetAll(BP); + NewParent = WingUtils::FindExactlyOneNamed(Parent, AllComponents); + if (!NewParent) return; + } + + if (!FWingActorComponent::SetParent(Node, NewParent)) + return; + + if (NewParent) + UWingServer::Printf(TEXT("Reparented %s under %s.\n"), *WingUtils::FormatName(Node), *NewParent->GetName()); + else + UWingServer::Printf(TEXT("Detached %s to root.\n"), *WingUtils::FormatName(Node)); + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Dump.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Dump.h index 00bd16b5..99d40338 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Dump.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Dump.h @@ -89,9 +89,7 @@ public: FString ParentName = Comp.GetParentName(); if (!ParentName.IsEmpty()) UWingServer::Printf(TEXT(" [parent: %s]"), *ParentName); - if (Comp.IsNative()) - UWingServer::Print(TEXT(" [native]")); - else if (!Comp.IsOwnedBy(BP)) + if (!Comp.IsOwnedBy(BP)) UWingServer::Print(TEXT(" [inherited]")); UWingServer::Print(TEXT("\n")); } diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump.h index 5b9684e2..b7507f58 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Property_Dump.h @@ -26,9 +26,6 @@ public: UPROPERTY(meta=(Optional, Description="Substring filter for property names")) FString Query; - UPROPERTY(meta=(Optional, Description="Truncate values to 80 characters (default true)")) - bool Truncate = true; - UPROPERTY(meta=(Optional, Description="Only show properties declared on the object's own class, not inherited ones")) bool Local = false; @@ -51,48 +48,28 @@ public: Props.RemoveAll([ObjClass](const FWingProperty& P) { return P->GetOwnerStruct() != ObjClass; }); } - // Group properties by category. - TMap> ByCategory; + if (Props.IsEmpty()) + { + UWingServer::Print(TEXT(" (no blueprint-visible properties found)\n")); + return; + } + + FString CurrentCategory; for (FWingProperty& P : Props) { - FString Category = P->HasMetaData(TEXT("Category")) ? P->GetMetaData(TEXT("Category")) : FString(); - ByCategory.FindOrAdd(Category).Add(P); - } - - // Sort category names, putting empty category last. - TArray Categories; - ByCategory.GetKeys(Categories); - Categories.Sort([](const FString& A, const FString& B) { - if (A.IsEmpty()) return false; - if (B.IsEmpty()) return true; - return A < B; - }); - - for (const FString& Category : Categories) - { - if (Category.IsEmpty()) - UWingServer::Print(TEXT("\nUncategorized:\n")); - else - UWingServer::Printf(TEXT("\n%s:\n"), *Category); - - for (FWingProperty& P : ByCategory[Category]) + FString Category = P.GetCategory(); + if (Category != CurrentCategory) { - FString PropName = WingUtils::FormatName(P.Prop); - FString ValueStr = P.GetText(); - - if (Truncate && (ValueStr.Len() > 80)) - ValueStr = ValueStr.Left(80) + TEXT("..."); - - bool bEditable = !P->HasAnyPropertyFlags(CPF_EditConst); - UWingServer::Printf(TEXT(" %s %s %s = %s\n"), - bEditable ? TEXT("editable") : TEXT("readonly"), - *UWingTypes::TypeToText(P.Prop), - *PropName, - *ValueStr); + CurrentCategory = Category; + UWingServer::Printf(TEXT("\n%s:\n"), *CurrentCategory); } - } - if (Props.IsEmpty()) - UWingServer::Print(TEXT(" (no blueprint-visible properties found)\n")); + bool bEditable = !P->HasAnyPropertyFlags(CPF_EditConst); + UWingServer::Printf(TEXT(" %s %s %s = %s\n"), + bEditable ? TEXT("editable") : TEXT("readonly"), + *UWingTypes::TypeToText(P.Prop), + *WingUtils::FormatName(P.Prop), + *P.GetTruncatedText(100)); + } } }; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingActorComponent.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingActorComponent.cpp index d0c3650c..5d4ca596 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingActorComponent.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingActorComponent.cpp @@ -1,4 +1,5 @@ #include "WingActorComponent.h" +#include "WingServer.h" #include "WingUtils.h" #include "Engine/Blueprint.h" #include "Engine/SCS_Node.h" @@ -47,6 +48,56 @@ bool FWingActorComponent::IsOwnedBy(const UBlueprint* BP) const return BP && BP->GeneratedClass == Owner; } +bool FWingActorComponent::SetParent(USCS_Node* ChildNode, const FWingActorComponent* Parent) +{ + // Validate before modifying anything + if (Parent) + { + if (Parent->SCSNode) + { + // Check for cycles: walk up from parent to make sure we don't reach the child + USCS_Node* Ancestor = Parent->SCSNode; + while (Ancestor) + { + if (Ancestor == ChildNode) + { + UWingServer::Printf(TEXT("ERROR: Cannot reparent — would create a cycle.\n")); + return false; + } + Ancestor = Ancestor->GetSCS()->FindParentNode(Ancestor); + } + } + else + { + USceneComponent* NativeScene = Cast(Parent->NativeComponent); + if (!NativeScene) + { + UWingServer::Printf(TEXT("ERROR: Native parent '%s' is not a SceneComponent — cannot attach to it.\n"), + *Parent->GetName()); + return false; + } + } + } + + // Clear any existing parent attachment + ChildNode->Modify(); + ChildNode->bIsParentComponentNative = false; + ChildNode->ParentComponentOrVariableName = NAME_None; + ChildNode->ParentComponentOwnerClassName = NAME_None; + + if (!Parent) + return true; + + if (Parent->SCSNode) + { + ChildNode->SetParent(Parent->SCSNode); + return true; + } + + ChildNode->SetParent(Cast(Parent->NativeComponent)); + return true; +} + TArray FWingActorComponent::GetAll(UBlueprint* BP) { TArray Result; diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp index f028b9a5..50cbc0df 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFetcher.cpp @@ -1,6 +1,7 @@ #include "WingFetcher.h" #include "WingServer.h" #include "WingUtils.h" +#include "WingActorComponent.h" #include "Engine/Blueprint.h" #include "EdGraph/EdGraph.h" #include "EdGraph/EdGraphNode.h" @@ -290,25 +291,17 @@ WingFetcher& WingFetcher::Component(const FString& Value) if (!BP) return TypeMismatch(TEXT("component"), TEXT("Blueprint")); - USimpleConstructionScript* SCS = BP->SimpleConstructionScript; - if (!SCS) + TArray AllComponents = FWingActorComponent::GetAll(BP); + FWingActorComponent* Comp = WingUtils::FindExactlyOneNamed(Value, AllComponents); + if (!Comp) return SetError(); + if (!Comp->IsOwnedBy(BP)) { - UWingServer::Printf(TEXT("ERROR: Blueprint %s has no SimpleConstructionScript (not an Actor Blueprint)\n"), *BP->GetName()); + UWingServer::Printf(TEXT("ERROR: Component '%s' belongs to %s, to edit it, you must go through that blueprint.\n"), + *Comp->GetName(), *WingUtils::FormatName(Comp->Owner)); return SetError(); } - - FName SearchName(*Value); - for (USCS_Node* SCSNode : SCS->GetAllNodes()) - { - if (SCSNode && SCSNode->GetVariableName() == SearchName) - { - SetObj(SCSNode->ComponentTemplate); - return *this; - } - } - - UWingServer::Printf(TEXT("ERROR: Component '%s' not found in %s\n"), *Value, *BP->GetName()); - return SetError(); + SetObj(Comp->SCSNode); + return *this; } WingFetcher& WingFetcher::LevelBlueprint(const FString& Value) diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp index c4001481..89d50170 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp @@ -19,6 +19,13 @@ FWingProperty::FWingProperty(FProperty* InProp, void* InContainer) FWingProperty::FWingProperty(FProperty* InProp, UObject* InContainer) : Prop(InProp), Container(static_cast(InContainer)) {} +FString FWingProperty::GetCategory() +{ + FString Result = Prop->GetMetaData(TEXT("Category")); + if (Result.IsEmpty()) Result = "Unclassified"; + return Result; +} + FString FWingProperty::GetText() const { void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); @@ -29,6 +36,16 @@ FString FWingProperty::GetText() const return Result; } +FString FWingProperty::GetTruncatedText(int32 MaxLen) const +{ + FString Result = GetText(); + for (int i = 0; i < Result.Len(); i++) + if (Result[i] == '\n') Result[i] = ' '; + if (Result.Len() > MaxLen) + Result = Result.Left(MaxLen) + TEXT("..."); + return Result; +} + bool FWingProperty::SetText(const FString &Value) { void* ValuePtr = Prop->ContainerPtrToValuePtr(Container); @@ -75,19 +92,26 @@ bool FWingProperty::SetText(const FString &Value) void FWingProperty::Collect(UStruct* StructType, void* Container, TArray &Props, EPropertyFlags Flags) { + TMap> Grouped; + for (TFieldIterator It(StructType); It; ++It) { if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; - Props.Emplace(*It, Container); + FString SortCat = *It->GetMetaData(TEXT("Category")); + Grouped.FindOrAdd(SortCat).Add(FWingProperty(*It, Container)); } -} + TArray Categories; -void FWingProperty::Collect(UObject* Container, TArray &Props, EPropertyFlags Flags) -{ - for (TFieldIterator It(Container->GetClass()); It; ++It) + Grouped.GetKeys(Categories); + Categories.Sort([](const FString& A, const FString& B) { + if (A.IsEmpty()) return false; + if (B.IsEmpty()) return true; + return A < B; + }); + + for (const FString& Category : Categories) { - if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; - Props.Emplace(*It, Container); + Props.Append(Grouped[Category]); } } @@ -99,7 +123,6 @@ void FWingProperty::Remove(TArray& Props, const FString& Name) TArray FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags) { if (!Obj) return {}; - TArray Result; // Blueprints don't have editable properties. So // instead, we fetch properties from the generated CDO, @@ -115,7 +138,8 @@ TArray FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags) Obj = BP->GeneratedClass->GetDefaultObject(); } - Collect(Obj, Result, Flags); + TArray Result; + Collect(Obj->GetClass(), Obj, Result, Flags); // If it's a Material Graph node, also collect properties from // the associated material expression. @@ -124,9 +148,10 @@ TArray FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags) { if (UMaterialExpression* Expr = MatNode->MaterialExpression) { - Collect(Expr, Result, Flags); + Collect(Expr->GetClass(), Expr, Result, Flags); } } + return Result; } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp index 5c280fe1..00b6a69f 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingTypes.cpp @@ -24,8 +24,7 @@ FString UWingTypes::GetProposedName(const UObject *Obj) Name.LeftChopInline(2); } } - WingUtils::SanitizeNameInPlace(Name); - return Name; + return WingUtils::SanitizeName(Name); } void UWingTypes::ReserveShortName(FName Name) diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp index 52743f4f..980bae88 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingUtils.cpp @@ -46,16 +46,11 @@ // ============================================================ // Name sanitization -// -// 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) +FString WingUtils::SanitizeName(const FString &InName) { + FString Name = InName; int32 Dst = 0; for (int32 Src = 0; Src < Name.Len(); Src++) { @@ -64,25 +59,32 @@ void WingUtils::SanitizeNameInPlace(FString &Name) if (c == ' ') c=L'·'; if (c == '<') c=L'◁'; if (c == '>') c=L'▷'; - if (c == ',') c=L'·'; + if (c == ',') c=L'▾'; Name[Dst++] = c; } if (Dst == 0) Name[Dst++] = L'·'; Name.LeftInline(Dst); + return Name; } -FString WingUtils::SanitizeName(const FString &Name) +FString WingUtils::UnsanitizeName(const FString &InName) { - FString Result = Name; - SanitizeNameInPlace(Result); - return Result; + FString Name = InName; + for (int32 i = 0; i < Name.Len(); i++) + { + TCHAR c = Name[i]; + if (c == L'·') c=' '; + if (c == L'◁') c='<'; + if (c == L'▷') c='>'; + if (c == L'▾') c=','; + Name[i] = c; + } + return Name; } FString WingUtils::SanitizeName(FName Name) { - FString Result = Name.ToString(); - SanitizeNameInPlace(Result); - return Result; + return SanitizeName(Name.ToString()); } // ============================================================ diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingActorComponent.h b/Plugins/UEWingman/Source/UEWingman/Public/WingActorComponent.h index 40038c20..b1253f75 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingActorComponent.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingActorComponent.h @@ -1,13 +1,16 @@ #pragma once #include "CoreMinimal.h" +#include "WingActorComponent.generated.h" class UBlueprint; class USCS_Node; class UActorComponent; +USTRUCT() struct FWingActorComponent { + GENERATED_BODY() USCS_Node* SCSNode = nullptr; UActorComponent* NativeComponent = nullptr; UClass* Owner = nullptr; @@ -24,6 +27,11 @@ struct FWingActorComponent bool IsNative() const { return NativeComponent != nullptr; } bool IsOwnedBy(const UBlueprint* BP) const; + // Attach ChildNode under this component. If this is an SCS node, uses + // AddChildNode. If native, uses SetParent with the native scene component. + // Returns false and prints an error if attachment is not possible. + static bool SetParent(USCS_Node* ChildNode, const FWingActorComponent* Parent); + // Collect all components: native from CDO first, then SCS nodes // walking the hierarchy from oldest ancestor to current blueprint. static TArray GetAll(UBlueprint* BP); diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h b/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h index ea48fe78..e3c05f03 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h @@ -17,6 +17,13 @@ struct FWingProperty FString GetText() const; bool SetText(const FString& Value); + // Get the Category metadata. + FString GetCategory(); + + // Get the text, replace newlines with whitespace, and + // truncate to the specified maximum length. + FString GetTruncatedText(int32 MaxLen) const; + explicit operator bool() const { return Prop != nullptr; } FProperty* operator->() const { return Prop; } @@ -27,6 +34,5 @@ struct FWingProperty static FWingProperty FindOneExactMatch(const TArray& Props, const FString& Name); private: - static void Collect(UStruct* StructType, void* Container, TArray &Props, EPropertyFlags Flags); - static void Collect(UObject* Container, TArray &Props, EPropertyFlags Flags); + static void Collect(UStruct* Struct, void* Container, TArray &Props, EPropertyFlags Flags); }; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h index 8789274f..d073d836 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingUtils.h @@ -155,9 +155,29 @@ public: //////////////////////////////////////////////////////// - static void SanitizeNameInPlace(FString& Name); + //////////////////////////////////////////////////////// + // 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. + //////////////////////////////////////////////////////// static FString SanitizeName(const FString& Name); static FString SanitizeName(FName Name); + + //////////////////////////////////////////////////////// + // Our name sanitization routine, above, will turn + // names with spaces into names like "Post·Initiate·Action" + // containing middle dots instead. There is a risk that the + // LLM will see these dotted names and think it is supposed to + // do that. So, we have an 'Unsanitize' routine to convert + // the middle dots back into spaces. Of course, next time the + // name is output, it will be sanitized again, so the LLM will + // always see the version with dots. This creates consistency + // for both the LLM and the human user (who is expecting whitespace). + //////////////////////////////////////////////////////// + static FString UnsanitizeName(const FString& Name); + static FString FormatNodeTitle(const UEdGraphNode *Node); // ----- Enum helpers -----