Massive overhaul of the WingActorComponent system, can now edit component properties better, lots of bug fixes

This commit is contained in:
2026-03-27 05:16:49 -04:00
parent 3bbddde491
commit a5ab6b02f9
19 changed files with 380 additions and 222 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -48,16 +48,8 @@ public:
UBlueprint* BP = F.Asset(Blueprint).Cast<UBlueprint>();
if (!BP) return;
USimpleConstructionScript* SCS = BP->SimpleConstructionScript;
if (!SCS)
{
UWingServer::Printf(TEXT("ERROR: Blueprint '%s' does not have a SimpleConstructionScript (not an Actor Blueprint)\n"),
*WingUtils::FormatName(BP));
return;
}
// Check that the proposed name is valid
TArray<FWingActorComponent> AllComponents = FWingActorComponent::GetAll(BP);
TArray<UWingComponentReference*> AllComponents = UWingComponentReference::GetAll(BP);
if (!WingUtils::FindExactlyNoneNamed(Component, AllComponents, TEXT("Component"))) return;
FString InternalName = WingUtils::CheckProposedName(Component);
if (InternalName.IsEmpty()) return;
@@ -70,25 +62,15 @@ public:
Req.IsChildOf = UActorComponent::StaticClass();
UClass* ComponentClass = UWingTypes::TextToOneObjectType(Class, Req);
if (!ComponentClass) return;
if (!UWingComponentReference::CheckValidComponentClass(ComponentClass)) return;
// Find the specified parent component
FWingActorComponent* ParentComp = WingUtils::FindExactlyOneNamed(Parent, AllComponents, TEXT("Component"));
UWingComponentReference* ParentComp = WingUtils::FindExactlyOneNamed(Parent, AllComponents, TEXT("Component"));
if (!ParentComp) return;
// Create the SCS node
USCS_Node *NewNode = FWingActorComponent::AddComponent(BP, ComponentClass, ParentComp, InternalName);
if (!NewNode)
{
UWingServer::Printf(TEXT("ERROR: Failed to create SCS node for component '%s' with class '%s'\n"),
*Component, *WingUtils::FormatName(ComponentClass));
return;
}
if (!UWingComponentReference::AddComponent(BP, ComponentClass, ParentComp, FName(*InternalName))) return;
UWingServer::Printf(TEXT("Added component %s (%s)"),
*WingUtils::FormatName(NewNode),
*WingUtils::FormatName(ComponentClass));
if (ParentComp)
UWingServer::Printf(TEXT(" under %s"), *ParentComp->GetName());
UWingServer::Print(TEXT("\n"));
UWingServer::Printf(TEXT("Component Added.\n"));
}
};

View File

@@ -53,7 +53,7 @@ public:
UWingServer::Printf(TEXT("Parent: %s\n"), *WingUtils::SanitizeName(Node->ParentComponentOrVariableName));
// Properties (already sorted and grouped by category)
TArray<FWingProperty> Props = FWingProperty::GetAll(Template, CPF_Edit);
TArray<FWingProperty> Props = FWingProperty::GetDetailsMutable(Template, CPF_Edit);
if (Props.IsEmpty())
{

View File

@@ -36,22 +36,17 @@ public:
virtual void Handle() override
{
WingFetcher F;
USCS_Node* Node = F.Walk(Component).Cast<USCS_Node>();
if (!Node) return;
USimpleConstructionScript* SCS = Node->GetSCS();
UWingComponentReference* CompRef = F.Walk(Component).Cast<UWingComponentReference>();
if (!CompRef) return;
// Find the new parent among all components (if specified)
UBlueprint *BP = SCS->GetBlueprint();
TArray<FWingActorComponent> AllComponents = FWingActorComponent::GetAll(BP);
FWingActorComponent* NewParent = WingUtils::FindExactlyOneNamed(Parent, AllComponents, TEXT("Component"));
UBlueprint *BP = CompRef->BP;
TArray<UWingComponentReference*> AllComponents = UWingComponentReference::GetAll(BP);
UWingComponentReference* NewParent = WingUtils::FindExactlyOneNamed(Parent, AllComponents, TEXT("Component"));
if (!NewParent) return;
if (!FWingActorComponent::ReparentComponent(BP,
Node, NewParent))
return;
if (!CompRef->ReparentComponent(NewParent)) return;
UWingServer::Printf(TEXT("Reparented %s under %s.\n"),
*WingUtils::FormatName(Node), *WingUtils::FormatName(*NewParent));
UWingServer::Printf(TEXT("Reparented component."));
}
};

View File

