Add single asset export functionality and enhance README with usage details

This commit is contained in:
2025-10-23 21:02:50 +09:00
parent e988c90e2e
commit 4671fcc0bb
3 changed files with 674 additions and 88 deletions

View File

@ -196,6 +196,202 @@ void FAssetExporterToJSON::ExportAssetsToJSON()
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message));
}
void FAssetExporterToJSON::ExportSingleAssetToJSON(const FAssetData& AssetData)
{
UE_LOG(LogAssetExporter, Log, TEXT("Exporting single asset: %s"), *AssetData.GetObjectPathString());
UObject* Asset = AssetData.GetAsset();
if (!Asset)
{
UE_LOG(LogAssetExporter, Error, TEXT("Failed to load asset: %s"), *AssetData.GetObjectPathString());
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Failed to load asset")));
return;
}
// Determine asset type and export accordingly
TArray<TSharedPtr<FJsonValue>> JsonArray;
FString AssetTypeName;
if (UDataTable* DataTable = Cast<UDataTable>(Asset))
{
AssetTypeName = TEXT("DataTable");
TSharedPtr<FJsonObject> DataTableJson = MakeShareable(new FJsonObject);
DataTableJson->SetStringField(TEXT("AssetName"), DataTable->GetName());
DataTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
DataTableJson->SetStringField(TEXT("RowStructure"), DataTable->GetRowStruct() ? DataTable->GetRowStruct()->GetName() : TEXT("None"));
// Export rows
TArray<TSharedPtr<FJsonValue>> RowsArray;
const UScriptStruct* RowStruct = DataTable->GetRowStruct();
const TMap<FName, uint8*>& RowMap = DataTable->GetRowMap();
for (const TPair<FName, uint8*>& Row : RowMap)
{
TSharedPtr<FJsonObject> RowJson = MakeShareable(new FJsonObject);
RowJson->SetStringField(TEXT("RowName"), Row.Key.ToString());
if (RowStruct)
{
FString RowDataString;
if (FJsonObjectConverter::UStructToJsonObjectString(RowStruct, Row.Value, RowDataString, 0, 0))
{
TSharedPtr<FJsonObject> RowDataJson;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(RowDataString);
if (FJsonSerializer::Deserialize(Reader, RowDataJson) && RowDataJson.IsValid())
{
RowJson->SetObjectField(TEXT("Data"), RowDataJson);
}
}
}
RowsArray.Add(MakeShareable(new FJsonValueObject(RowJson)));
}
DataTableJson->SetArrayField(TEXT("Rows"), RowsArray);
JsonArray.Add(MakeShareable(new FJsonValueObject(DataTableJson)));
}
else if (UBlueprint* Blueprint = Cast<UBlueprint>(Asset))
{
AssetTypeName = TEXT("Blueprint");
TSharedPtr<FJsonObject> BlueprintJson = ExtractBlueprintDetails(Blueprint);
if (BlueprintJson.IsValid())
{
BlueprintJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
JsonArray.Add(MakeShareable(new FJsonValueObject(BlueprintJson)));
}
}
else if (UAnimMontage* AnimMontage = Cast<UAnimMontage>(Asset))
{
AssetTypeName = TEXT("AnimMontage");
TSharedPtr<FJsonObject> MontageJson = MakeShareable(new FJsonObject);
MontageJson->SetStringField(TEXT("AssetName"), AnimMontage->GetName());
MontageJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
MontageJson->SetNumberField(TEXT("SequenceLength"), static_cast<double>(AnimMontage->GetPlayLength()));
MontageJson->SetNumberField(TEXT("RateScale"), static_cast<double>(AnimMontage->RateScale));
// Export sections
TArray<TSharedPtr<FJsonValue>> SectionsArray;
for (const FCompositeSection& Section : AnimMontage->CompositeSections)
{
TSharedPtr<FJsonObject> SectionJson = MakeShareable(new FJsonObject);
SectionJson->SetStringField(TEXT("SectionName"), Section.SectionName.ToString());
SectionJson->SetNumberField(TEXT("StartTime"), static_cast<double>(Section.GetTime()));
SectionJson->SetStringField(TEXT("NextSectionName"), Section.NextSectionName.ToString());
SectionsArray.Add(MakeShareable(new FJsonValueObject(SectionJson)));
}
MontageJson->SetArrayField(TEXT("Sections"), SectionsArray);
// Note: Simplified export for single asset - full export includes SlotAnimTracks and Notifies
JsonArray.Add(MakeShareable(new FJsonValueObject(MontageJson)));
}
else if (UCurveTable* CurveTable = Cast<UCurveTable>(Asset))
{
AssetTypeName = TEXT("CurveTable");
TSharedPtr<FJsonObject> CurveTableJson = MakeShareable(new FJsonObject);
CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName());
CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
FString ModeStr = TEXT("Unknown");
switch (CurveTable->GetCurveTableMode())
{
case ECurveTableMode::Empty:
ModeStr = TEXT("Empty");
break;
case ECurveTableMode::SimpleCurves:
ModeStr = TEXT("SimpleCurves");
break;
case ECurveTableMode::RichCurves:
ModeStr = TEXT("RichCurves");
break;
}
CurveTableJson->SetStringField(TEXT("CurveTableMode"), ModeStr);
// Export curves (simplified for single asset)
TArray<TSharedPtr<FJsonValue>> CurvesArray;
if (CurveTable->GetCurveTableMode() == ECurveTableMode::RichCurves)
{
const TMap<FName, FRichCurve*>& RichCurveMap = CurveTable->GetRichCurveRowMap();
for (const TPair<FName, FRichCurve*>& Row : RichCurveMap)
{
TSharedPtr<FJsonObject> CurveJson = MakeShareable(new FJsonObject);
CurveJson->SetStringField(TEXT("CurveName"), Row.Key.ToString());
if (FRichCurve* Curve = Row.Value)
{
TArray<TSharedPtr<FJsonValue>> KeysArray;
const TArray<FRichCurveKey>& Keys = Curve->GetConstRefOfKeys();
for (const FRichCurveKey& Key : Keys)
{
TSharedPtr<FJsonObject> KeyJson = MakeShareable(new FJsonObject);
KeyJson->SetNumberField(TEXT("Time"), static_cast<double>(Key.Time));
KeyJson->SetNumberField(TEXT("Value"), static_cast<double>(Key.Value));
KeysArray.Add(MakeShareable(new FJsonValueObject(KeyJson)));
}
CurveJson->SetArrayField(TEXT("Keys"), KeysArray);
}
CurvesArray.Add(MakeShareable(new FJsonValueObject(CurveJson)));
}
}
CurveTableJson->SetArrayField(TEXT("Curves"), CurvesArray);
JsonArray.Add(MakeShareable(new FJsonValueObject(CurveTableJson)));
}
else
{
UE_LOG(LogAssetExporter, Warning, TEXT("Unsupported asset type: %s"), *Asset->GetClass()->GetName());
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(
TEXT("Unsupported asset type: %s\n\nSupported types: DataTable, Blueprint, AnimMontage, CurveTable"),
*Asset->GetClass()->GetName()
)));
return;
}
if (JsonArray.Num() == 0)
{
UE_LOG(LogAssetExporter, Warning, TEXT("No data to export"));
return;
}
// Create output directory
const UAssetExportSettings* Settings = GetDefault<UAssetExportSettings>();
FString OutputPath = FPaths::ProjectContentDir();
if (Settings && !Settings->OutputDirectory.Path.IsEmpty())
{
OutputPath = OutputPath / Settings->OutputDirectory.Path;
}
else
{
OutputPath = OutputPath / TEXT("Exports");
}
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*OutputPath))
{
PlatformFile.CreateDirectory(*OutputPath);
}
// Create timestamped filename
FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
FString SafeAssetName = Asset->GetName().Replace(TEXT(" "), TEXT("_"));
FString FileName = FString::Printf(TEXT("%s_%s_%s.json"), *AssetTypeName, *SafeAssetName, *Timestamp);
// Save JSON
if (SaveJsonToFile(JsonArray, FileName, OutputPath))
{
FString FilePath = OutputPath / FileName;
FString Message = FString::Printf(
TEXT("Asset exported successfully!\n\n")
TEXT("Asset: %s\n")
TEXT("Type: %s\n\n")
TEXT("Output file:\n%s"),
*Asset->GetName(),
*AssetTypeName,
*FilePath
);
UE_LOG(LogAssetExporter, Log, TEXT("Single asset export completed: %s"), *FilePath);
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message));
}
}
int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray<TSharedPtr<FJsonValue>>& OutJsonArray)
{
UE_LOG(LogAssetExporter, Log, TEXT("Exporting DataTables from: %s"), *FolderPath);
@ -210,13 +406,22 @@ int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray<T
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
UE_LOG(LogAssetExporter, Log, TEXT("Found %d DataTables to export"), AssetList.Num());
int32 Count = 0;
int32 TotalCount = AssetList.Num();
for (const FAssetData& AssetData : AssetList)
{
UDataTable* DataTable = Cast<UDataTable>(AssetData.GetAsset());
if (!DataTable)
continue;
// Progress logging every 10 assets or for first/last
if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1)
{
UE_LOG(LogAssetExporter, Log, TEXT(" Processing DataTable %d/%d: %s"), Count + 1, TotalCount, *DataTable->GetName());
}
TSharedPtr<FJsonObject> DataTableJson = MakeShareable(new FJsonObject);
DataTableJson->SetStringField(TEXT("AssetName"), DataTable->GetName());
DataTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
@ -275,13 +480,22 @@ int32 FAssetExporterToJSON::ExportBlueprints(const FString& FolderPath, TArray<T
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
UE_LOG(LogAssetExporter, Log, TEXT("Found %d Blueprints to export"), AssetList.Num());
int32 Count = 0;
int32 TotalCount = AssetList.Num();
for (const FAssetData& AssetData : AssetList)
{
UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
if (!Blueprint || !Blueprint->GeneratedClass)
continue;
// Progress logging every 10 assets or for first/last
if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1)
{
UE_LOG(LogAssetExporter, Log, TEXT(" Processing Blueprint %d/%d: %s"), Count + 1, TotalCount, *Blueprint->GetName());
}
TSharedPtr<FJsonObject> BlueprintJson = ExtractBlueprintDetails(Blueprint);
if (BlueprintJson.IsValid())
{
@ -340,26 +554,125 @@ TArray<TSharedPtr<FJsonValue>> FAssetExporterToJSON::ExtractBlueprintVariables(U
if (!DefaultObject)
return VariablesArray;
// Iterate through all properties
for (TFieldIterator<FProperty> PropIt(Blueprint->GeneratedClass, EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
// CRITICAL: Export Blueprint custom variables (NewVariables)
// This is the most important part for Blueprint analysis
for (const FBPVariableDescription& Variable : Blueprint->NewVariables)
{
TSharedPtr<FJsonObject> VarJson = MakeShareable(new FJsonObject);
VarJson->SetStringField(TEXT("Name"), Variable.VarName.ToString());
VarJson->SetStringField(TEXT("VarGuid"), Variable.VarGuid.ToString());
// Export variable type information
VarJson->SetStringField(TEXT("Category"), Variable.VarType.PinCategory.ToString());
if (Variable.VarType.PinSubCategoryObject.IsValid())
{
VarJson->SetStringField(TEXT("SubCategoryObject"), Variable.VarType.PinSubCategoryObject->GetName());
}
if (!Variable.VarType.PinSubCategory.IsNone())
{
VarJson->SetStringField(TEXT("SubCategory"), Variable.VarType.PinSubCategory.ToString());
}
// Container type (Array, Set, Map)
if (Variable.VarType.ContainerType != EPinContainerType::None)
{
FString ContainerTypeStr;
switch (Variable.VarType.ContainerType)
{
case EPinContainerType::Array:
ContainerTypeStr = TEXT("Array");
break;
case EPinContainerType::Set:
ContainerTypeStr = TEXT("Set");
break;
case EPinContainerType::Map:
ContainerTypeStr = TEXT("Map");
break;
}
VarJson->SetStringField(TEXT("ContainerType"), ContainerTypeStr);
}
// Export default value
VarJson->SetStringField(TEXT("DefaultValue"), Variable.DefaultValue);
// Export property flags
VarJson->SetBoolField(TEXT("IsEditable"), (Variable.PropertyFlags & CPF_Edit) != 0);
VarJson->SetBoolField(TEXT("IsBlueprintVisible"), (Variable.PropertyFlags & CPF_BlueprintVisible) != 0);
VarJson->SetBoolField(TEXT("IsBlueprintReadOnly"), (Variable.PropertyFlags & CPF_BlueprintReadOnly) != 0);
VarJson->SetBoolField(TEXT("IsExposedOnSpawn"), (Variable.PropertyFlags & CPF_ExposeOnSpawn) != 0);
VarJson->SetBoolField(TEXT("IsInstanceEditable"), (Variable.PropertyFlags & CPF_DisableEditOnInstance) == 0);
// Export category
if (!Variable.Category.IsEmpty())
{
VarJson->SetStringField(TEXT("CategoryName"), Variable.Category.ToString());
}
// Export replication
if (Variable.RepNotifyFunc != NAME_None)
{
VarJson->SetStringField(TEXT("RepNotifyFunc"), Variable.RepNotifyFunc.ToString());
}
// Export metadata
if (Variable.MetaDataArray.Num() > 0)
{
TSharedPtr<FJsonObject> MetaDataJson = MakeShareable(new FJsonObject);
for (const FBPVariableMetaDataEntry& MetaDataEntry : Variable.MetaDataArray)
{
MetaDataJson->SetStringField(MetaDataEntry.DataKey.ToString(), MetaDataEntry.DataValue);
}
if (MetaDataJson->Values.Num() > 0)
{
VarJson->SetObjectField(TEXT("MetaData"), MetaDataJson);
}
}
VarJson->SetStringField(TEXT("Source"), TEXT("Blueprint"));
VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson)));
}
// Extract parent class properties with Category = "WorldStalker"
// This captures important C++ properties like ActivationOrderGroup
for (TFieldIterator<FProperty> PropIt(Blueprint->GeneratedClass); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
if (!Property)
continue;
// Check if this property has Category metadata set to "WorldStalker"
const FString* CategoryMeta = Property->FindMetaData(TEXT("Category"));
if (!CategoryMeta || !CategoryMeta->Equals(TEXT("WorldStalker")))
continue;
// This is a WorldStalker category property - export it!
TSharedPtr<FJsonObject> VarJson = MakeShareable(new FJsonObject);
VarJson->SetStringField(TEXT("Name"), Property->GetName());
VarJson->SetStringField(TEXT("Type"), Property->GetCPPType());
// Get default value
// Get default value from CDO
FString DefaultValue;
const void* ValuePtr = Property->ContainerPtrToValuePtr<void>(DefaultObject);
Property->ExportTextItem_Direct(DefaultValue, ValuePtr, nullptr, nullptr, PPF_None);
VarJson->SetStringField(TEXT("DefaultValue"), DefaultValue);
// Additional metadata
// Property flags
VarJson->SetBoolField(TEXT("IsEditable"), Property->HasAnyPropertyFlags(CPF_Edit));
VarJson->SetBoolField(TEXT("IsBlueprintVisible"), Property->HasAnyPropertyFlags(CPF_BlueprintVisible));
VarJson->SetBoolField(TEXT("IsBlueprintReadOnly"), Property->HasAnyPropertyFlags(CPF_BlueprintReadOnly));
VarJson->SetBoolField(TEXT("IsEditDefaultsOnly"), Property->HasAnyPropertyFlags(CPF_DisableEditOnInstance));
// Category and source
VarJson->SetStringField(TEXT("CategoryName"), TEXT("WorldStalker"));
VarJson->SetStringField(TEXT("Source"), TEXT("C++ParentClass"));
// Get owning class name for clarity
if (Property->GetOwnerClass())
{
VarJson->SetStringField(TEXT("OwnerClass"), Property->GetOwnerClass()->GetName());
}
VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson)));
}
@ -598,12 +911,17 @@ int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
UE_LOG(LogAssetExporter, Log, TEXT("Found %d AnimMontages to export"), AssetList.Num());
// Track exported assets to avoid duplicates
TSet<FString> ExportedPaths;
int32 Count = 0;
int32 TotalCount = AssetList.Num();
int32 ProcessedCount = 0;
for (const FAssetData& AssetData : AssetList)
{
ProcessedCount++;
FString AssetPath = AssetData.GetObjectPathString();
// Skip if already exported
@ -617,6 +935,12 @@ int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray
if (!AnimMontage)
continue;
// Progress logging every 10 assets or for first/last
if (Count == 0 || Count % 10 == 0 || ProcessedCount == TotalCount)
{
UE_LOG(LogAssetExporter, Log, TEXT(" Processing AnimMontage %d/%d: %s"), ProcessedCount, TotalCount, *AnimMontage->GetName());
}
ExportedPaths.Add(AssetPath);
TSharedPtr<FJsonObject> MontageJson = MakeShareable(new FJsonObject);
@ -785,13 +1109,22 @@ int32 FAssetExporterToJSON::ExportCurveTables(const FString& FolderPath, TArray<
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
UE_LOG(LogAssetExporter, Log, TEXT("Found %d CurveTables to export"), AssetList.Num());
int32 Count = 0;
int32 TotalCount = AssetList.Num();
for (const FAssetData& AssetData : AssetList)
{
UCurveTable* CurveTable = Cast<UCurveTable>(AssetData.GetAsset());
if (!CurveTable)
continue;
// Progress logging every 10 assets or for first/last
if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1)
{
UE_LOG(LogAssetExporter, Log, TEXT(" Processing CurveTable %d/%d: %s"), Count + 1, TotalCount, *CurveTable->GetName());
}
TSharedPtr<FJsonObject> CurveTableJson = MakeShareable(new FJsonObject);
CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName());
CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());