From efb1c94144c9609ee43a9355460ba1d1edfb3724 Mon Sep 17 00:00:00 2001 From: jyelon Date: Wed, 1 Apr 2026 05:10:00 -0400 Subject: [PATCH] Working on Asset_Create --- Content/Testing/BP1.uasset | 3 - Content/Testing/BP_T1.uasset | 3 + Content/Testing/BP_Test1.uasset | 3 - .../Source/UEWingman/Handlers/Asset_Create.h | 39 ++++ .../UEWingman/Handlers/Asset_SearchTypes.h | 63 ++++++ .../UEWingman/Handlers/Blueprint_Create.h | 2 +- .../UEWingman/Private/WingFactories.cpp | 181 ++++++++++++++++++ .../Source/UEWingman/Private/WingManual.cpp | 10 +- .../UEWingman/Private/WingPackageMaker.cpp | 52 +++++ .../Source/UEWingman/Private/WingProperty.cpp | 64 +++++-- .../Source/UEWingman/Public/WingFactories.h | 42 ++++ .../UEWingman/Public/WingPackageMaker.h | 40 +--- .../Source/UEWingman/Public/WingProperty.h | 9 + 13 files changed, 453 insertions(+), 58 deletions(-) delete mode 100644 Content/Testing/BP1.uasset create mode 100644 Content/Testing/BP_T1.uasset delete mode 100644 Content/Testing/BP_Test1.uasset create mode 100644 Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Create.h create mode 100644 Plugins/UEWingman/Source/UEWingman/Handlers/Asset_SearchTypes.h create mode 100644 Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp create mode 100644 Plugins/UEWingman/Source/UEWingman/Private/WingPackageMaker.cpp create mode 100644 Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h diff --git a/Content/Testing/BP1.uasset b/Content/Testing/BP1.uasset deleted file mode 100644 index d485839a..00000000 --- a/Content/Testing/BP1.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:023eaf937ba6b115dd7f52faacac99d31e9f334bb040fbc6b2de27ab3086096f -size 24131 diff --git a/Content/Testing/BP_T1.uasset b/Content/Testing/BP_T1.uasset new file mode 100644 index 00000000..f98b4da1 --- /dev/null +++ b/Content/Testing/BP_T1.uasset @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:dca01920076a3e51c87d3fbab8c11f1e57f069086b7aeb67876f8f6c21999a3a +size 14090 diff --git a/Content/Testing/BP_Test1.uasset b/Content/Testing/BP_Test1.uasset deleted file mode 100644 index 00a61a77..00000000 --- a/Content/Testing/BP_Test1.uasset +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:23a8acb077ec95e1f6d6f367b1984c4cbdb7593970924e0d53738553fdc12c40 -size 27311 diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Create.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Create.h new file mode 100644 index 00000000..15b5bdcf --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_Create.h @@ -0,0 +1,39 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingFactories.h" +#include "Asset_Create.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UWing_Asset_Create : public UObject, public IWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Full asset path for the new asset (e.g. '/Game/MyFolder/MyAsset')")) + FString AssetPath; + + UPROPERTY(meta=(Description="Factory type name from Asset_SearchTypes (e.g. 'Material', 'Blueprint')")) + FString Factory; + + UPROPERTY(meta=(Optional, Description="Factory configuration properties as key-value pairs")) + FWingJsonObject Config; + + virtual FString GetDescription() const override + { + return TEXT("Create a new asset using a factory. Use Asset_SearchTypes to find " + "available factory types and their configurable properties."); + } + + virtual void Handle() override + { + UWingFactories::CreateAsset(AssetPath, Factory, Config); + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_SearchTypes.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_SearchTypes.h new file mode 100644 index 00000000..44137c98 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Asset_SearchTypes.h @@ -0,0 +1,63 @@ +#pragma once + +#include "CoreMinimal.h" +#include "WingServer.h" +#include "WingHandler.h" +#include "WingFactories.h" +#include "Asset_SearchTypes.generated.h" + + +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- +// --------------------------------------------------------------------------- + +UCLASS() +class UWing_Asset_SearchTypes : public UObject, public IWingHandler +{ + GENERATED_BODY() + +public: + UPROPERTY(meta=(Description="Query string, can contain *")) + FString Query; + + UPROPERTY(meta=(Optional, Description="Maximum number of results (default 50)")) + int32 MaxResults = 50; + + virtual FString GetDescription() const override + { + return TEXT("Search for asset factory types that can be used with Asset_Create. " + "Returns factory names and their configurable properties."); + } + + virtual void Handle() override + { + FString ExtQuery = TEXT("*") + Query + TEXT("*"); + const TArray& All = UWingFactories::AllFactories(); + + int32 Count = 0; + for (const UWingFactories::Info& Entry : All) + { + if (Count >= MaxResults) break; + if (!Entry.CanCreateNew()) continue; + if (!Entry.Name.MatchesWildcard(ExtQuery, ESearchCase::IgnoreCase)) continue; + + UWingServer::Printf(TEXT("%s\n"), *Entry.Name); + + for (const FName& Prop : Entry.Config) + { + UWingServer::Printf(TEXT(" %s\n"), *Prop.ToString()); + } + + Count++; + } + + if (Count == 0) + { + UWingServer::Print(TEXT("No matching factory types found.\n")); + } + else if (Count >= MaxResults) + { + UWingServer::Printf(TEXT("WARNING: Reached limit of %d results. You may specify MaxResults.\n"), MaxResults); + } + } +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h b/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h index f8fae457..a9dce7d5 100644 --- a/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h +++ b/Plugins/UEWingman/Source/UEWingman/Handlers/Blueprint_Create.h @@ -75,7 +75,7 @@ public: UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint( ParentClassObj, Maker.Package(), - FName(*Maker.Name()), + Maker.GetFName(), BlueprintType, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass() diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp new file mode 100644 index 00000000..7bc6a52b --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingFactories.cpp @@ -0,0 +1,181 @@ +#include "WingFactories.h" +#include "WingServer.h" +#include "Editor.h" +#include "WingUtils.h" +#include "WingProperty.h" +#include "WingPackageMaker.h" +#include "Kismet2/KismetEditorUtilities.h" +#include "Kismet2/EnumEditorUtils.h" +#include "Factories/BlueprintFactory.h" +#include "Factories/EnumFactory.h" +#include "Algo/BinarySearch.h" + +void UWingFactories::Initialize(FSubsystemCollectionBase& Collection) +{ + Super::Initialize(Collection); + PopulateRegistry(); +} + +const TArray& UWingFactories::AllFactories() +{ + return GEditor->GetEditorSubsystem()->Registry; +} + +bool UWingFactories::Info::CanCreateNew() const +{ + return FactoryClass->GetDefaultObject()->CanCreateNew(); +} + +void UWingFactories::PopulateRegistry() +{ + TArray FactoryClasses; + GetDerivedClasses(UFactory::StaticClass(), FactoryClasses); + + // Populate it with initial data. + for (UClass* Class : FactoryClasses) + { + if (Class->HasAnyClassFlags(CLASS_Abstract)) continue; + + Info Entry; + Entry.Name = DeriveFactoryName(Class); + Entry.FactoryClass = Class; + Entry.Config = FWingProperty::GetNames(Class, CPF_Edit); + Registry.Add(MoveTemp(Entry)); + } + + // Sort the registry. + Registry.Sort([](const Info& A, const Info& B) { return A.Name < B.Name; }); + + // Blacklist certain bad factories. + DisableFactory(TEXT("PhysicsAsset")); // PhysicsAsset factory pops a modal dialog +} + +void UWingFactories::DisableFactory(const TCHAR* Name) +{ + Info* Entry = Find(Name); + if (!Entry) + { + UE_LOG(LogTemp, Fatal, TEXT("UWingFactories::DisableFactory: factory '%s' not found"), Name); + return; + } + Entry->Disabled = true; +} + +UObject* UWingFactories::CreateAsset(const FString& Path, const FString& FactoryName, FWingJsonObject& Config) +{ + UWingFactories* Self = GEditor->GetEditorSubsystem(); + + // Look up the factory info. + const Info* FactoryInfo = Self->Find(FactoryName); + if (!FactoryInfo) + { + UWingServer::Printf(TEXT("ERROR: Unknown factory '%s'\n"), *FactoryName); + return nullptr; + } + + // Make sure this is the a creation factory, as opposed to an import factory. + if (!FactoryInfo->CanCreateNew()) + { + UWingServer::Printf(TEXT("ERROR: Factory '%s' cannot create objects from scratch\n"), *FactoryName); + return nullptr; + } + + // Validate the path, and that there's not already something there. + WingPackageMaker Maker(Path); + if (!Maker.Ok()) return nullptr; + FName Name = Maker.GetFName(); + + // Create the factory instance. + UFactory* Factory = NewObject(GetTransientPackage(), FactoryInfo->FactoryClass); + + // Get the editable properties + TArray Props = + FWingProperty::GetNamed(Factory->GetClass(), Factory, FactoryInfo->Config); + + // if there is no config table, make a blank config table. + TSharedPtr ConfigJson = Config.Json; + if (ConfigJson == nullptr) ConfigJson = MakeShared(); + + // Populate the configuration properties from the json. + if (!FWingProperty::PopulateFromJson(Props, ConfigJson.Get(), false)) + return nullptr; + + // Pre-check: block factories that would cause problems. + // In particular, this blocks those that would pop a dialog. + if (!PreCheck(Factory, Name, Path)) return nullptr; + + // Create the asset. + if (!Maker.Make()) return nullptr; + UObject* NewAsset = Factory->FactoryCreateNew( + FactoryInfo->FactoryClass->GetDefaultObject()->GetSupportedClass(), + Maker.Package(), + Name, + RF_Public | RF_Standalone | RF_Transactional, + nullptr, + GWarn + ); + if (!NewAsset) + { + UWingServer::Printf(TEXT("ERROR: Factory '%s' returned null\n"), *FactoryName); + return nullptr; + } + + UWingServer::Printf(TEXT("Created: %s\n"), *WingUtils::ExternalizeID(Name)); + return NewAsset; +} + +bool UWingFactories::PreCheck(UFactory* Factory, FName Name, const FString &Path) +{ + // Blueprint factories: FactoryCreateNew pops FMessageDialog if ParentClass is invalid. + if (UBlueprintFactory* BPFactory = Cast(Factory)) + { + if (!BPFactory->ParentClass) + { + UWingServer::Print(TEXT("ERROR: ParentClass must be set\n")); + return false; + } + if (!FKismetEditorUtilities::CanCreateBlueprintOfClass(BPFactory->ParentClass)) + { + UWingServer::Printf(TEXT("ERROR: Cannot create a blueprint based on class '%s'\n"), *BPFactory->ParentClass->GetName()); + return false; + } + } + + // Enum factory: FactoryCreateNew pops FMessageDialog if the name already exists. + if (UEnumFactory* EFactory = Cast(Factory)) + { + if(!FEnumEditorUtils::IsNameAvailebleForUserDefinedEnum(Name)) + { + UWingServer::Printf(TEXT("Enum name is already taken: %s"), + *WingUtils::ExternalizeID(Name)); + } + } + + return true; +} + +UWingFactories::Info* UWingFactories::Find(const FString& Name) +{ + int32 Index = Algo::LowerBound(Registry, Name, [](Info& Entry, const FString& N) { + return Entry.Name < N; + }); + if (Index < Registry.Num() && Registry[Index].Name == Name) + { + return &Registry[Index]; + } + return nullptr; +} + +FString UWingFactories::DeriveFactoryName(UClass* FactoryClass) +{ + FString Name = FactoryClass->GetName(); + if (Name.EndsWith(TEXT("FactoryNew"))) + { + Name.LeftChopInline(10); + } + else if (Name.EndsWith(TEXT("Factory"))) + { + Name.LeftChopInline(7); + } + return Name; +} diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp index 2c3e0e11..56708340 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingManual.cpp @@ -60,11 +60,6 @@ void WingManual::PrintHandlerHelp(UClass* HandlerClass) void WingManual::PrintManual(TSet
Sections, UClass *Handler, bool Abridged) { - if (Handler == nullptr) - { - Sections.Remove(Section::HandlerHelp); - } - if (Sections.IsEmpty()) return; const bool bPrintAll = Sections.Contains(Section::All); @@ -74,7 +69,7 @@ void WingManual::PrintManual(TSet
Sections, UClass *Handler, bool Abrid UWingServer::Printf(TEXT("\n--- AUTOMATIC DOCUMENTATION ---\n")); } - if (Sections.Contains(Section::HandlerHelp) || bPrintAll) + if (Handler && (Sections.Contains(Section::HandlerHelp) || bPrintAll)) { PrintHandlerHelp(Handler); } @@ -286,6 +281,9 @@ void WingManual::PrintManual(TSet
Sections, UClass *Handler, bool Abrid "\n Graph_Dump: a fairly detailed listing of any Graph" "\n Property_Dump: show information on many objects" "\n" + "\n You can use ShowCommands(Query=SomeCommand,Verbose=true)" + "\n to get detailed help for a specific command." + "\n" )); } diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingPackageMaker.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingPackageMaker.cpp new file mode 100644 index 00000000..8913a420 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingPackageMaker.cpp @@ -0,0 +1,52 @@ +#include "WingPackageMaker.h" +#include "AssetRegistry/AssetRegistryModule.h" + +WingPackageMaker::WingPackageMaker(const FString& InFullPath) + : FullPath(InFullPath) +{ + if (!CheckNewAssetPath(InFullPath)) + { + bError = true; + return; + } +} + +bool WingPackageMaker::CheckNewAssetPath(const FString& Path) +{ + if (!FPackageName::IsValidTextForLongPackageName(Path)) + { + UWingServer::Printf(TEXT("ERROR: Package path '%s' is not a valid package name\n"), *Path); + return false; + } + if (!Path.StartsWith(TEXT("/Game"))) + { + UWingServer::Printf(TEXT("ERROR: Package path '%s' must start with '/Game'\n"), *Path); + return false; + } + if (FindObject(nullptr, *Path)) + { + UWingServer::Printf(TEXT("ERROR: An asset already exists at '%s'\n"), *Path); + return false; + } + return true; +} + + + +bool WingPackageMaker::Make() +{ + if (bError) return false; + Pkg = CreatePackage(*FullPath); + if (!Pkg) + { + UWingServer::Printf(TEXT("ERROR: Failed to create package at '%s'\n"), *FullPath); + bError = true; + return false; + } + + Pkg->ClearFlags(RF_Transient); + Pkg->SetIsExternallyReferenceable(true); + Pkg->MarkPackageDirty(); + return true; +} + diff --git a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp index eb5c34ee..744a54ea 100644 --- a/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp +++ b/Plugins/UEWingman/Source/UEWingman/Private/WingProperty.cpp @@ -107,9 +107,27 @@ bool FWingProperty::SetText(FString Value) *Value, *WingUtils::FormatName(Prop), *Prop->GetCPPType()); return false; } + if (!CheckImportTextResult(Value)) return false; return true; } +bool FWingProperty::CheckImportTextResult(const FString &Value) +{ + uint8 *VP = Prop->ContainerPtrToValuePtr(Container); + if (FObjectPropertyBase *OProp = CastField(Prop)) + { + UObject *Obj = OProp->GetObjectPropertyValue(VP); + if (Obj == nullptr && Value.TrimStartAndEnd().Compare(TEXT("None"), ESearchCase::IgnoreCase) != 0) + { + UWingServer::Printf(TEXT("ERROR: Failed to parse '%s' for property '%s'\n"), + *Value, *WingUtils::FormatName(Prop)); + return false; + } + } + return true; +} + + bool FWingProperty::SetJson(const TSharedPtr &JsonValue) { if (JsonValue->Type == EJson::String) @@ -235,6 +253,41 @@ void FWingProperty::Collect(UStruct* StructType, void* Container, TArray FWingProperty::GetAll(UObject* Object, EPropertyFlags Flags) +{ + return GetAll(Object->GetClass(), Object, Flags); +} + +TArray FWingProperty::GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags) +{ + TArray Result; + Collect(StructType, Container, Result, Flags); + return Result; +} + +TArray FWingProperty::GetNamed(UStruct* StructType, void* Container, const TArray &Names) +{ + TArray Result; + for (FName Name : Names) + { + FProperty *Prop = StructType->FindPropertyByName(Name); + if (Prop != nullptr) Result.Emplace(Prop, Container); + } + return Result; +} + + +TArray FWingProperty::GetNames(UStruct *StructType, EPropertyFlags Flags) +{ + TArray Result; + for (TFieldIterator It(StructType); It; ++It) + { + if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue; + Result.Add(It->GetFName()); + } + return Result; +} + void FWingProperty::Remove(TArray& Props, const FString& Name) { Props.RemoveAll([&](const FWingProperty& P) { return P.Prop->GetName() == Name; }); @@ -312,17 +365,6 @@ TArray FWingProperty::GetDetailsGeneral(UObject* Obj, EPropertyFl return Result; } -TArray FWingProperty::GetAll(UObject* Object, EPropertyFlags Flags) -{ - return GetAll(Object->GetClass(), Object, Flags); -} - -TArray FWingProperty::GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags) -{ - TArray Result; - Collect(StructType, Container, Result, Flags); - return Result; -} TArray FWingProperty::FindAllSubstring(const TArray& Props, const FString& Substring) { diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h b/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h new file mode 100644 index 00000000..883e38b7 --- /dev/null +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingFactories.h @@ -0,0 +1,42 @@ +#pragma once + +#include "CoreMinimal.h" +#include "EditorSubsystem.h" +#include "Factories/Factory.h" +#include "WingHandler.h" +#include "WingFactories.generated.h" + +UCLASS() +class UWingFactories : public UEditorSubsystem +{ + GENERATED_BODY() + +public: + struct Info + { + FString Name; + UClass* FactoryClass = nullptr; + TArray Config; + bool Disabled = false; + bool CanCreateNew() const; + }; + + virtual void Initialize(FSubsystemCollectionBase& Collection) override; + virtual void Deinitialize() override {} + + static const TArray& AllFactories(); + + // Create an asset on disk, using a factory. Returns the main object. + // If there are problems, prints error messages and returns nullptr. + static UObject *CreateAsset(const FString &Path, const FString &Factory, FWingJsonObject &Config); + +private: + TArray Registry; + + Info* Find(const FString& Name); + + void PopulateRegistry(); + void DisableFactory(const TCHAR* Name); + static bool PreCheck(UFactory *Factory, FName Name, const FString &Path); + static FString DeriveFactoryName(UClass* FactoryClass); +}; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingPackageMaker.h b/Plugins/UEWingman/Source/UEWingman/Public/WingPackageMaker.h index 77037ba1..901e8947 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingPackageMaker.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingPackageMaker.h @@ -12,43 +12,13 @@ class WingPackageMaker { public: - WingPackageMaker(const FString& InFullPath) - : FullPath(InFullPath) - { - // Path must start with /Game. - if (!FullPath.StartsWith(TEXT("/Game"))) - { - UWingServer::Printf(TEXT("ERROR: Package path '%s' must start with '/Game'\n"), *FullPath); - bError = true; - return; - } - - // Check for an existing asset at this path. - if (FindObject(nullptr, *FullPath)) - { - UWingServer::Printf(TEXT("ERROR: An asset already exists at '%s'\n"), *FullPath); - bError = true; - return; - } - } + WingPackageMaker(const FString& InFullPath); bool Ok() const { return !bError; } - - bool Make() - { - if (bError) return false; - Pkg = CreatePackage(*FullPath); - if (!Pkg) - { - UWingServer::Printf(TEXT("ERROR: Failed to create package at '%s'\n"), *FullPath); - bError = true; - return false; - } - return true; - } - + bool Make(); UPackage* Package() const { return Pkg; } - FString Name() const { return FPackageName::GetShortName(FullPath); } + FString GetName() const { return FPackageName::GetShortName(FullPath); } + FName GetFName() const { return FName(GetName()); } template AssetClass* CreateAsset() @@ -72,4 +42,6 @@ private: FString FullPath; UPackage* Pkg = nullptr; bool bError = false; + + static bool CheckNewAssetPath(const FString& Path); }; diff --git a/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h b/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h index a98ab12e..d302a9c4 100644 --- a/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h +++ b/Plugins/UEWingman/Source/UEWingman/Public/WingProperty.h @@ -59,6 +59,14 @@ struct FWingProperty static TArray GetAll(UObject* Object, EPropertyFlags Flags); static TArray GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags); + // Get the names of the properties in the specified class. + // + static TArray GetNames(UStruct *StructType, EPropertyFlags Flags); + + // Get the named properties. + // + static TArray GetNamed(UStruct* StructType, void* Container, const TArray &Names); + // Functions to find items by name in an array of properties. // static TArray FindAllSubstring(const TArray& Props, const FString& Substring); @@ -76,4 +84,5 @@ private: static TArray GetDetailsGeneral(UObject* Obj, EPropertyFlags Flags, bool Mutable); void PrintExpectsReceived(const TCHAR *Type); static void Collect(UStruct* Struct, void* Container, TArray &Props, EPropertyFlags Flags); + bool CheckImportTextResult(const FString &Value); };