@@ -83,17 +83,18 @@ public:
}
// Components
TArray<FWingActorComponent> Components = FWingActorComponent::GetAll(BP);
if (!Components.IsEmpty())
TArray<UWingComponentReference*> Components3 = UWingComponentReference::GetAll(BP);
if (!Components3.IsEmpty())
{
UWingServer::Print(TEXT("\nComponents:\n"));
for (const FWingActorComponent& Comp : Components)
for (const UWingComponentReference* Ref : Components3)
{
UWingServer::Printf(TEXT(" %s %s"), *Comp.GetClassName(), *WingUtils::FormatName(Comp));
FString ParentName = Comp.GetParentName();
if (!ParentName.IsEmpty())
UWingServer::Printf(TEXT(" [parent: %s]"), *ParentName);
if (!Comp.IsOwnedBy(BP))
UWingServer::Printf(TEXT(" %s %s"),
*Ref->TypeName,
*WingUtils::FormatName(Ref));
if (!Ref->ParentName.IsEmpty())
UWingServer::Printf(TEXT(" [parent: %s]"), *Ref->ParentName);
if (Ref->Inherited)
UWingServer::Print(TEXT(" [inherited]"));
UWingServer::Print(TEXT("\n"));
}

View File

@@ -91,7 +91,7 @@ public:
UEdGraphNode* Node = F.Node(Entry.Node).Cast<UEdGraphNode>();
if (!Node) return;
TArray<FWingProperty> All = FWingProperty::GetAll(Node, CPF_Edit);
TArray<FWingProperty> All = FWingProperty::GetDetailsMutable(Node, CPF_Edit);
FWingProperty P = FWingProperty::FindOneExactMatch(All, Entry.Name);
if (!P) return;

View File

@@ -40,7 +40,7 @@ public:
WingFetcher F;
UObject* Template = F.Walk(Object).Cast<UObject>();
if (!Template) return;
TArray<FWingProperty> AllProps = FWingProperty::GetAll(Template, CPF_Edit);
TArray<FWingProperty> AllProps = FWingProperty::GetDetailsImmutable(Template, CPF_Edit);
TArray<FWingProperty> Props = FWingProperty::FindAllSubstring(AllProps, Query);
if (Local)
{

View File

@@ -36,7 +36,7 @@ public:
UObject* Obj = F.Walk(Object).Cast<UObject>();
if (!Obj) return;
TArray<FWingProperty> All = FWingProperty::GetAll(Obj, CPF_Edit);
TArray<FWingProperty> All = FWingProperty::GetDetailsMutable(Obj, CPF_Edit);
FWingProperty P = FWingProperty::FindOneExactMatch(All, Property);
if (!P) return;

View File

@@ -42,7 +42,7 @@ public:
UWingServer::Print(TEXT("Error: No properties specified\n"));
return;
}
TArray<FWingProperty> All = FWingProperty::GetAll(Obj, CPF_Edit);
TArray<FWingProperty> All = FWingProperty::GetDetailsMutable(Obj, CPF_Edit);
int SuccessCount = 0;
// Validation pass — resolve all properties and values before modifying anything.

View File

@@ -1,5 +1,6 @@
#include "WingActorComponent.h"
#include "WingServer.h"
#include "WingTypes.h"
#include "WingUtils.h"
#include "Engine/Blueprint.h"
#include "Engine/SCS_Node.h"
@@ -8,191 +9,250 @@
#include "Components/SceneComponent.h"
#include "GameFramework/Actor.h"
#include "Kismet2/BlueprintEditorUtils.h"
#include "Engine/InheritableComponentHandler.h"
FWingActorComponent::FWingActorComponent(USCS_Node *Node) :
SCSNode(Node), Owner(Node->GetSCS()->GetBlueprint()->GeneratedClass) {}
FString FWingActorComponent::GetName() const
UActorComponent *UWingComponentReference::FindComponentInCDO(UClass *Class, FName Name)
{
if (SCSNode) return WingUtils::FormatName(SCSNode);
if (NativeComponent) return WingUtils::FormatName(NativeComponent);
return FString();
if (!Class) return nullptr;
AActor* CDO = Cast<AActor>(Class->GetDefaultObject());
if (!CDO) return nullptr;
return FindObjectFast<UActorComponent>(CDO, Name);
}
FString FWingActorComponent::GetClassName() const
USCS_Node* UWingComponentReference::FindSCSNodeByName(UBlueprint *BP, FName Name)
{
if (SCSNode)
return SCSNode->ComponentClass ? WingUtils::FormatName(SCSNode->ComponentClass) : TEXT("None");
if (NativeComponent)
return WingUtils::FormatName(NativeComponent->GetClass());
return FString();
}
FString FWingActorComponent::GetParentName() const
{
if (SCSNode)
for (UBlueprint *WalkBP : WingUtils::GetAncestorBlueprints(BP))
{
// Cross-blueprint case: parent is inherited, stored by name.
if (SCSNode->ParentComponentOrVariableName != NAME_None)
return WingUtils::SanitizeName(SCSNode->ParentComponentOrVariableName);
// Same-blueprint case: find the node whose child list contains us.
for (USCS_Node* Candidate : SCSNode->GetSCS()->GetAllNodes())
{
if (Candidate->ChildNodes.Contains(SCSNode))
return WingUtils::FormatName(Candidate);
}
return FString();
if (!WalkBP->SimpleConstructionScript) continue;
USCS_Node* Node = WalkBP->SimpleConstructionScript->FindSCSNode(Name);
if (Node != nullptr) return Node;
}
if (NativeComponent)
{
if (USceneComponent* Scene = Cast<USceneComponent>(NativeComponent))
if (USceneComponent* Parent = Scene->GetAttachParent())
return WingUtils::FormatName(Parent);
return FString();
}
return FString();
return nullptr;
}
bool FWingActorComponent::IsOwnedBy(const UBlueprint* BP) const
UWingComponentReference::FoundComponent UWingComponentReference::FindComponent(UBlueprint *BP, FName Name)
{
return BP && BP->GeneratedClass == Owner;
UClass* NativeClass = FBlueprintEditorUtils::FindFirstNativeClass(BP->ParentClass);
FoundComponent Result;
Result.Name = Name;
Result.SCS = FindSCSNodeByName(BP, Name);
Result.Native = FindComponentInCDO(NativeClass, Name);
return Result;
}
bool FWingActorComponent::CheckEditableComponent(UBlueprint *BP, const FWingActorComponent *Component)
bool UWingComponentReference::CheckValidParent(UWingComponentReference::FoundComponent FC)
{
if ((!Component) || (Component->IsEmpty()))
if (FC.SCS && FC.Native)
{
UWingServer::Printf(TEXT("Component does not exist.\n"));
UWingServer::Printf(TEXT("Weirdness, two components named %s\n"),
*WingUtils::SanitizeName(FC.Name));
return false;
}
if (Component->IsNative())
if ((FC.SCS == nullptr) && (FC.Native == nullptr))
{
UWingServer::Printf(TEXT("Cannot edit native components.\n"));
UWingServer::Printf(TEXT("No such parent component: %s\n"),
*WingUtils::SanitizeName(FC.Name));
return false;
}
if (!Component->IsOwnedBy(BP))
if (FC.Native && (!Cast<USceneComponent>(FC.Native)))
{
UWingServer::Printf(TEXT("Component %s is inherited, to edit it, you must edit BP %s\n"),
*WingUtils::FormatName(*Component), *WingUtils::FormatName(Component->Owner));
UWingServer::Printf(TEXT("Not a SceneComponent, so cannot be parent: %s\n"),
*WingUtils::SanitizeName(FC.Name));
return false;
}
return true;
}
bool FWingActorComponent::CheckValidParent(const FWingActorComponent *Parent)
bool UWingComponentReference::CheckNoSuchComponent(FoundComponent FC)
{
if ((!Parent) || (Parent->IsEmpty()))
if (FC.SCS || FC.Native)
{
UWingServer::Printf(TEXT("Cannot create a component without a parent.\n"));
return false;
}
if (Parent->IsNative() && (!Parent->NativeComponent->GetClass()->IsChildOf(USceneComponent::StaticClass())))
{
UWingServer::Printf(TEXT("Native component %s is not a scene component, cannot be a parent."),
*WingUtils::FormatName(Parent->NativeComponent));
UWingServer::Printf(TEXT("A component named %s already exists"),
*WingUtils::SanitizeName(FC.Name));
return false;
}
return true;
}
bool FWingActorComponent::CheckValidComponentClass(UClass *Class)
bool UWingComponentReference::CheckValidComponentClass(UClass *Class)
{
if (!Class->IsChildOf(UActorComponent::StaticClass()))
{
UWingServer::Printf(TEXT("Cannot create a component of class %s, which is not an actor component.\n"),
*WingUtils::FormatName(Class));
UWingServer::Printf(TEXT("Class does not derive from ActorComponent: %s\n"),
*WingUtils::FormatName(Class));
return false;
}
return true;
}
void FWingActorComponent::AddNodeInternal(UBlueprint *BP, USCS_Node *NewNode, const FWingActorComponent *Parent)
void UWingComponentReference::AddChildNode(UBlueprint *BP, USCS_Node *NewNode, FoundComponent Parent)
{
if (Parent->SCSNode && Parent->IsOwnedBy(BP))
if (Parent.SCS)
{
Parent->SCSNode->AddChildNode(NewNode, true);
}
else if (Parent->SCSNode)
{
NewNode->SetParent(Parent->SCSNode);
BP->SimpleConstructionScript->AddNode(NewNode);
if (Parent.SCS->GetSCS() == BP->SimpleConstructionScript)
{
Parent.SCS->AddChildNode(NewNode);
}
else
{
NewNode->SetParent(Parent.SCS);
BP->SimpleConstructionScript->AddNode(NewNode);
}
}
else
{
NewNode->SetParent(Cast<USceneComponent>(Parent->NativeComponent));
NewNode->SetParent(Cast<USceneComponent>(Parent.Native));
BP->SimpleConstructionScript->AddNode(NewNode);
}
}
USCS_Node *FWingActorComponent::AddComponent(UBlueprint *BP, UClass *Class, const FWingActorComponent* Parent, const FString &Name)
bool UWingComponentReference::AddComponent(UBlueprint *BP, UClass *Class, UWingComponentReference *Parent, FName Name)
{
if (!CheckValidParent(Parent)) return nullptr;
if (!CheckValidComponentClass(Class)) return nullptr;
FoundComponent ExistingComponent = FindComponent(BP, Name);
if (!CheckNoSuchComponent(ExistingComponent)) return false;
FoundComponent ParentComponent = FindComponent(BP, Parent->VariableName);
if (!CheckValidParent(ParentComponent)) return false;
if (!CheckValidComponentClass(Class)) return false;
USCS_Node *NewNode = BP->SimpleConstructionScript->CreateNode(Class, FName(*Name));
USCS_Node *NewNode = BP->SimpleConstructionScript->CreateNode(Class, Name);
if (NewNode == nullptr)
{
UWingServer::Printf(TEXT("Could not create new component %s of class %s, unknown reason.\n"),
*Name, *WingUtils::FormatName(Class));
return nullptr;
}
AddNodeInternal(BP, NewNode, Parent);
return NewNode;
}
bool FWingActorComponent::ReparentComponent(UBlueprint *BP, USCS_Node *SCSNode, const FWingActorComponent* Parent)
{
FWingActorComponent Component(SCSNode);
if (!CheckEditableComponent(BP, &Component)) return false;
if (!CheckValidParent(Parent)) return false;
if (Parent->SCSNode == SCSNode)
{
UWingServer::Printf(TEXT("You may not parent a component to itself.\n"));
return false;
}
if (Parent->SCSNode && Parent->SCSNode->IsChildOf(SCSNode))
{
UWingServer::Printf(TEXT("You may not parent a component to one of its own children.\n"));
*WingUtils::SanitizeName(Name), *WingUtils::FormatName(Class));
return false;
}
BP->SimpleConstructionScript->RemoveNode(SCSNode);
AddNodeInternal(BP, SCSNode, Parent);
AddChildNode(BP, NewNode, ParentComponent);
return true;
}
TArray<FWingActorComponent> FWingActorComponent::GetAll(UBlueprint* BP)
bool UWingComponentReference::ReparentComponent(UWingComponentReference *Parent)
{
TArray<FWingActorComponent> Result;
if (!BP) return Result;
FoundComponent ParentComponent = FindComponent(BP, Parent->VariableName);
if (!CheckValidParent(ParentComponent)) return false;
FoundComponent ThisComponent = FindComponent(BP, VariableName);
UClass* GenClass = BP->GeneratedClass;
if (!GenClass) return Result;
// Native components from CDO
if (AActor* CDO = Cast<AActor>(GenClass->GetDefaultObject()))
if (ThisComponent.SCS == nullptr)
{
UClass* NativeClass = FBlueprintEditorUtils::FindFirstNativeClass(GenClass);
TArray<UActorComponent*> NativeComponents;
CDO->GetComponents(NativeComponents);
for (UActorComponent* Comp : NativeComponents)
Result.Emplace(Comp, NativeClass);
UWingServer::Printf(TEXT("Cannot reparent native components."));
return false;
}
if (ThisComponent.SCS->GetSCS() != BP->SimpleConstructionScript)
{
UWingServer::Printf(TEXT("Component belongs to blueprint %s, edit that blueprint to reparent it."),
*WingUtils::FormatName(ThisComponent.SCS->GetSCS()->GetBlueprint()));
return false;
}
if (ParentComponent.SCS && ParentComponent.SCS->IsChildOf(ThisComponent.SCS))
{
UWingServer::Printf(TEXT("Cannot parent a component to itself or its own child."));
return false;
}
// SCS nodes, walking hierarchy from oldest ancestor to current BP
TArray<UBlueprint*> Hierarchy;
UBlueprint::GetBlueprintHierarchyFromClass(GenClass, Hierarchy);
for (int32 i = Hierarchy.Num() - 1; i >= 0; --i)
BP->SimpleConstructionScript->RemoveNode(ThisComponent.SCS);
AddChildNode(BP, ThisComponent.SCS, ParentComponent);
return true;
}
TMap<FName, FName> UWingComponentReference::CalculateParentNames(USimpleConstructionScript *Script)
{
TMap<FName, FName> ParentNames;
for (USCS_Node* Node : Script->GetAllNodes())
{
UBlueprint* WalkBP = Hierarchy[i];
if (!WalkBP->SimpleConstructionScript) continue;
ParentNames.Add(Node->GetVariableName(), Node->ParentComponentOrVariableName);
}
for (USCS_Node* Parent : Script->GetAllNodes())
{
for (USCS_Node *Child : Parent->GetChildNodes())
{
ParentNames[Child->GetVariableName()] = Parent->GetVariableName();
}
}
return ParentNames;
}
UActorComponent* UWingComponentReference::GetImmutableTemplate() const
{
if (Native) return FindComponentInCDO(BP->GeneratedClass, VariableName);
USCS_Node *Node = FindSCSNodeByName(BP, VariableName);
if (Node == nullptr) return nullptr;
FComponentKey Key(Node);
for (UBlueprint *WalkBP : WingUtils::GetAncestorBlueprints(BP, false))
{
if (Node->GetSCS()->GetBlueprint() == WalkBP) return Node->ComponentTemplate;
UInheritableComponentHandler* ICH = WalkBP->GetInheritableComponentHandler(/*bCreateIfNecessary=*/ false);
if (!ICH) continue;
UActorComponent* Override = ICH->GetOverridenComponentTemplate(Key);
if (Override) return Override;
}
return nullptr;
}
UActorComponent* UWingComponentReference::GetMutableTemplate() const
{
if (Native) return FindComponentInCDO(BP->GeneratedClass, VariableName);
USCS_Node* Node = FindSCSNodeByName(BP, VariableName);
if (!Node) return nullptr;
if (Node->GetSCS()->GetBlueprint() == BP) return Node->ComponentTemplate;
FComponentKey Key(Node);
UInheritableComponentHandler* ICH = BP->GetInheritableComponentHandler(/*bCreateIfNecessary=*/ true);
if (!ICH) return nullptr;
UActorComponent* Override = ICH->GetOverridenComponentTemplate(Key);
if (!Override) Override = ICH->CreateOverridenComponentTemplate(Key);
return Override;
}
TArray<UWingComponentReference*> UWingComponentReference::GetAll(UBlueprint* BP)
{
TArray<UWingComponentReference*> Result;
if (!BP) return Result;
// Find the native ancestor class
UClass* NativeClass = FBlueprintEditorUtils::FindFirstNativeClass(BP->ParentClass);
// Native components from the native ancestor's CDO
if (NativeClass)
{
if (AActor* CDO = Cast<AActor>(NativeClass->GetDefaultObject()))
{
TArray<UActorComponent*> NativeComponents;
CDO->GetComponents(NativeComponents);
for (UActorComponent* Comp : NativeComponents)
{
UWingComponentReference* Ref = NewObject<UWingComponentReference>();
Ref->BP = BP;
Ref->VariableName = Comp->GetFName();
Ref->TypeName = UWingTypes::TypeToText(Comp->GetClass());
if (USceneComponent* Scene = Cast<USceneComponent>(Comp))
if (USceneComponent* AttachParent = Scene->GetAttachParent())
Ref->ParentName = WingUtils::SanitizeName(AttachParent->GetFName());
Ref->Native = true;
Ref->Inherited = true;
Result.Add(Ref);
}
}
}
// Visit all SCS nodes, oldest first.
for (UBlueprint *WalkBP : WingUtils::GetAncestorBlueprints(BP, true))
{
if (WalkBP->SimpleConstructionScript == nullptr) continue;
TMap<FName, FName> ParentNames = CalculateParentNames(WalkBP->SimpleConstructionScript);
for (USCS_Node* Node : WalkBP->SimpleConstructionScript->GetAllNodes())
Result.Emplace(Node);
{
UWingComponentReference* Ref = NewObject<UWingComponentReference>();
Ref->BP = BP;
Ref->VariableName = Node->GetVariableName();
Ref->TypeName = UWingTypes::TypeToText(Node->ComponentClass);
Ref->ParentName = WingUtils::SanitizeName(ParentNames[Node->GetVariableName()]);
Ref->Native = false;
Ref->Inherited = (WalkBP != BP);
Result.Add(Ref);
}
}
return Result;
}

