// 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(); 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 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> DataTableArray; TArray> BlueprintArray; TArray> AnimMontageArray; TArray> 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>& OutJsonArray) { UE_LOG(LogAssetExporter, Log, TEXT("Exporting DataTables from: %s"), *FolderPath); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); TArray 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(AssetData.GetAsset()); if (!DataTable) continue; TSharedPtr 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> RowsArray; const UScriptStruct* RowStruct = DataTable->GetRowStruct(); const TMap& RowMap = DataTable->GetRowMap(); for (const TPair& Row : RowMap) { TSharedPtr 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 RowDataJson; TSharedRef> 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>& OutJsonArray) { UE_LOG(LogAssetExporter, Log, TEXT("Exporting Blueprints from: %s"), *FolderPath); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); TArray 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(AssetData.GetAsset()); if (!Blueprint || !Blueprint->GeneratedClass) continue; TSharedPtr 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 FAssetExporterToJSON::ExtractBlueprintDetails(UBlueprint* Blueprint) { if (!Blueprint || !Blueprint->GeneratedClass) return nullptr; TSharedPtr 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> Variables = ExtractBlueprintVariables(Blueprint); BlueprintJson->SetArrayField(TEXT("Variables"), Variables); // Extract functions TArray> Functions = ExtractBlueprintFunctions(Blueprint); BlueprintJson->SetArrayField(TEXT("Functions"), Functions); // Extract components TArray> Components = ExtractBlueprintComponents(Blueprint); BlueprintJson->SetArrayField(TEXT("Components"), Components); // Extract event graphs TArray> EventGraphs = ExtractBlueprintEventGraphs(Blueprint); BlueprintJson->SetArrayField(TEXT("EventGraphs"), EventGraphs); return BlueprintJson; } TArray> FAssetExporterToJSON::ExtractBlueprintVariables(UBlueprint* Blueprint) { TArray> VariablesArray; if (!Blueprint || !Blueprint->GeneratedClass) return VariablesArray; UObject* DefaultObject = Blueprint->GeneratedClass->GetDefaultObject(); if (!DefaultObject) return VariablesArray; // Iterate through all properties for (TFieldIterator PropIt(Blueprint->GeneratedClass, EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt) { FProperty* Property = *PropIt; if (!Property) continue; TSharedPtr 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(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> FAssetExporterToJSON::ExtractBlueprintFunctions(UBlueprint* Blueprint) { TArray> FunctionsArray; if (!Blueprint) return FunctionsArray; // Iterate through all function graphs for (UEdGraph* Graph : Blueprint->FunctionGraphs) { if (!Graph) continue; TSharedPtr FuncJson = MakeShareable(new FJsonObject); FuncJson->SetStringField(TEXT("Name"), Graph->GetName()); TArray> InputsArray; TArray> OutputsArray; // Find entry and result nodes to extract parameters for (UEdGraphNode* Node : Graph->Nodes) { if (UK2Node_FunctionEntry* EntryNode = Cast(Node)) { // Extract input parameters for (UEdGraphPin* Pin : EntryNode->Pins) { if (Pin && Pin->Direction == EGPD_Output) { TSharedPtr 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(Node)) { // Extract output parameters for (UEdGraphPin* Pin : ResultNode->Pins) { if (Pin && Pin->Direction == EGPD_Input) { TSharedPtr 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> FAssetExporterToJSON::ExtractBlueprintComponents(UBlueprint* Blueprint) { TArray> ComponentsArray; if (!Blueprint || !Blueprint->SimpleConstructionScript) return ComponentsArray; // Extract components from SCS (Simple Construction Script) const TArray& Nodes = Blueprint->SimpleConstructionScript->GetAllNodes(); for (USCS_Node* Node : Nodes) { if (!Node || !Node->ComponentTemplate) continue; TSharedPtr 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> FAssetExporterToJSON::ExtractBlueprintEventGraphs(UBlueprint* Blueprint) { TArray> GraphsArray; if (!Blueprint) return GraphsArray; // Iterate through all graphs (EventGraph, ConstructionScript, etc.) for (UEdGraph* Graph : Blueprint->UbergraphPages) { if (!Graph) continue; TSharedPtr GraphJson = MakeShareable(new FJsonObject); GraphJson->SetStringField(TEXT("GraphName"), Graph->GetName()); // Extract all nodes TArray> NodesArray; TMap NodeToIndexMap; // For connection references int32 NodeIndex = 0; for (UEdGraphNode* Node : Graph->Nodes) { if (!Node) continue; NodeToIndexMap.Add(Node, NodeIndex++); TSharedPtr 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(Node->NodePosX)); NodeJson->SetNumberField(TEXT("NodePosY"), static_cast(Node->NodePosY)); // Extract node comment if (!Node->NodeComment.IsEmpty()) { NodeJson->SetStringField(TEXT("Comment"), Node->NodeComment); } // Extract pins (inputs/outputs) TArray> PinsArray; for (UEdGraphPin* Pin : Node->Pins) { if (!Pin) continue; TSharedPtr 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> ConnectionsArray; for (UEdGraphPin* LinkedPin : Pin->LinkedTo) { if (LinkedPin && LinkedPin->GetOwningNode()) { TSharedPtr 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 NodePropertiesJson = MakeShareable(new FJsonObject); for (TFieldIterator 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(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>& OutJsonArray) { UE_LOG(LogAssetExporter, Log, TEXT("Exporting AnimMontages from: %s"), *FolderPath); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); TArray 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 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(AssetData.GetAsset()); if (!AnimMontage) continue; ExportedPaths.Add(AssetPath); TSharedPtr MontageJson = MakeShareable(new FJsonObject); MontageJson->SetStringField(TEXT("AssetName"), AnimMontage->GetName()); MontageJson->SetStringField(TEXT("AssetPath"), AssetPath); MontageJson->SetNumberField(TEXT("SequenceLength"), static_cast(AnimMontage->GetPlayLength())); MontageJson->SetNumberField(TEXT("RateScale"), static_cast(AnimMontage->RateScale)); // Export sections with enhanced information TArray> SectionsArray; for (const FCompositeSection& Section : AnimMontage->CompositeSections) { TSharedPtr SectionJson = MakeShareable(new FJsonObject); SectionJson->SetStringField(TEXT("SectionName"), Section.SectionName.ToString()); SectionJson->SetNumberField(TEXT("StartTime"), static_cast(Section.GetTime())); SectionJson->SetStringField(TEXT("NextSectionName"), Section.NextSectionName.ToString()); // Export section metadata if (Section.MetaData.Num() > 0) { TArray> MetaDataArray; for (UAnimMetaData* MetaData : Section.MetaData) { if (MetaData) { TSharedPtr 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> SlotsArray; for (const FSlotAnimationTrack& SlotTrack : AnimMontage->SlotAnimTracks) { TSharedPtr SlotJson = MakeShareable(new FJsonObject); SlotJson->SetStringField(TEXT("SlotName"), SlotTrack.SlotName.ToString()); // Export anim segments in this slot TArray> SegmentsArray; for (const FAnimSegment& Segment : SlotTrack.AnimTrack.AnimSegments) { TSharedPtr 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(Segment.StartPos)); SegmentJson->SetNumberField(TEXT("AnimStartTime"), static_cast(Segment.AnimStartTime)); SegmentJson->SetNumberField(TEXT("AnimEndTime"), static_cast(Segment.AnimEndTime)); SegmentJson->SetNumberField(TEXT("AnimPlayRate"), static_cast(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> NotifiesArray; for (const FAnimNotifyEvent& NotifyEvent : AnimMontage->Notifies) { TSharedPtr NotifyJson = MakeShareable(new FJsonObject); NotifyJson->SetStringField(TEXT("NotifyName"), NotifyEvent.NotifyName.ToString()); NotifyJson->SetNumberField(TEXT("TriggerTime"), static_cast(NotifyEvent.GetTime())); NotifyJson->SetNumberField(TEXT("Duration"), static_cast(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 CustomPropsJson = MakeShareable(new FJsonObject); for (TFieldIterator 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(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(AnimMontage->BlendIn.GetBlendTime())); MontageJson->SetNumberField(TEXT("BlendOutTime"), static_cast(AnimMontage->BlendOut.GetBlendTime())); MontageJson->SetNumberField(TEXT("BlendOutTriggerTime"), static_cast(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>& OutJsonArray) { UE_LOG(LogAssetExporter, Log, TEXT("Exporting CurveTables from: %s"), *FolderPath); FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked("AssetRegistry"); TArray 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(AssetData.GetAsset()); if (!CurveTable) continue; TSharedPtr 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> CurvesArray; if (CurveTable->GetCurveTableMode() == ECurveTableMode::RichCurves) { const TMap& RichCurveMap = CurveTable->GetRichCurveRowMap(); for (const TPair& Row : RichCurveMap) { TSharedPtr CurveJson = MakeShareable(new FJsonObject); CurveJson->SetStringField(TEXT("CurveName"), Row.Key.ToString()); if (FRichCurve* Curve = Row.Value) { TArray> KeysArray; // Get keys using GetConstRefOfKeys const TArray& Keys = Curve->GetConstRefOfKeys(); for (const FRichCurveKey& Key : Keys) { TSharedPtr KeyJson = MakeShareable(new FJsonObject); KeyJson->SetNumberField(TEXT("Time"), static_cast(Key.Time)); KeyJson->SetNumberField(TEXT("Value"), static_cast(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& SimpleCurveMap = CurveTable->GetSimpleCurveRowMap(); for (const TPair& Row : SimpleCurveMap) { TSharedPtr CurveJson = MakeShareable(new FJsonObject); CurveJson->SetStringField(TEXT("CurveName"), Row.Key.ToString()); if (FSimpleCurve* Curve = Row.Value) { TArray> KeysArray; // Get keys using GetConstRefOfKeys const TArray& Keys = Curve->GetConstRefOfKeys(); for (const FSimpleCurveKey& Key : Keys) { TSharedPtr KeyJson = MakeShareable(new FJsonObject); KeyJson->SetNumberField(TEXT("Time"), static_cast(Key.Time)); KeyJson->SetNumberField(TEXT("Value"), static_cast(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>& JsonArray, const FString& FileName, const FString& OutputPath) { // Create root object with metadata TSharedPtr 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>> Writer = TJsonWriterFactory>::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; }