Files
DS-Asset_Export_to_JSON/AssetExporterToJSON.cpp
jinilkim 3d9d6cc664 Add Asset Export to JSON feature for LLM combat balance analysis
Features:
- Export DataTable, Blueprint, AnimMontage, CurveTable to JSON
- INI-based configuration via UDeveloperSettings
- Comprehensive Blueprint EventGraph extraction
- Enhanced AnimMontage with CustomProperties from Notifies
- Project Settings integration (Edit -> Project Settings -> Plugins)
- Tools menu integration (Tools -> WorldStalker -> Export Assets to JSON)

Files added:
- AssetExportSettings.h/cpp - UDeveloperSettings configuration class
- AssetExporterToJSON.h/cpp - Core export implementation
- README.md - Comprehensive feature documentation
- MERGE_INSTRUCTIONS.md - Integration guide for existing files

Unreal Engine 5.5.4 compatible
2025-10-22 15:14:50 +09:00

918 lines
32 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetExporterToJSON.h"
#include "AssetExportSettings.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "Engine/DataTable.h"
#include "Engine/CurveTable.h"
#include "Engine/Blueprint.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimNotifies/AnimNotify.h"
#include "Animation/AnimNotifies/AnimNotifyState.h"
#include "Animation/AnimMetaData.h"
#include "Curves/RichCurve.h"
#include "Curves/SimpleCurve.h"
#include "JsonObjectConverter.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Misc/DateTime.h"
#include "HAL/PlatformFileManager.h"
#include "Misc/MessageDialog.h"
#include "Components/ActorComponent.h"
#include "Engine/SimpleConstructionScript.h"
#include "Engine/SCS_Node.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "EdGraph/EdGraph.h"
#include "EdGraph/EdGraphPin.h"
#include "K2Node_FunctionEntry.h"
#include "K2Node_FunctionResult.h"
DEFINE_LOG_CATEGORY_STATIC(LogAssetExporter, Log, All);
void FAssetExporterToJSON::ExportAssetsToJSON()
{
UE_LOG(LogAssetExporter, Log, TEXT("Starting Asset Export to JSON process..."));
// Get settings
const UAssetExportSettings* Settings = GetDefault<UAssetExportSettings>();
if (!Settings)
{
UE_LOG(LogAssetExporter, Error, TEXT("Failed to load Asset Export Settings"));
FMessageDialog::Open(EAppMsgType::Ok,
FText::FromString(TEXT("Failed to load export settings. Please check Project Settings -> Plugins -> Asset Export to JSON")));
return;
}
// Check if any paths are configured
if (Settings->ExportFolderPaths.Num() == 0)
{
UE_LOG(LogAssetExporter, Warning, TEXT("No export paths configured"));
FMessageDialog::Open(EAppMsgType::Ok,
FText::FromString(TEXT("No export paths configured.\n\nPlease add folder paths in:\nProject Settings -> Plugins -> Asset Export to JSON -> Export Paths")));
return;
}
// Convert FDirectoryPath to package paths
TArray<FString> PackagePaths;
for (const FDirectoryPath& DirPath : Settings->ExportFolderPaths)
{
FString Path = DirPath.Path;
// Convert to package path if not already
if (!Path.StartsWith(TEXT("/Game/")))
{
// Remove leading/trailing slashes
Path.TrimStartAndEndInline();
Path.RemoveFromStart(TEXT("/"));
Path.RemoveFromEnd(TEXT("/"));
// Add /Game/ prefix
Path = TEXT("/Game/") + Path;
}
PackagePaths.Add(Path);
UE_LOG(LogAssetExporter, Log, TEXT("Export path: %s"), *Path);
}
// Create output directory
FString OutputPath = FPaths::ProjectContentDir();
if (!Settings->OutputDirectory.Path.IsEmpty())
{
OutputPath = OutputPath / Settings->OutputDirectory.Path;
}
else
{
OutputPath = OutputPath / TEXT("Exports");
}
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
if (!PlatformFile.DirectoryExists(*OutputPath))
{
if (!PlatformFile.CreateDirectory(*OutputPath))
{
UE_LOG(LogAssetExporter, Error, TEXT("Failed to create output directory: %s"), *OutputPath);
FMessageDialog::Open(EAppMsgType::Ok,
FText::FromString(FString::Printf(TEXT("Failed to create output directory:\n%s"), *OutputPath)));
return;
}
}
// Add timestamp subfolder if enabled
if (Settings->bCreateTimestampedFolder)
{
FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
OutputPath = OutputPath / Timestamp;
if (!PlatformFile.CreateDirectory(*OutputPath))
{
UE_LOG(LogAssetExporter, Error, TEXT("Failed to create timestamped directory: %s"), *OutputPath);
return;
}
}
UE_LOG(LogAssetExporter, Log, TEXT("Output directory: %s"), *OutputPath);
// Export from all configured paths
TArray<TSharedPtr<FJsonValue>> DataTableArray;
TArray<TSharedPtr<FJsonValue>> BlueprintArray;
TArray<TSharedPtr<FJsonValue>> AnimMontageArray;
TArray<TSharedPtr<FJsonValue>> CurveTableArray;
int32 TotalExported = 0;
for (const FString& PackagePath : PackagePaths)
{
UE_LOG(LogAssetExporter, Log, TEXT("Processing folder: %s"), *PackagePath);
// Export each asset type from this folder based on settings
if (Settings->bExportDataTables)
{
TotalExported += ExportDataTables(PackagePath, DataTableArray);
}
if (Settings->bExportBlueprints)
{
TotalExported += ExportBlueprints(PackagePath, BlueprintArray);
}
if (Settings->bExportAnimMontages)
{
TotalExported += ExportAnimMontages(PackagePath, AnimMontageArray);
}
if (Settings->bExportCurveTables)
{
TotalExported += ExportCurveTables(PackagePath, CurveTableArray);
}
}
// Save all collected data
int32 DataTableCount = DataTableArray.Num();
int32 BlueprintCount = BlueprintArray.Num();
int32 AnimMontageCount = AnimMontageArray.Num();
int32 CurveTableCount = CurveTableArray.Num();
if (DataTableCount > 0)
{
SaveJsonToFile(DataTableArray, TEXT("DataTable.json"), OutputPath);
}
if (BlueprintCount > 0)
{
SaveJsonToFile(BlueprintArray, TEXT("Blueprint.json"), OutputPath);
}
if (AnimMontageCount > 0)
{
SaveJsonToFile(AnimMontageArray, TEXT("AnimMontage.json"), OutputPath);
}
if (CurveTableCount > 0)
{
SaveJsonToFile(CurveTableArray, TEXT("CurveTable.json"), OutputPath);
}
UE_LOG(LogAssetExporter, Log, TEXT("Export completed! Total assets exported: %d"), TotalExported);
UE_LOG(LogAssetExporter, Log, TEXT(" - DataTables: %d"), DataTableCount);
UE_LOG(LogAssetExporter, Log, TEXT(" - Blueprints: %d"), BlueprintCount);
UE_LOG(LogAssetExporter, Log, TEXT(" - AnimMontages: %d"), AnimMontageCount);
UE_LOG(LogAssetExporter, Log, TEXT(" - CurveTables: %d"), CurveTableCount);
// Show completion message
FString Message = FString::Printf(
TEXT("Export completed successfully!\n\n")
TEXT("Total assets exported: %d\n")
TEXT("- DataTables: %d\n")
TEXT("- Blueprints: %d\n")
TEXT("- AnimMontages: %d\n")
TEXT("- CurveTables: %d\n\n")
TEXT("Output directory:\n%s"),
TotalExported,
DataTableCount,
BlueprintCount,
AnimMontageCount,
CurveTableCount,
*OutputPath
);
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);
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetList;
FARFilter Filter;
Filter.PackagePaths.Add(FName(*FolderPath));
Filter.ClassPaths.Add(UDataTable::StaticClass()->GetClassPathName());
Filter.bRecursivePaths = true;
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
int32 Count = 0;
for (const FAssetData& AssetData : AssetList)
{
UDataTable* DataTable = Cast<UDataTable>(AssetData.GetAsset());
if (!DataTable)
continue;
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());
// Convert row data to JSON
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);
OutJsonArray.Add(MakeShareable(new FJsonValueObject(DataTableJson)));
Count++;
UE_LOG(LogAssetExporter, Verbose, TEXT(" Exported DataTable: %s (%d rows)"), *DataTable->GetName(), RowMap.Num());
}
UE_LOG(LogAssetExporter, Log, TEXT("Exported %d DataTables"), Count);
return Count;
}
int32 FAssetExporterToJSON::ExportBlueprints(const FString& FolderPath, TArray<TSharedPtr<FJsonValue>>& OutJsonArray)
{
UE_LOG(LogAssetExporter, Log, TEXT("Exporting Blueprints from: %s"), *FolderPath);
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetList;
FARFilter Filter;
Filter.PackagePaths.Add(FName(*FolderPath));
Filter.ClassPaths.Add(UBlueprint::StaticClass()->GetClassPathName());
Filter.bRecursivePaths = true;
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
int32 Count = 0;
for (const FAssetData& AssetData : AssetList)
{
UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
if (!Blueprint || !Blueprint->GeneratedClass)
continue;
TSharedPtr<FJsonObject> BlueprintJson = ExtractBlueprintDetails(Blueprint);
if (BlueprintJson.IsValid())
{
BlueprintJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
OutJsonArray.Add(MakeShareable(new FJsonValueObject(BlueprintJson)));
Count++;
UE_LOG(LogAssetExporter, Verbose, TEXT(" Exported Blueprint: %s"), *Blueprint->GetName());
}
}
UE_LOG(LogAssetExporter, Log, TEXT("Exported %d Blueprints"), Count);
return Count;
}
TSharedPtr<FJsonObject> FAssetExporterToJSON::ExtractBlueprintDetails(UBlueprint* Blueprint)
{
if (!Blueprint || !Blueprint->GeneratedClass)
return nullptr;
TSharedPtr<FJsonObject> BlueprintJson = MakeShareable(new FJsonObject);
BlueprintJson->SetStringField(TEXT("AssetName"), Blueprint->GetName());
// Parent class
UClass* ParentClass = Blueprint->GeneratedClass->GetSuperClass();
BlueprintJson->SetStringField(TEXT("ParentClass"), ParentClass ? ParentClass->GetName() : TEXT("None"));
// Extract variables
TArray<TSharedPtr<FJsonValue>> Variables = ExtractBlueprintVariables(Blueprint);
BlueprintJson->SetArrayField(TEXT("Variables"), Variables);
// Extract functions
TArray<TSharedPtr<FJsonValue>> Functions = ExtractBlueprintFunctions(Blueprint);
BlueprintJson->SetArrayField(TEXT("Functions"), Functions);
// Extract components
TArray<TSharedPtr<FJsonValue>> Components = ExtractBlueprintComponents(Blueprint);
BlueprintJson->SetArrayField(TEXT("Components"), Components);
// Extract event graphs
TArray<TSharedPtr<FJsonValue>> EventGraphs = ExtractBlueprintEventGraphs(Blueprint);
BlueprintJson->SetArrayField(TEXT("EventGraphs"), EventGraphs);
return BlueprintJson;
}
TArray<TSharedPtr<FJsonValue>> FAssetExporterToJSON::ExtractBlueprintVariables(UBlueprint* Blueprint)
{
TArray<TSharedPtr<FJsonValue>> VariablesArray;
if (!Blueprint || !Blueprint->GeneratedClass)
return VariablesArray;
UObject* DefaultObject = Blueprint->GeneratedClass->GetDefaultObject();
if (!DefaultObject)
return VariablesArray;
// Iterate through all properties
for (TFieldIterator<FProperty> PropIt(Blueprint->GeneratedClass, EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
if (!Property)
continue;
TSharedPtr<FJsonObject> VarJson = MakeShareable(new FJsonObject);
VarJson->SetStringField(TEXT("Name"), Property->GetName());
VarJson->SetStringField(TEXT("Type"), Property->GetCPPType());
// Get default value
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
VarJson->SetBoolField(TEXT("IsEditable"), Property->HasAnyPropertyFlags(CPF_Edit));
VarJson->SetBoolField(TEXT("IsBlueprintVisible"), Property->HasAnyPropertyFlags(CPF_BlueprintVisible));
VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson)));
}
return VariablesArray;
}
TArray<TSharedPtr<FJsonValue>> FAssetExporterToJSON::ExtractBlueprintFunctions(UBlueprint* Blueprint)
{
TArray<TSharedPtr<FJsonValue>> FunctionsArray;
if (!Blueprint)
return FunctionsArray;
// Iterate through all function graphs
for (UEdGraph* Graph : Blueprint->FunctionGraphs)
{
if (!Graph)
continue;
TSharedPtr<FJsonObject> FuncJson = MakeShareable(new FJsonObject);
FuncJson->SetStringField(TEXT("Name"), Graph->GetName());
TArray<TSharedPtr<FJsonValue>> InputsArray;
TArray<TSharedPtr<FJsonValue>> OutputsArray;
// Find entry and result nodes to extract parameters
for (UEdGraphNode* Node : Graph->Nodes)
{
if (UK2Node_FunctionEntry* EntryNode = Cast<UK2Node_FunctionEntry>(Node))
{
// Extract input parameters
for (UEdGraphPin* Pin : EntryNode->Pins)
{
if (Pin && Pin->Direction == EGPD_Output)
{
TSharedPtr<FJsonObject> ParamJson = MakeShareable(new FJsonObject);
ParamJson->SetStringField(TEXT("Name"), Pin->PinName.ToString());
ParamJson->SetStringField(TEXT("Type"), Pin->PinType.PinCategory.ToString());
InputsArray.Add(MakeShareable(new FJsonValueObject(ParamJson)));
}
}
}
else if (UK2Node_FunctionResult* ResultNode = Cast<UK2Node_FunctionResult>(Node))
{
// Extract output parameters
for (UEdGraphPin* Pin : ResultNode->Pins)
{
if (Pin && Pin->Direction == EGPD_Input)
{
TSharedPtr<FJsonObject> ParamJson = MakeShareable(new FJsonObject);
ParamJson->SetStringField(TEXT("Name"), Pin->PinName.ToString());
ParamJson->SetStringField(TEXT("Type"), Pin->PinType.PinCategory.ToString());
OutputsArray.Add(MakeShareable(new FJsonValueObject(ParamJson)));
}
}
}
}
FuncJson->SetArrayField(TEXT("Inputs"), InputsArray);
FuncJson->SetArrayField(TEXT("Outputs"), OutputsArray);
FunctionsArray.Add(MakeShareable(new FJsonValueObject(FuncJson)));
}
return FunctionsArray;
}
TArray<TSharedPtr<FJsonValue>> FAssetExporterToJSON::ExtractBlueprintComponents(UBlueprint* Blueprint)
{
TArray<TSharedPtr<FJsonValue>> ComponentsArray;
if (!Blueprint || !Blueprint->SimpleConstructionScript)
return ComponentsArray;
// Extract components from SCS (Simple Construction Script)
const TArray<USCS_Node*>& Nodes = Blueprint->SimpleConstructionScript->GetAllNodes();
for (USCS_Node* Node : Nodes)
{
if (!Node || !Node->ComponentTemplate)
continue;
TSharedPtr<FJsonObject> CompJson = MakeShareable(new FJsonObject);
CompJson->SetStringField(TEXT("Name"), Node->GetVariableName().ToString());
CompJson->SetStringField(TEXT("Class"), Node->ComponentTemplate->GetClass()->GetName());
// Parent component
if (Node->ParentComponentOrVariableName != NAME_None)
{
CompJson->SetStringField(TEXT("Parent"), Node->ParentComponentOrVariableName.ToString());
}
ComponentsArray.Add(MakeShareable(new FJsonValueObject(CompJson)));
}
return ComponentsArray;
}
TArray<TSharedPtr<FJsonValue>> FAssetExporterToJSON::ExtractBlueprintEventGraphs(UBlueprint* Blueprint)
{
TArray<TSharedPtr<FJsonValue>> GraphsArray;
if (!Blueprint)
return GraphsArray;
// Iterate through all graphs (EventGraph, ConstructionScript, etc.)
for (UEdGraph* Graph : Blueprint->UbergraphPages)
{
if (!Graph)
continue;
TSharedPtr<FJsonObject> GraphJson = MakeShareable(new FJsonObject);
GraphJson->SetStringField(TEXT("GraphName"), Graph->GetName());
// Extract all nodes
TArray<TSharedPtr<FJsonValue>> NodesArray;
TMap<UEdGraphNode*, int32> NodeToIndexMap; // For connection references
int32 NodeIndex = 0;
for (UEdGraphNode* Node : Graph->Nodes)
{
if (!Node)
continue;
NodeToIndexMap.Add(Node, NodeIndex++);
TSharedPtr<FJsonObject> NodeJson = MakeShareable(new FJsonObject);
NodeJson->SetStringField(TEXT("NodeName"), Node->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
NodeJson->SetStringField(TEXT("NodeClass"), Node->GetClass()->GetName());
NodeJson->SetNumberField(TEXT("NodePosX"), static_cast<double>(Node->NodePosX));
NodeJson->SetNumberField(TEXT("NodePosY"), static_cast<double>(Node->NodePosY));
// Extract node comment
if (!Node->NodeComment.IsEmpty())
{
NodeJson->SetStringField(TEXT("Comment"), Node->NodeComment);
}
// Extract pins (inputs/outputs)
TArray<TSharedPtr<FJsonValue>> PinsArray;
for (UEdGraphPin* Pin : Node->Pins)
{
if (!Pin)
continue;
TSharedPtr<FJsonObject> PinJson = MakeShareable(new FJsonObject);
PinJson->SetStringField(TEXT("PinName"), Pin->PinName.ToString());
PinJson->SetStringField(TEXT("PinCategory"), Pin->PinType.PinCategory.ToString());
PinJson->SetStringField(TEXT("Direction"), Pin->Direction == EGPD_Input ? TEXT("Input") : TEXT("Output"));
// Get default value if any
if (!Pin->DefaultValue.IsEmpty())
{
PinJson->SetStringField(TEXT("DefaultValue"), Pin->DefaultValue);
}
else if (Pin->DefaultObject)
{
PinJson->SetStringField(TEXT("DefaultObject"), Pin->DefaultObject->GetName());
}
else if (!Pin->DefaultTextValue.IsEmpty())
{
PinJson->SetStringField(TEXT("DefaultText"), Pin->DefaultTextValue.ToString());
}
// Track connections (will be added later)
TArray<TSharedPtr<FJsonValue>> ConnectionsArray;
for (UEdGraphPin* LinkedPin : Pin->LinkedTo)
{
if (LinkedPin && LinkedPin->GetOwningNode())
{
TSharedPtr<FJsonObject> ConnectionJson = MakeShareable(new FJsonObject);
ConnectionJson->SetStringField(TEXT("TargetNode"), LinkedPin->GetOwningNode()->GetNodeTitle(ENodeTitleType::FullTitle).ToString());
ConnectionJson->SetStringField(TEXT("TargetPin"), LinkedPin->PinName.ToString());
ConnectionsArray.Add(MakeShareable(new FJsonValueObject(ConnectionJson)));
}
}
if (ConnectionsArray.Num() > 0)
{
PinJson->SetArrayField(TEXT("LinkedTo"), ConnectionsArray);
}
PinsArray.Add(MakeShareable(new FJsonValueObject(PinJson)));
}
NodeJson->SetArrayField(TEXT("Pins"), PinsArray);
// Extract node-specific properties
TSharedPtr<FJsonObject> NodePropertiesJson = MakeShareable(new FJsonObject);
for (TFieldIterator<FProperty> PropIt(Node->GetClass(), EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
if (!Property || !Property->HasAnyPropertyFlags(CPF_Edit))
continue;
FString PropertyName = Property->GetName();
FString PropertyValue;
Property->ExportText_Direct(PropertyValue, Property->ContainerPtrToValuePtr<void>(Node), nullptr, nullptr, PPF_None);
// Only add non-empty values
if (!PropertyValue.IsEmpty() && PropertyValue != TEXT("()") && PropertyValue != TEXT("\"\""))
{
NodePropertiesJson->SetStringField(PropertyName, PropertyValue);
}
}
if (NodePropertiesJson->Values.Num() > 0)
{
NodeJson->SetObjectField(TEXT("Properties"), NodePropertiesJson);
}
NodesArray.Add(MakeShareable(new FJsonValueObject(NodeJson)));
}
GraphJson->SetArrayField(TEXT("Nodes"), NodesArray);
GraphJson->SetNumberField(TEXT("NodeCount"), NodesArray.Num());
GraphsArray.Add(MakeShareable(new FJsonValueObject(GraphJson)));
}
return GraphsArray;
}
int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray<TSharedPtr<FJsonValue>>& OutJsonArray)
{
UE_LOG(LogAssetExporter, Log, TEXT("Exporting AnimMontages from: %s"), *FolderPath);
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetList;
FARFilter Filter;
Filter.PackagePaths.Add(FName(*FolderPath));
Filter.ClassPaths.Add(UAnimMontage::StaticClass()->GetClassPathName());
Filter.bRecursivePaths = true;
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
// Track exported assets to avoid duplicates
TSet<FString> ExportedPaths;
int32 Count = 0;
for (const FAssetData& AssetData : AssetList)
{
FString AssetPath = AssetData.GetObjectPathString();
// Skip if already exported
if (ExportedPaths.Contains(AssetPath))
{
UE_LOG(LogAssetExporter, Verbose, TEXT(" Skipping duplicate AnimMontage: %s"), *AssetPath);
continue;
}
UAnimMontage* AnimMontage = Cast<UAnimMontage>(AssetData.GetAsset());
if (!AnimMontage)
continue;
ExportedPaths.Add(AssetPath);
TSharedPtr<FJsonObject> MontageJson = MakeShareable(new FJsonObject);
MontageJson->SetStringField(TEXT("AssetName"), AnimMontage->GetName());
MontageJson->SetStringField(TEXT("AssetPath"), AssetPath);
MontageJson->SetNumberField(TEXT("SequenceLength"), static_cast<double>(AnimMontage->GetPlayLength()));
MontageJson->SetNumberField(TEXT("RateScale"), static_cast<double>(AnimMontage->RateScale));
// Export sections with enhanced information
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());
// Export section metadata
if (Section.MetaData.Num() > 0)
{
TArray<TSharedPtr<FJsonValue>> MetaDataArray;
for (UAnimMetaData* MetaData : Section.MetaData)
{
if (MetaData)
{
TSharedPtr<FJsonObject> MetaJson = MakeShareable(new FJsonObject);
MetaJson->SetStringField(TEXT("Class"), MetaData->GetClass()->GetName());
MetaDataArray.Add(MakeShareable(new FJsonValueObject(MetaJson)));
}
}
SectionJson->SetArrayField(TEXT("MetaData"), MetaDataArray);
}
SectionsArray.Add(MakeShareable(new FJsonValueObject(SectionJson)));
}
MontageJson->SetArrayField(TEXT("Sections"), SectionsArray);
MontageJson->SetNumberField(TEXT("NumSections"), SectionsArray.Num());
// Export slot animation tracks
TArray<TSharedPtr<FJsonValue>> SlotsArray;
for (const FSlotAnimationTrack& SlotTrack : AnimMontage->SlotAnimTracks)
{
TSharedPtr<FJsonObject> SlotJson = MakeShareable(new FJsonObject);
SlotJson->SetStringField(TEXT("SlotName"), SlotTrack.SlotName.ToString());
// Export anim segments in this slot
TArray<TSharedPtr<FJsonValue>> SegmentsArray;
for (const FAnimSegment& Segment : SlotTrack.AnimTrack.AnimSegments)
{
TSharedPtr<FJsonObject> SegmentJson = MakeShareable(new FJsonObject);
if (Segment.GetAnimReference())
{
SegmentJson->SetStringField(TEXT("AnimReference"), Segment.GetAnimReference()->GetName());
SegmentJson->SetStringField(TEXT("AnimPath"), Segment.GetAnimReference()->GetPathName());
}
SegmentJson->SetNumberField(TEXT("StartPos"), static_cast<double>(Segment.StartPos));
SegmentJson->SetNumberField(TEXT("AnimStartTime"), static_cast<double>(Segment.AnimStartTime));
SegmentJson->SetNumberField(TEXT("AnimEndTime"), static_cast<double>(Segment.AnimEndTime));
SegmentJson->SetNumberField(TEXT("AnimPlayRate"), static_cast<double>(Segment.AnimPlayRate));
SegmentJson->SetNumberField(TEXT("LoopingCount"), Segment.LoopingCount);
SegmentsArray.Add(MakeShareable(new FJsonValueObject(SegmentJson)));
}
SlotJson->SetArrayField(TEXT("AnimSegments"), SegmentsArray);
SlotsArray.Add(MakeShareable(new FJsonValueObject(SlotJson)));
}
MontageJson->SetArrayField(TEXT("SlotAnimTracks"), SlotsArray);
// Export anim notifies
TArray<TSharedPtr<FJsonValue>> NotifiesArray;
for (const FAnimNotifyEvent& NotifyEvent : AnimMontage->Notifies)
{
TSharedPtr<FJsonObject> NotifyJson = MakeShareable(new FJsonObject);
NotifyJson->SetStringField(TEXT("NotifyName"), NotifyEvent.NotifyName.ToString());
NotifyJson->SetNumberField(TEXT("TriggerTime"), static_cast<double>(NotifyEvent.GetTime()));
NotifyJson->SetNumberField(TEXT("Duration"), static_cast<double>(NotifyEvent.GetDuration()));
NotifyJson->SetStringField(TEXT("NotifyType"), NotifyEvent.NotifyStateClass ? TEXT("NotifyState") : TEXT("Notify"));
// Extract notify class and custom properties
UObject* NotifyObject = nullptr;
if (NotifyEvent.Notify)
{
NotifyJson->SetStringField(TEXT("NotifyClass"), NotifyEvent.Notify->GetClass()->GetName());
NotifyObject = NotifyEvent.Notify;
}
else if (NotifyEvent.NotifyStateClass)
{
NotifyJson->SetStringField(TEXT("NotifyStateClass"), NotifyEvent.NotifyStateClass->GetClass()->GetName());
NotifyObject = NotifyEvent.NotifyStateClass;
}
// Extract custom properties from notify object
if (NotifyObject)
{
TSharedPtr<FJsonObject> CustomPropsJson = MakeShareable(new FJsonObject);
for (TFieldIterator<FProperty> PropIt(NotifyObject->GetClass()); PropIt; ++PropIt)
{
FProperty* Property = *PropIt;
// Only export editable properties
if (Property && Property->HasAnyPropertyFlags(CPF_Edit))
{
FString PropertyName = Property->GetName();
FString PropertyValue;
Property->ExportText_Direct(PropertyValue, Property->ContainerPtrToValuePtr<void>(NotifyObject), nullptr, nullptr, PPF_None);
CustomPropsJson->SetStringField(PropertyName, PropertyValue);
}
}
if (CustomPropsJson->Values.Num() > 0)
{
NotifyJson->SetObjectField(TEXT("CustomProperties"), CustomPropsJson);
}
}
NotifyJson->SetBoolField(TEXT("IsBranchingPoint"), NotifyEvent.bTriggerOnDedicatedServer);
NotifiesArray.Add(MakeShareable(new FJsonValueObject(NotifyJson)));
}
MontageJson->SetArrayField(TEXT("AnimNotifies"), NotifiesArray);
// Export blend settings
MontageJson->SetNumberField(TEXT("BlendInTime"), static_cast<double>(AnimMontage->BlendIn.GetBlendTime()));
MontageJson->SetNumberField(TEXT("BlendOutTime"), static_cast<double>(AnimMontage->BlendOut.GetBlendTime()));
MontageJson->SetNumberField(TEXT("BlendOutTriggerTime"), static_cast<double>(AnimMontage->BlendOutTriggerTime));
// Blend modes
FString BlendModeInStr = AnimMontage->BlendModeIn == EMontageBlendMode::Standard ? TEXT("Standard") : TEXT("Inertialization");
FString BlendModeOutStr = AnimMontage->BlendModeOut == EMontageBlendMode::Standard ? TEXT("Standard") : TEXT("Inertialization");
MontageJson->SetStringField(TEXT("BlendModeIn"), BlendModeInStr);
MontageJson->SetStringField(TEXT("BlendModeOut"), BlendModeOutStr);
// Sync group
if (AnimMontage->SyncGroup != NAME_None)
{
MontageJson->SetStringField(TEXT("SyncGroup"), AnimMontage->SyncGroup.ToString());
}
OutJsonArray.Add(MakeShareable(new FJsonValueObject(MontageJson)));
Count++;
UE_LOG(LogAssetExporter, Verbose, TEXT(" Exported AnimMontage: %s (%d sections, %d slots, %d notifies)"),
*AnimMontage->GetName(),
SectionsArray.Num(),
SlotsArray.Num(),
NotifiesArray.Num());
}
UE_LOG(LogAssetExporter, Log, TEXT("Exported %d AnimMontages"), Count);
return Count;
}
int32 FAssetExporterToJSON::ExportCurveTables(const FString& FolderPath, TArray<TSharedPtr<FJsonValue>>& OutJsonArray)
{
UE_LOG(LogAssetExporter, Log, TEXT("Exporting CurveTables from: %s"), *FolderPath);
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>("AssetRegistry");
TArray<FAssetData> AssetList;
FARFilter Filter;
Filter.PackagePaths.Add(FName(*FolderPath));
Filter.ClassPaths.Add(UCurveTable::StaticClass()->GetClassPathName());
Filter.bRecursivePaths = true;
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
int32 Count = 0;
for (const FAssetData& AssetData : AssetList)
{
UCurveTable* CurveTable = Cast<UCurveTable>(AssetData.GetAsset());
if (!CurveTable)
continue;
TSharedPtr<FJsonObject> CurveTableJson = MakeShareable(new FJsonObject);
CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName());
CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
// Store curve table mode
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
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;
// Get keys using GetConstRefOfKeys
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)));
}
}
else if (CurveTable->GetCurveTableMode() == ECurveTableMode::SimpleCurves)
{
const TMap<FName, FSimpleCurve*>& SimpleCurveMap = CurveTable->GetSimpleCurveRowMap();
for (const TPair<FName, FSimpleCurve*>& Row : SimpleCurveMap)
{
TSharedPtr<FJsonObject> CurveJson = MakeShareable(new FJsonObject);
CurveJson->SetStringField(TEXT("CurveName"), Row.Key.ToString());
if (FSimpleCurve* Curve = Row.Value)
{
TArray<TSharedPtr<FJsonValue>> KeysArray;
// Get keys using GetConstRefOfKeys
const TArray<FSimpleCurveKey>& Keys = Curve->GetConstRefOfKeys();
for (const FSimpleCurveKey& 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);
OutJsonArray.Add(MakeShareable(new FJsonValueObject(CurveTableJson)));
Count++;
UE_LOG(LogAssetExporter, Verbose, TEXT(" Exported CurveTable: %s (%d curves)"), *CurveTable->GetName(), CurvesArray.Num());
}
UE_LOG(LogAssetExporter, Log, TEXT("Exported %d CurveTables"), Count);
return Count;
}
bool FAssetExporterToJSON::SaveJsonToFile(const TArray<TSharedPtr<FJsonValue>>& JsonArray, const FString& FileName, const FString& OutputPath)
{
// Create root object with metadata
TSharedPtr<FJsonObject> RootJson = MakeShareable(new FJsonObject);
RootJson->SetStringField(TEXT("ExportedAt"), FDateTime::Now().ToString(TEXT("%Y-%m-%d %H:%M:%S")));
RootJson->SetNumberField(TEXT("TotalCount"), JsonArray.Num());
RootJson->SetArrayField(TEXT("Assets"), JsonArray);
// Serialize to string
FString OutputString;
TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&OutputString);
if (!FJsonSerializer::Serialize(RootJson.ToSharedRef(), Writer))
{
UE_LOG(LogAssetExporter, Error, TEXT("Failed to serialize JSON for %s"), *FileName);
return false;
}
// Save to file
FString FilePath = OutputPath / FileName;
if (!FFileHelper::SaveStringToFile(OutputString, *FilePath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
UE_LOG(LogAssetExporter, Error, TEXT("Failed to save file: %s"), *FilePath);
return false;
}
UE_LOG(LogAssetExporter, Log, TEXT("Saved %s with %d assets"), *FileName, JsonArray.Num());
return true;
}