Work on blueprint components in MCP

This commit is contained in:
2026-03-19 15:53:25 -04:00
parent 2e4606c9e4
commit 56f2257dd9
15 changed files with 385 additions and 202 deletions

View File

@@ -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<USceneComponent>(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<USceneComponent>(Parent->NativeComponent));
return true;
}
TArray<FWingActorComponent> FWingActorComponent::GetAll(UBlueprint* BP)
{
TArray<FWingActorComponent> Result;

View File

@@ -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<FWingActorComponent> 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)

View File

@@ -19,6 +19,13 @@ FWingProperty::FWingProperty(FProperty* InProp, void* InContainer)
FWingProperty::FWingProperty(FProperty* InProp, UObject* InContainer)
: Prop(InProp), Container(static_cast<void*>(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<void>(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<void>(Container);
@@ -75,19 +92,26 @@ bool FWingProperty::SetText(const FString &Value)
void FWingProperty::Collect(UStruct* StructType, void* Container, TArray<FWingProperty> &Props, EPropertyFlags Flags)
{
TMap<FString, TArray<FWingProperty>> Grouped;
for (TFieldIterator<FProperty> 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<FString> Categories;
void FWingProperty::Collect(UObject* Container, TArray<FWingProperty> &Props, EPropertyFlags Flags)
{
for (TFieldIterator<FProperty> 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<FWingProperty>& Props, const FString& Name)
TArray<FWingProperty> FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
{
if (!Obj) return {};
TArray<FWingProperty> Result;
// Blueprints don't have editable properties. So
// instead, we fetch properties from the generated CDO,
@@ -115,7 +138,8 @@ TArray<FWingProperty> FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
Obj = BP->GeneratedClass->GetDefaultObject();
}
Collect(Obj, Result, Flags);
TArray<FWingProperty> 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> FWingProperty::GetAll(UObject* Obj, EPropertyFlags Flags)
{
if (UMaterialExpression* Expr = MatNode->MaterialExpression)
{
Collect(Expr, Result, Flags);
Collect(Expr->GetClass(), Expr, Result, Flags);
}
}
return Result;
}

View File

@@ -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)

View File

@@ -46,16 +46,11 @@
// ============================================================
// Name sanitization
//
// Our parsers reserve certain punctuation marks for parsing
// types, paths, and the like. For example: Array<Int>.
// 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());
}
// ============================================================