View File

@@ -302,25 +302,19 @@ WingFetcher& WingFetcher::Component(const FString& Value)
return SetError();
}
TArray<FWingActorComponent> AllComponents = FWingActorComponent::GetAll(BP);
FWingActorComponent* Comp = WingUtils::FindExactlyOneNamed(Value, AllComponents, TEXT("component"));
if (!Comp)
TArray<UWingComponentReference*> AllComponents = UWingComponentReference::GetAll(BP);
UWingComponentReference* Found = WingUtils::FindExactlyOneNamed(Value, AllComponents, TEXT("component"));
if (!Found)
{
UWingServer::Printf(TEXT("Components that exist in the blueprint:\n"));
for (const FWingActorComponent &C : AllComponents)
for (const UWingComponentReference* C : AllComponents)
{
UWingServer::Printf(TEXT(" %s\n"), *WingUtils::FormatName(C));
}
return SetError();
}
if (!Comp->IsOwnedBy(BP))
{
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();
}
SetObj(Comp->SCSNode);
SetObj(Found);
return *this;
}

View File

@@ -275,7 +275,7 @@ void WingGraphExport::EmitMaterialProperties(UEdGraphNode* Node, FStringBuilderB
UMaterialExpression* Expression = MatNode->MaterialExpression;
FString PrimaryCategory = Expression->GetClass()->GetName();
TArray<FWingProperty> Props = FWingProperty::GetAll(Expression, CPF_Edit);
TArray<FWingProperty> Props = FWingProperty::GetDetailsMutable(Expression, CPF_Edit);
for (const FWingProperty& WP : Props)
{

View File

@@ -1,4 +1,5 @@
#include "WingProperty.h"
#include "WingActorComponent.h"
#include "WingUtils.h"
#include "WingHandler.h"
#include "WingServer.h"
@@ -256,14 +257,12 @@ void FWingProperty::Move(TArray<FWingProperty> &Out, TArray<FWingProperty> &In,
In.SetNum(Dst);
}
TArray<FWingProperty> FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
TArray<FWingProperty> FWingProperty::GetDetailsGeneral(UObject* Obj, EPropertyFlags Flags, bool Mutable)
{
if (!Obj) return {};
// Blueprints don't have editable properties. So
// instead, we fetch properties from the generated CDO,
// which is probably what the user intended.
//
// instead, we fetch properties from the generated CDO.
if (UBlueprint *BP = ::Cast<UBlueprint>(Obj))
{
if (BP->GeneratedClass == nullptr)
@@ -274,17 +273,15 @@ TArray<FWingProperty> FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
Obj = BP->GeneratedClass->GetDefaultObject();
}
// SCS nodes don't have useful editable properties.
// Redirect to the component template instead.
//
if (USCS_Node* Node = ::Cast<USCS_Node>(Obj))
// Component references: get the proper template.
if (UWingComponentReference* Ref = ::Cast<UWingComponentReference>(Obj))
{
if (!Node->ComponentTemplate)
Obj = Mutable ? Ref->GetMutableTemplate() : Ref->GetImmutableTemplate();
if (!Obj)
{
UWingServer::Printf(TEXT("ERROR: SCS node '%s' has no component template\n"), *Obj->GetName());
UWingServer::Printf(TEXT("ERROR: Component '%s' has no template\n"), *Ref->VariableName.ToString());
return {};
}
Obj = Node->ComponentTemplate;
}
TArray<FWingProperty> Result;
@@ -315,6 +312,11 @@ TArray<FWingProperty> FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
return Result;
}
TArray<FWingProperty> FWingProperty::GetAll(UObject* Object, EPropertyFlags Flags)
{
return GetAll(Object->GetClass(), Object, Flags);
}
TArray<FWingProperty> FWingProperty::GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags)
{
TArray<FWingProperty> Result;

View File

@@ -269,9 +269,9 @@ FString WingUtils::FormatName(const FBPInterfaceDescription &IFace)
return FormatName(IFace.Interface);
}
FString WingUtils::FormatName(const FWingActorComponent &Comp)
FString WingUtils::FormatName(const UWingComponentReference *Ref)
{
return SanitizeName(Comp.GetName());
return SanitizeName(Ref->VariableName);
}
FString WingUtils::FormatName(const UWidget *Widget)
@@ -473,6 +473,18 @@ TArray<UEdGraphNode*> WingUtils::AllNodes(UEdGraph *Graph)
return Result;
}
TArray<UBlueprint*> WingUtils::GetAncestorBlueprints(UBlueprint *BP, bool OldestFirst)
{
TArray<UBlueprint*> Blueprints;
for (UBlueprint* WalkBP = BP; WalkBP; )
{
Blueprints.Add(WalkBP);
WalkBP = UBlueprint::GetBlueprintFromClass(WalkBP->ParentClass);
}
if (OldestFirst) Algo::Reverse(Blueprints);
return Blueprints;
}
// ============================================================
// Material helpers
// ============================================================

