Overhaul asset creation handlers.

This commit is contained in:
2026-03-12 18:31:55 -04:00
parent 4c6bdae2c2
commit c6f7a89ccd
10 changed files with 183 additions and 225 deletions

View File

@@ -4,6 +4,7 @@
#include "MCPHandler.h" #include "MCPHandler.h"
#include "MCPAssetFinder.h" #include "MCPAssetFinder.h"
#include "MCPUtils.h" #include "MCPUtils.h"
#include "MCPPackageMaker.h"
#include "Animation/AnimBlueprint.h" #include "Animation/AnimBlueprint.h"
#include "Animation/AnimBlueprintGeneratedClass.h" #include "Animation/AnimBlueprintGeneratedClass.h"
#include "Animation/AnimInstance.h" #include "Animation/AnimInstance.h"
@@ -22,11 +23,8 @@ class UMCP_AnimBlueprint_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Name for the new Animation Blueprint asset")) UPROPERTY(meta=(Description="Full asset path for the new Animation Blueprint (e.g. '/Game/AnimBP/ABP_Character')"))
FString Name; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use")) UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
FString Skeleton; FString Skeleton;
@@ -43,19 +41,8 @@ public:
{ {
MCPErrorCallback CB(Result); MCPErrorCallback CB(Result);
if (Name.IsEmpty() || PackagePath.IsEmpty() || Skeleton.IsEmpty()) MCPPackageMaker Maker(AssetPath, CB);
{ if (!Maker.Ok()) return;
return CB.SetError(TEXT("Missing required fields: name, packagePath, skeleton"));
}
if (!PackagePath.StartsWith(TEXT("/Game")))
{
return CB.SetError(TEXT("packagePath must start with '/Game'"));
}
// Check if asset already exists
MCPAssets<UBlueprint> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(CB).EAny().Info()) return;
// Resolve skeleton // Resolve skeleton
MCPAssets<USkeleton> SkeletonAssets; MCPAssets<USkeleton> SkeletonAssets;
@@ -82,20 +69,14 @@ public:
ParentClassObj = Found; ParentClassObj = Found;
} }
// Create the package // Create the package and Animation Blueprint
FString FullPackagePath = PackagePath / Name; if (!Maker.Make()) return;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return CB.SetError(FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
}
// Create the Animation Blueprint
UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>( UAnimBlueprint* NewAnimBP = CastChecked<UAnimBlueprint>(
FKismetEditorUtilities::CreateBlueprint( FKismetEditorUtilities::CreateBlueprint(
ParentClassObj, ParentClassObj,
Package, Maker.Package(),
FName(*Name), FName(*Maker.Name()),
BPTYPE_Normal, BPTYPE_Normal,
UAnimBlueprint::StaticClass(), UAnimBlueprint::StaticClass(),
UAnimBlueprintGeneratedClass::StaticClass() UAnimBlueprintGeneratedClass::StaticClass()
@@ -113,7 +94,7 @@ public:
FKismetEditorUtilities::CompileBlueprint(NewAnimBP); FKismetEditorUtilities::CompileBlueprint(NewAnimBP);
bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP); bool bSaved = MCPUtils::SaveBlueprintPackage(NewAnimBP);
Result.Appendf(TEXT("Created: %s\n"), *FullPackagePath); Result.Appendf(TEXT("Created: %s\n"), *AssetPath);
Result.Appendf(TEXT("ParentClass: %s\n"), *MCPUtils::FormatName(ParentClassObj)); Result.Appendf(TEXT("ParentClass: %s\n"), *MCPUtils::FormatName(ParentClassObj));
Result.Appendf(TEXT("Saved: %s\n"), bSaved ? TEXT("true") : TEXT("false")); Result.Appendf(TEXT("Saved: %s\n"), bSaved ? TEXT("true") : TEXT("false"));

View File

@@ -4,6 +4,7 @@
#include "MCPHandler.h" #include "MCPHandler.h"
#include "MCPAssetFinder.h" #include "MCPAssetFinder.h"
#include "MCPUtils.h" #include "MCPUtils.h"
#include "MCPPackageMaker.h"
#include "Animation/Skeleton.h" #include "Animation/Skeleton.h"
#include "Animation/BlendSpace.h" #include "Animation/BlendSpace.h"
#include "BlendSpace_Create.generated.h" #include "BlendSpace_Create.generated.h"
@@ -19,11 +20,8 @@ class UMCP_BlendSpace_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Name for the new Blend Space asset")) UPROPERTY(meta=(Description="Full asset path for the new Blend Space (e.g. '/Game/BlendSpaces/BS_Locomotion')"))
FString Name; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Name or path of the skeleton asset to use")) UPROPERTY(meta=(Description="Name or path of the skeleton asset to use"))
FString Skeleton; FString Skeleton;
@@ -35,32 +33,19 @@ public:
virtual void Handle(FStringBuilderBase& Result) override virtual void Handle(FStringBuilderBase& Result) override
{ {
if (!PackagePath.StartsWith(TEXT("/Game"))) MCPErrorCallback CB(Result);
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if an asset with this name already exists. MCPPackageMaker Maker(AssetPath, CB);
MCPAssets<UBlendSpace> ExistCheck; if (!Maker.Ok()) return;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Resolve skeleton. // Resolve skeleton.
MCPAssets<USkeleton> SkeletonAssets; MCPAssets<USkeleton> SkeletonAssets;
if (!SkeletonAssets.Exact(Skeleton).Errors(Result).ENone().ETwo().Load()) return; if (!SkeletonAssets.Exact(Skeleton).Errors(CB).ENone().ETwo().Load()) return;
USkeleton* SkeletonObj = SkeletonAssets.Object(); USkeleton* SkeletonObj = SkeletonAssets.Object();
// Create the package. // Create the package and Blend Space.
FString FullPackagePath = PackagePath / Name; if (!Maker.Make()) return;
UPackage* Package = CreatePackage(*FullPackagePath); UBlendSpace* NewBS = NewObject<UBlendSpace>(Maker.Package(), FName(*Maker.Name()), RF_Public | RF_Standalone);
if (!Package)
{
Result.Appendf(TEXT("ERROR: Failed to create package at '%s'\n"), *FullPackagePath);
return;
}
// Create the Blend Space.
UBlendSpace* NewBS = NewObject<UBlendSpace>(Package, FName(*Name), RF_Public | RF_Standalone);
if (!NewBS) if (!NewBS)
{ {
Result.Append(TEXT("ERROR: Failed to create Blend Space object\n")); Result.Append(TEXT("ERROR: Failed to create Blend Space object\n"));

View File

@@ -2,8 +2,8 @@
#include "CoreMinimal.h" #include "CoreMinimal.h"
#include "MCPHandler.h" #include "MCPHandler.h"
#include "MCPAssetFinder.h"
#include "MCPUtils.h" #include "MCPUtils.h"
#include "MCPPackageMaker.h"
#include "Engine/Blueprint.h" #include "Engine/Blueprint.h"
#include "Kismet2/KismetEditorUtilities.h" #include "Kismet2/KismetEditorUtilities.h"
#include "Blueprint_Create.generated.h" #include "Blueprint_Create.generated.h"
@@ -19,11 +19,8 @@ class UMCP_Blueprint_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="New Blueprint asset name")) UPROPERTY(meta=(Description="Full asset path for the new Blueprint (e.g. '/Game/Blueprints/BP_MyActor')"))
FString Blueprint; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)")) UPROPERTY(meta=(Description="Parent class name (C++ class name or Blueprint name)"))
FString ParentClass; FString ParentClass;
@@ -40,15 +37,8 @@ public:
{ {
MCPErrorCallback Error(Result); MCPErrorCallback Error(Result);
// Validate packagePath starts with /Game MCPPackageMaker Maker(AssetPath, Error);
if (!PackagePath.StartsWith(TEXT("/Game"))) if (!Maker.Ok()) return;
{
return Error.SetError(TEXT("PackagePath must start with '/Game'"));
}
// Check if asset already exists
MCPAssets<UBlueprint> ExistCheck;
if (!ExistCheck.Exact(Blueprint).Errors(Error).EAny().Info()) return;
// Resolve parent class — try C++ class first, then Blueprint asset // Resolve parent class — try C++ class first, then Blueprint asset
UClass* ParentClassObj = MCPUtils::FindClassByName(ParentClass); UClass* ParentClassObj = MCPUtils::FindClassByName(ParentClass);
@@ -81,19 +71,13 @@ public:
ParentClassObj = UInterface::StaticClass(); ParentClassObj = UInterface::StaticClass();
} }
// Create the package // Create the package and Blueprint
FString FullPackagePath = PackagePath / Blueprint; if (!Maker.Make()) return;
UPackage* Package = CreatePackage(*FullPackagePath);
if (!Package)
{
return Error.SetError(FString::Printf(TEXT("Failed to create package at '%s'"), *FullPackagePath));
}
// Create the Blueprint
UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint( UBlueprint* NewBP = FKismetEditorUtilities::CreateBlueprint(
ParentClassObj, ParentClassObj,
Package, Maker.Package(),
FName(*Blueprint), FName(*Maker.Name()),
BlueprintTypeEnum, BlueprintTypeEnum,
UBlueprint::StaticClass(), UBlueprint::StaticClass(),
UBlueprintGeneratedClass::StaticClass() UBlueprintGeneratedClass::StaticClass()

View File

@@ -6,9 +6,8 @@
#include "MCPUtils.h" #include "MCPUtils.h"
#include "Engine/UserDefinedEnum.h" #include "Engine/UserDefinedEnum.h"
#include "Kismet2/EnumEditorUtils.h" #include "Kismet2/EnumEditorUtils.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Factories/EnumFactory.h" #include "Factories/EnumFactory.h"
#include "MCPPackageMaker.h"
#include "Enum_Create.generated.h" #include "Enum_Create.generated.h"
@@ -35,12 +34,10 @@ public:
virtual void Handle(FStringBuilderBase& Result) override virtual void Handle(FStringBuilderBase& Result) override
{ {
FString PackagePath, AssetName; MCPErrorCallback Error(Result);
if (!MCPUtils::SplitAssetPath(AssetPath, PackagePath, AssetName))
{ MCPPackageMaker Maker(AssetPath, Error);
Result.Append(TEXT("ERROR: AssetPath must be a full path (e.g. '/Game/DataTypes/E_MyEnum')\n")); if (!Maker.Ok()) return;
return;
}
TArray<FString> EnumValues; TArray<FString> EnumValues;
for (const TSharedPtr<FJsonValue>& Val : Values.Array) for (const TSharedPtr<FJsonValue>& Val : Values.Array)
@@ -50,25 +47,12 @@ public:
} }
if (EnumValues.Num() == 0) if (EnumValues.Num() == 0)
{ {
Result.Append(TEXT("ERROR: Values must be a non-empty array of strings\n")); return Error.SetError(TEXT("Values must be a non-empty array of strings"));
return;
} }
// Check that no enum with this name already exists.
MCPAssets<UUserDefinedEnum> ExistCheck;
if (!ExistCheck.Exact(AssetName).Errors(Result).EAny().Info()) return;
// Create the enum using AssetTools. // Create the enum using AssetTools.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); UUserDefinedEnum* NewEnum = Maker.CreateAsset<UUserDefinedEnum, UEnumFactory>();
UEnumFactory* Factory = NewObject<UEnumFactory>(); if (!NewEnum) return;
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PackagePath, UUserDefinedEnum::StaticClass(), Factory);
UUserDefinedEnum* NewEnum = Cast<UUserDefinedEnum>(NewAsset);
if (!NewEnum)
{
Result.Append(TEXT("ERROR: Failed to create UserDefinedEnum asset\n"));
return;
}
// Add enum values — UUserDefinedEnum starts with a MAX value. // Add enum values — UUserDefinedEnum starts with a MAX value.
// We need to add entries before MAX. // We need to add entries before MAX.

View File

@@ -6,8 +6,7 @@
#include "MCPUtils.h" #include "MCPUtils.h"
#include "Materials/MaterialFunction.h" #include "Materials/MaterialFunction.h"
#include "Factories/MaterialFunctionFactoryNew.h" #include "Factories/MaterialFunctionFactoryNew.h"
#include "AssetToolsModule.h" #include "MCPPackageMaker.h"
#include "IAssetTools.h"
#include "MaterialFunction_Create.generated.h" #include "MaterialFunction_Create.generated.h"
@@ -21,11 +20,8 @@ class UMCP_MaterialFunction_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Name for the new material function asset")) UPROPERTY(meta=(Description="Full asset path for the new material function (e.g. '/Game/Materials/MF_MyFunc')"))
FString Name; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Optional, Description="Description for the material function")) UPROPERTY(meta=(Optional, Description="Description for the material function"))
FString Description; FString Description;
@@ -37,27 +33,14 @@ public:
virtual void Handle(FStringBuilderBase& Result) override virtual void Handle(FStringBuilderBase& Result) override
{ {
if (!PackagePath.StartsWith(TEXT("/Game"))) MCPErrorCallback Error(Result);
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if asset already exists. MCPPackageMaker Maker(AssetPath, Error);
MCPAssets<UMaterialFunction> ExistCheck; if (!Maker.Ok()) return;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Create via IAssetTools + factory. // Create via IAssetTools + factory.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); UMaterialFunction* MF = Maker.CreateAsset<UMaterialFunction, UMaterialFunctionFactoryNew>();
UMaterialFunctionFactoryNew* Factory = NewObject<UMaterialFunctionFactoryNew>(); if (!MF) return;
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialFunction::StaticClass(), Factory);
UMaterialFunction* MF = Cast<UMaterialFunction>(NewAsset);
if (!MF)
{
Result.Appendf(TEXT("ERROR: Failed to create Material Function '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Set optional description. // Set optional description.
if (!Description.IsEmpty()) if (!Description.IsEmpty())

View File

@@ -8,8 +8,7 @@
#include "Materials/MaterialInterface.h" #include "Materials/MaterialInterface.h"
#include "Materials/MaterialInstanceConstant.h" #include "Materials/MaterialInstanceConstant.h"
#include "Factories/MaterialInstanceConstantFactoryNew.h" #include "Factories/MaterialInstanceConstantFactoryNew.h"
#include "AssetToolsModule.h" #include "MCPPackageMaker.h"
#include "IAssetTools.h"
#include "MaterialInstance_Create.generated.h" #include "MaterialInstance_Create.generated.h"
@@ -23,11 +22,8 @@ class UMCP_MaterialInstance_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Name for the new Material Instance asset")) UPROPERTY(meta=(Description="Full asset path for the new Material Instance (e.g. '/Game/Materials/MI_GoldShiny')"))
FString Name; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)")) UPROPERTY(meta=(Description="Parent material name or path (Material or Material Instance)"))
FString ParentMaterial; FString ParentMaterial;
@@ -39,15 +35,10 @@ public:
virtual void Handle(FStringBuilderBase& Result) override virtual void Handle(FStringBuilderBase& Result) override
{ {
if (!PackagePath.StartsWith(TEXT("/Game"))) MCPErrorCallback Error(Result);
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if asset already exists. MCPPackageMaker Maker(AssetPath, Error);
MCPAssets<UMaterialInstanceConstant> ExistCheck; if (!Maker.Ok()) return;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Load parent material -- try as Material first, then as Material Instance. // Load parent material -- try as Material first, then as Material Instance.
UMaterialInterface* ParentMaterialObj = nullptr; UMaterialInterface* ParentMaterialObj = nullptr;
@@ -79,16 +70,8 @@ public:
} }
// Create via factory + AssetTools. // Create via factory + AssetTools.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); UMaterialInstanceConstant* MI = Maker.CreateAsset<UMaterialInstanceConstant, UMaterialInstanceConstantFactoryNew>();
UMaterialInstanceConstantFactoryNew* Factory = NewObject<UMaterialInstanceConstantFactoryNew>(); if (!MI) return;
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterialInstanceConstant::StaticClass(), Factory);
UMaterialInstanceConstant* MI = Cast<UMaterialInstanceConstant>(NewAsset);
if (!MI)
{
Result.Appendf(TEXT("ERROR: Failed to create Material Instance '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Set parent. // Set parent.
TArray<UObject*> Chain = { MI }; TArray<UObject*> Chain = { MI };

View File

@@ -7,8 +7,7 @@
#include "Materials/Material.h" #include "Materials/Material.h"
#include "MaterialDomain.h" #include "MaterialDomain.h"
#include "Factories/MaterialFactoryNew.h" #include "Factories/MaterialFactoryNew.h"
#include "AssetToolsModule.h" #include "MCPPackageMaker.h"
#include "IAssetTools.h"
#include "Material_Create.generated.h" #include "Material_Create.generated.h"
@@ -22,11 +21,8 @@ class UMCP_Material_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Name for the new material asset")) UPROPERTY(meta=(Description="Full asset path for the new material (e.g. '/Game/Materials/M_Gold')"))
FString Name; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI")) UPROPERTY(meta=(Optional, Description="Material domain: Surface, DeferredDecal, LightFunction, Volume, PostProcess, UI"))
FString Domain; FString Domain;
@@ -44,42 +40,29 @@ public:
virtual void Handle(FStringBuilderBase& Result) override virtual void Handle(FStringBuilderBase& Result) override
{ {
if (!PackagePath.StartsWith(TEXT("/Game"))) MCPErrorCallback Error(Result);
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n")); MCPPackageMaker Maker(AssetPath, Error);
return; if (!Maker.Ok()) return;
}
// Parse optional enum properties before creating the asset. // Parse optional enum properties before creating the asset.
EMaterialDomain ParsedDomain = MD_Surface; EMaterialDomain ParsedDomain = MD_Surface;
if (!Domain.IsEmpty()) if (!Domain.IsEmpty())
{ {
if (!MCPUtils::StringToEnum(Domain, ParsedDomain, Result, TEXT("MD_"))) if (!MCPUtils::StringToEnum(Domain, ParsedDomain, Error, TEXT("MD_")))
return; return;
} }
EBlendMode ParsedBlendMode = BLEND_Opaque; EBlendMode ParsedBlendMode = BLEND_Opaque;
if (!BlendMode.IsEmpty()) if (!BlendMode.IsEmpty())
{ {
if (!MCPUtils::StringToEnum(BlendMode, ParsedBlendMode, Result, TEXT("BLEND_"))) if (!MCPUtils::StringToEnum(BlendMode, ParsedBlendMode, Error, TEXT("BLEND_")))
return; return;
} }
// Check if an asset with this name already exists.
MCPAssets<UMaterial> ExistCheck;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Create via IAssetTools + factory. // Create via IAssetTools + factory.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); UMaterial* MaterialObj = Maker.CreateAsset<UMaterial, UMaterialFactoryNew>();
UMaterialFactoryNew* Factory = NewObject<UMaterialFactoryNew>(); if (!MaterialObj) return;
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UMaterial::StaticClass(), Factory);
UMaterial* MaterialObj = Cast<UMaterial>(NewAsset);
if (!MaterialObj)
{
Result.Appendf(TEXT("ERROR: Failed to create Material '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Apply optional properties. // Apply optional properties.
TArray<UObject*> Chain = { MaterialObj }; TArray<UObject*> Chain = { MaterialObj };

View File

@@ -44,30 +44,45 @@ public:
TArray<FProperty*> Props = MCPUtils::SearchProperties(Template, Query, CPF_Edit, Local); TArray<FProperty*> Props = MCPUtils::SearchProperties(Template, Query, CPF_Edit, Local);
UStruct* CurrentOwner = nullptr; // Group properties by category.
TMap<FString, TArray<FProperty*>> ByCategory;
for (FProperty* Prop : Props) for (FProperty* Prop : Props)
{ {
FString PropName = MCPUtils::FormatName(Prop); FString Category = Prop->HasMetaData(TEXT("Category")) ? Prop->GetMetaData(TEXT("Category")) : FString();
ByCategory.FindOrAdd(Category).Add(Prop);
}
// Print section heading when the owning class changes. // Sort category names, putting empty category last.
UStruct* Owner = Prop->GetOwnerStruct(); TArray<FString> Categories;
if (Owner != CurrentOwner) 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())
Result.Append(TEXT("\nUncategorized:\n"));
else
Result.Appendf(TEXT("\n%s:\n"), *Category);
for (FProperty* Prop : ByCategory[Category])
{ {
CurrentOwner = Owner; FString PropName = MCPUtils::FormatName(Prop);
Result.Appendf(TEXT("\nFrom %s:\n\n"), *MCPUtils::FormatName(Owner)); FString ValueStr = MCPUtils::GetPropertyValueText(Template, Prop);
if (Truncate && (ValueStr.Len() > 80))
ValueStr = ValueStr.Left(80) + TEXT("...");
bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst);
Result.Appendf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("editable") : TEXT("readonly"),
*MCPUtils::FormatPropertyType(Prop),
*PropName,
*ValueStr);
} }
FString ValueStr = MCPUtils::GetPropertyValueText(Template, Prop);
if (Truncate && (ValueStr.Len() > 80))
ValueStr = ValueStr.Left(80) + TEXT("...");
bool bEditable = !Prop->HasAnyPropertyFlags(CPF_EditConst);
Result.Appendf(TEXT(" %s %s %s = %s\n"),
bEditable ? TEXT("editable") : TEXT("readonly"),
*MCPUtils::FormatPropertyType(Prop),
*PropName,
*ValueStr);
} }
if (Props.IsEmpty()) if (Props.IsEmpty())

View File

@@ -7,9 +7,8 @@
#include "StructUtils/UserDefinedStruct.h" #include "StructUtils/UserDefinedStruct.h"
#include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/BlueprintEditorUtils.h"
#include "UserDefinedStructure/UserDefinedStructEditorData.h" #include "UserDefinedStructure/UserDefinedStructEditorData.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "Factories/StructureFactory.h" #include "Factories/StructureFactory.h"
#include "MCPPackageMaker.h"
#include "Struct_Create.generated.h" #include "Struct_Create.generated.h"
@@ -35,11 +34,8 @@ class UMCP_Struct_Create : public UObject, public IMCPHandler
GENERATED_BODY() GENERATED_BODY()
public: public:
UPROPERTY(meta=(Description="Name for the new struct asset")) UPROPERTY(meta=(Description="Full asset path for the new struct (e.g. '/Game/DataTypes/S_MyStruct')"))
FString Name; FString AssetPath;
UPROPERTY(meta=(Description="Package path where the asset will be created (must start with /Game)"))
FString PackagePath;
UPROPERTY(meta=(Optional, Description="Array of initial properties, each with 'name' and 'type' fields")) UPROPERTY(meta=(Optional, Description="Array of initial properties, each with 'name' and 'type' fields"))
FMCPJsonArray Properties; FMCPJsonArray Properties;
@@ -51,27 +47,14 @@ public:
virtual void Handle(FStringBuilderBase& Result) override virtual void Handle(FStringBuilderBase& Result) override
{ {
if (!PackagePath.StartsWith(TEXT("/Game"))) MCPErrorCallback Error(Result);
{
Result.Append(TEXT("ERROR: PackagePath must start with '/Game'\n"));
return;
}
// Check if an asset with this name already exists. MCPPackageMaker Maker(AssetPath, Error);
MCPAssets<UUserDefinedStruct> ExistCheck; if (!Maker.Ok()) return;
if (!ExistCheck.Exact(Name).Errors(Result).EAny().Info()) return;
// Create the struct using the AssetTools factory. // Create the struct using the AssetTools factory.
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get(); UUserDefinedStruct* NewStruct = Maker.CreateAsset<UUserDefinedStruct, UStructureFactory>();
UStructureFactory* Factory = NewObject<UStructureFactory>(); if (!NewStruct) return;
UObject* NewAsset = AssetTools.CreateAsset(Name, PackagePath, UUserDefinedStruct::StaticClass(), Factory);
UUserDefinedStruct* NewStruct = Cast<UUserDefinedStruct>(NewAsset);
if (!NewStruct)
{
Result.Appendf(TEXT("ERROR: Failed to create struct '%s' in '%s'\n"), *Name, *PackagePath);
return;
}
// Add properties if specified. // Add properties if specified.
int32 PropsAdded = 0; int32 PropsAdded = 0;

View File

@@ -0,0 +1,77 @@
#pragma once
#include "CoreMinimal.h"
#include "MCPUtils.h"
#include "UObject/Package.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
// Helper for creating new asset packages. Validates the path and checks for
// conflicting assets in the constructor. Call Ok() to check, then Make() to
// actually create the UPackage.
class MCPPackageMaker
{
public:
MCPPackageMaker(const FString& InFullPath, MCPErrorCallback InError)
: FullPath(InFullPath)
, Error(InError)
{
// Path must start with /Game.
if (!FullPath.StartsWith(TEXT("/Game")))
{
Error.SetError(FString::Printf(TEXT("Package path '%s' must start with '/Game'"), *FullPath));
bError = true;
return;
}
// Check for an existing asset at this path.
if (FindObject<UPackage>(nullptr, *FullPath))
{
Error.SetError(FString::Printf(TEXT("An asset already exists at '%s'"), *FullPath));
bError = true;
return;
}
}
bool Ok() const { return !bError; }
bool Make()
{
if (bError) return false;
Pkg = CreatePackage(*FullPath);
if (!Pkg)
{
Error.SetError(FString::Printf(TEXT("Failed to create package at '%s'"), *FullPath));
bError = true;
return false;
}
return true;
}
UPackage* Package() const { return Pkg; }
FString Name() const { return FPackageName::GetShortName(FullPath); }
template<typename AssetClass, typename FactoryClass>
AssetClass* CreateAsset()
{
if (bError) return nullptr;
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
FactoryClass* Factory = NewObject<FactoryClass>();
FString PkgPath = FPackageName::GetLongPackagePath(FullPath);
FString AssetName = FPackageName::GetShortName(FullPath);
UObject* NewAsset = AssetTools.CreateAsset(AssetName, PkgPath, AssetClass::StaticClass(), Factory);
AssetClass* Result = Cast<AssetClass>(NewAsset);
if (!Result)
{
Error.SetError(FString::Printf(TEXT("Failed to create asset at '%s'"), *FullPath));
bError = true;
}
return Result;
}
private:
FString FullPath;
MCPErrorCallback Error;
UPackage* Pkg = nullptr;
bool bError = false;
};