Working on Asset_Create

This commit is contained in:
2026-04-01 05:10:00 -04:00
parent 89815bcd13
commit efb1c94144
13 changed files with 453 additions and 58 deletions

View File

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

View File

@@ -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<UWingFactories::Info>& 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);
}
}
};

View File

@@ -75,7 +75,7 @@ public:
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
ParentClassObj,
Maker.Package(),
FName(*Maker.Name()),
Maker.GetFName(),
BlueprintType,
UBlueprint::StaticClass(),
UBlueprintGeneratedClass::StaticClass()

View File

@@ -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::Info>& UWingFactories::AllFactories()
{
return GEditor->GetEditorSubsystem<UWingFactories>()->Registry;
}
bool UWingFactories::Info::CanCreateNew() const
{
return FactoryClass->GetDefaultObject<UFactory>()->CanCreateNew();
}
void UWingFactories::PopulateRegistry()
{
TArray<UClass*> 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<UWingFactories>();
// 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<UFactory>(GetTransientPackage(), FactoryInfo->FactoryClass);
// Get the editable properties
TArray<FWingProperty> Props =
FWingProperty::GetNamed(Factory->GetClass(), Factory, FactoryInfo->Config);
// if there is no config table, make a blank config table.
TSharedPtr<FJsonObject> ConfigJson = Config.Json;
if (ConfigJson == nullptr) ConfigJson = MakeShared<FJsonObject>();
// 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<UFactory>()->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<UBlueprintFactory>(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<UEnumFactory>(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;
}

View File

@@ -60,11 +60,6 @@ void WingManual::PrintHandlerHelp(UClass* HandlerClass)
void WingManual::PrintManual(TSet<Section> 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<Section> 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<Section> 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"
));
}

View File

@@ -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<UPackage>(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;
}

View File

@@ -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<uint8>(Container);
if (FObjectPropertyBase *OProp = CastField<FObjectPropertyBase>(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<FJsonValue> &JsonValue)
{
if (JsonValue->Type == EJson::String)
@@ -235,6 +253,41 @@ void FWingProperty::Collect(UStruct* StructType, void* Container, TArray<FWingPr
}
}
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;
Collect(StructType, Container, Result, Flags);
return Result;
}
TArray<FWingProperty> FWingProperty::GetNamed(UStruct* StructType, void* Container, const TArray<FName> &Names)
{
TArray<FWingProperty> Result;
for (FName Name : Names)
{
FProperty *Prop = StructType->FindPropertyByName(Name);
if (Prop != nullptr) Result.Emplace(Prop, Container);
}
return Result;
}
TArray<FName> FWingProperty::GetNames(UStruct *StructType, EPropertyFlags Flags)
{
TArray<FName> Result;
for (TFieldIterator<FProperty> It(StructType); It; ++It)
{
if (Flags != 0 && !It->HasAnyPropertyFlags(Flags)) continue;
Result.Add(It->GetFName());
}
return Result;
}
void FWingProperty::Remove(TArray<FWingProperty>& Props, const FString& Name)
{
Props.RemoveAll([&](const FWingProperty& P) { return P.Prop->GetName() == Name; });
@@ -312,17 +365,6 @@ TArray<FWingProperty> FWingProperty::GetDetailsGeneral(UObject* Obj, EPropertyFl
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;
Collect(StructType, Container, Result, Flags);
return Result;
}
TArray<FWingProperty> FWingProperty::FindAllSubstring(const TArray<FWingProperty>& Props, const FString& Substring)
{

View File

@@ -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<FName> Config;
bool Disabled = false;
bool CanCreateNew() const;
};
virtual void Initialize(FSubsystemCollectionBase& Collection) override;
virtual void Deinitialize() override {}
static const TArray<Info>& 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<Info> 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);
};

View File

@@ -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<UPackage>(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<typename AssetClass, typename FactoryClass>
AssetClass* CreateAsset()
@@ -72,4 +42,6 @@ private:
FString FullPath;
UPackage* Pkg = nullptr;
bool bError = false;
static bool CheckNewAssetPath(const FString& Path);
};

View File

@@ -59,6 +59,14 @@ struct FWingProperty
static TArray<FWingProperty> GetAll(UObject* Object, EPropertyFlags Flags);
static TArray<FWingProperty> GetAll(UStruct* StructType, void* Container, EPropertyFlags Flags);
// Get the names of the properties in the specified class.
//
static TArray<FName> GetNames(UStruct *StructType, EPropertyFlags Flags);
// Get the named properties.
//
static TArray<FWingProperty> GetNamed(UStruct* StructType, void* Container, const TArray<FName> &Names);
// Functions to find items by name in an array of properties.
//
static TArray<FWingProperty> FindAllSubstring(const TArray<FWingProperty>& Props, const FString& Substring);
@@ -76,4 +84,5 @@ 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);
bool CheckImportTextResult(const FString &Value);
};