View File

@@ -6,42 +6,62 @@
class UBlueprint;
class USCS_Node;
class UActorComponent;
class USimpleConstructionScript;
USTRUCT()
struct FWingActorComponent
// a 'Component Reference' is basically just a pointer to a
// blueprint plus a component name. The other fields in here
// are purely for convenience of printing things out, they
// are *never* used for any analysis.
//
UCLASS()
class UWingComponentReference : public UObject
{
GENERATED_BODY()
USCS_Node* SCSNode = nullptr;
UActorComponent* NativeComponent = nullptr;
UClass* Owner = nullptr;
public:
// The blueprint that was queried: not necessarily the
// same blueprint that defined the component.
UPROPERTY()
UBlueprint* BP = nullptr;
// The raw component name, not sanitized yet.
FName VariableName;
FWingActorComponent() = default;
FWingActorComponent(USCS_Node* Node);
FWingActorComponent(UActorComponent* InNative, UClass* InOwner) : NativeComponent(InNative), Owner(InOwner) {}
// Sanitized Parent Name (for display only)
FString ParentName;
explicit operator bool() const { return SCSNode || NativeComponent; }
// Sanitized TypeName of the component (for display only)
FString TypeName;
FString GetName() const;
FString GetClassName() const;
FString GetParentName() const;
bool IsEmpty() const { return SCSNode == nullptr && NativeComponent == nullptr; }
bool IsNative() const { return NativeComponent != nullptr; }
bool IsOwnedBy(const UBlueprint* BP) const;
// True if the component is native (for display only)
bool Native = false;
// Add a new SCS node, underneath the existing parent.
static USCS_Node *AddComponent(UBlueprint *BP, UClass *Class, const FWingActorComponent* Parent, const FString &Name);
// True if the component was inherited (for display only)
bool Inherited = false;
// Reparent an existing SCS node to a specified parent.
static bool ReparentComponent(UBlueprint *BP, USCS_Node *Component, const FWingActorComponent *Parent);
UActorComponent* GetImmutableTemplate() const;
UActorComponent* GetMutableTemplate() const;
bool ReparentComponent(UWingComponentReference *Parent);
bool DeleteComponent();
static bool AddComponent(UBlueprint *BP, UClass *Class, UWingComponentReference *Parent, FName Name);
// Collect all components: native from CDO first, then SCS nodes
// walking the hierarchy from oldest ancestor to current blueprint.
static TArray<FWingActorComponent> GetAll(UBlueprint* BP);
static TArray<UWingComponentReference*> GetAll(UBlueprint* BP);
private:
static bool CheckEditableComponent(UBlueprint *BP, const FWingActorComponent *Component);
static bool CheckValidParent(const FWingActorComponent *Component);
static bool CheckValidComponentClass(UClass *Class);
static void AddNodeInternal(UBlueprint *BP, USCS_Node *NewNode, const FWingActorComponent *Parent);
private:
struct FoundComponent
{
FName Name;
USCS_Node *SCS;
UActorComponent *Native;
};
static UActorComponent *FindComponentInCDO(UClass *Class, FName Name);
static USCS_Node* FindSCSNodeByName(UBlueprint *BP, FName VariableName);
static FoundComponent FindComponent(UBlueprint *BP, FName Name);
static bool CheckValidParent(FoundComponent FC);
static bool CheckNoSuchComponent(FoundComponent FC);
static void AddChildNode(UBlueprint *BP, USCS_Node *Node, FoundComponent Parent);
static TMap<FName, FName> CalculateParentNames(USimpleConstructionScript *Script);
};

