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
918 lines
32 KiB
C++
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;
|
|
}
|