View File

@@ -28,19 +28,53 @@ struct FWingProperty
explicit operator bool() const { return Prop != nullptr; }
FProperty* operator->() const { return Prop; }
static TArray<FWingProperty> GetAll(UObject* Obj, EPropertyFlags Flags);
// Return the "Details Panel" properties for the specified object.
//
// This is the set of properties that would typically appear in the
// details panel if you were to click the object in the editor. Note
// that this is not always the properties of the object itself. For
// example, if you click a widget, the details panel shows you
// properties of the widget, but also properties of the slot. If you
// click a material graph node, the details panel shows you properties
// of the node, but also properties of the material expression.
//
// When editing an inherited ActorComponent, you're actually editing
// properties that *override* the original properties of the ActorComponent.
// The mutable version, 'GetDetailsMutable', tells this function to first
// create the overrides, and then edit those.
//
// For a more direct "just give me the properties of this object,"
// use the GetAll function.
//
static TArray<FWingProperty> GetDetailsMutable(UObject* Obj, EPropertyFlags Flags)
{ return GetDetailsGeneral(Obj, Flags, true); }
static TArray<FWingProperty> GetDetailsImmutable(UObject *Obj, EPropertyFlags Flags)
{ return GetDetailsGeneral(Obj, Flags, false); }
// Get the raw properties of the specified object or struct.
//
// This gets the properties that are literally present in the
// specified object or struct. No special interpretation is done.
//
static TArray<FWingProperty> GetAll(UObject* Object, EPropertyFlags Flags);
static TArray<FWingProperty> GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags);
// Functions to find items by name in an array of properties.
//
static TArray<FWingProperty> FindAllSubstring(const TArray<FWingProperty>& Props, const FString& Substring);
static FWingProperty FindOneExactMatch(const TArray<FWingProperty>& Props, const FString& Name);
static void Remove(TArray<FWingProperty>& Props, const FString& Name);
static void Move(TArray<FWingProperty> &Out, TArray<FWingProperty> &In, const FString &Name);
// Functions to populate properties from a JSON object.
//
static bool PopulateFromJson(FWingProperty& Prop, const FJsonObject* Json, bool AllOptional = false);
static bool PopulateFromJson(TArray<FWingProperty>& Props, const FJsonObject* Json, bool AllOptional = false);
static bool PopulateFromJson(UStruct* StructType, void* Container, const FJsonObject* Object);
static bool PopulateFromJson(UStruct* StructType, void* Container, const TSharedPtr<FJsonValue>& Object);
private:
static TArray<FWingProperty> GetDetailsGeneral(UObject* Obj, EPropertyFlags Flags, bool Mutable);
void PrintExpectsReceived(const TCHAR *Type);
static void Collect(UStruct* Struct, void* Container, TArray<FWingProperty> &Props, EPropertyFlags Flags);
};

View File

@@ -34,6 +34,7 @@ struct FBPVariableDescription;
struct FUserPinInfo;
struct FBPInterfaceDescription;
struct FWingActorComponent;
class UWingComponentReference;
// Stateless utility functions used by MCP handlers and the MCP server.
// This is effectively a namespace — all methods are static.
@@ -80,6 +81,7 @@ public:
static FString FormatName(const FUserPinInfo &Pin);
static FString FormatName(const FBPInterfaceDescription &IFace);
static FString FormatName(const FWingActorComponent &Comp);
static FString FormatName(const UWingComponentReference *Ref);
static FString FormatName(const UWidget *Widget);
////////////////////////////////////////////////////////
@@ -208,7 +210,7 @@ public:
static TArray<UEdGraph*> AllGraphs(UBlueprint* BP);
static TArray<UEdGraphNode*> AllNodes(UBlueprint* BP);
static TArray<UEdGraphNode*> AllNodes(UEdGraph *Graph);
static TArray<UBlueprint*> GetAncestorBlueprints(UBlueprint *BP, bool OldestFirst = false);
// ----- Material helpers -----
static void EnsureMaterialGraph(UMaterial* Material);

56
integration-MEMORY.md Normal file
View File

@@ -0,0 +1,56 @@
## PATIENCE!
The user likes to take his time. Do not start doing things until he gives
you explicit instructions! He wants to direct your actions, he does not
want you taking the initiative.
## Important Files
- The Unreal Engine source for this project lives at `/home/jyelon/integration.UE` (sibling to the project dir), not `~/UnrealEngine`.
- Many of Unreal's basic data types (FString, TArray, TSet, TMap, etc.) are in `integration.UE/Engine/Source/Runtime/Core/Public/Containers/`.
- Editor log files are at `integration/User/jyelon/Saved/Logs/`.
## Critical: Minimize Diff Noise
- The user reviews all changes via `git diff` in VS Code. Every unnecessary diff line costs review time.
- **Preserve all existing comments.** Comments describe the logic, not the syntax — they survive refactors. Only remove a comment if the code it described was genuinely deleted.
- **Don't rename variables, reorder code, or reformat lines** unless directly required by the task. If a variable name works, leave it. If a comment is in the right place, don't move it.
- Don't make gratuitous changes. If it doesn't need to change, don't touch it.
## Coding Style Preferences
- **NO GLOBALLY SCOPED STATIC FUNCTIONS in Unreal code.** Use class methods or namespace-scoped functions instead. This is a hard rule — the user has corrected this multiple times.
- Prefer in-class member initializers (`int x_ = 0;` in the header) over explicit initialization in constructor bodies. When touching constructors, migrate toward this style.
- Inline empty constructors/destructors in the header when possible.
- Avoid ternary operator unless very concise (e.g., `x ? 1 : 0`). For longer expressions, use early-return `if` instead.
- When asked for a small change, do exactly that. If it turns out to require broader refactoring, STOP and explain the situation instead of silently rewriting the module.
## Tools
- `tools/clangd-query.py` — Query clangd for C++ symbols.
- `python3 tools/clangd-query.py symbol <name>` — Find symbols by name across the whole project.
- `python3 tools/clangd-query.py definition <file> <line> <col>` — Go to definition
- `python3 tools/clangd-query.py references <file> <line> <col>` — Find all references
- `python3 tools/clangd-query.py diagnostics <file>` — Get errors/warnings for a file.
- `python3 tools/clangd-query.py stop` — Stop the daemon.
- Prefer this over grep when looking for C++ class/function definitions or references.
- Use `python3 build.py c++` for lightweight C++ rebuilds, `python3 build.py all` for full rebuilds.
- VS Code is the IDE. Use `code --goto <filename>:<line>` to open files.
## MCP
The UE Wingman plugin for unreal allows you to control the unreal editor. Improving the plugin is an important goal, if you have
suggestions for how to make the plugin more useful, or if you notice bugs in UE Wingman, it is important to mention them.
## Feedback
- [Use mv for file renames](feedback_use_mv_for_renames.md) — use `mv` instead of copy-and-rewrite when renaming files
## PATIENCE! AGAIN!
You really do like solving problems, but this repo belongs to the
user. Remember, you are a guest here. No altering anything without
his direct instructions.