commit 3d9d6cc664b932b492eaabaad7cf6e44872de2e7 Author: jinilkim Date: Wed Oct 22 15:14:50 2025 +0900 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 diff --git a/AssetExportSettings.cpp b/AssetExportSettings.cpp new file mode 100644 index 0000000..4b52634 --- /dev/null +++ b/AssetExportSettings.cpp @@ -0,0 +1,43 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#include "AssetExportSettings.h" + +#define LOCTEXT_NAMESPACE "AssetExportSettings" + +UAssetExportSettings::UAssetExportSettings() +{ + // Default settings + OutputDirectory.Path = TEXT("Exports"); + bCreateTimestampedFolder = true; + + // Enable all asset types by default + bExportDataTables = true; + bExportBlueprints = true; + bExportAnimMontages = true; + bExportCurveTables = true; + + // Default export paths - commonly used folders + ExportFolderPaths.Add(FDirectoryPath{TEXT("Blueprints/Enemy")}); + ExportFolderPaths.Add(FDirectoryPath{TEXT("Blueprints/Characters")}); + ExportFolderPaths.Add(FDirectoryPath{TEXT("Blueprints/Abilities")}); + ExportFolderPaths.Add(FDirectoryPath{TEXT("DataTables")}); +} + +FName UAssetExportSettings::GetCategoryName() const +{ + return FName(TEXT("Plugins")); +} + +FText UAssetExportSettings::GetSectionText() const +{ + return LOCTEXT("AssetExportSettingsSection", "Asset Export to JSON"); +} + +#if WITH_EDITOR +FText UAssetExportSettings::GetSectionDescription() const +{ + return LOCTEXT("AssetExportSettingsDescription", "Configure paths and settings for exporting assets to JSON format for LLM analysis"); +} +#endif + +#undef LOCTEXT_NAMESPACE diff --git a/AssetExportSettings.h b/AssetExportSettings.h new file mode 100644 index 0000000..178a468 --- /dev/null +++ b/AssetExportSettings.h @@ -0,0 +1,78 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" +#include "Engine/DeveloperSettings.h" +#include "AssetExportSettings.generated.h" + +/** + * Settings for Asset Export to JSON feature + * Configure export paths in Project Settings -> Plugins -> Asset Export + */ +UCLASS(config=Editor, defaultconfig, meta=(DisplayName="Asset Export to JSON")) +class WORLDSTALKEREDITOR_API UAssetExportSettings : public UDeveloperSettings +{ + GENERATED_BODY() + +public: + UAssetExportSettings(); + + /** + * Folder paths to export assets from + * Paths are relative to /Game/ (e.g., "Blueprints/Enemy", "DataTables") + * Add or remove paths using the array controls below + */ + UPROPERTY(config, EditAnywhere, Category = "Export Paths", meta=( + ContentDir, + RelativePath, + LongPackageName + )) + TArray ExportFolderPaths; + + /** + * Output directory for exported JSON files + * Default: Content/Exports + */ + UPROPERTY(config, EditAnywhere, Category = "Export Settings", meta=(RelativePath)) + FDirectoryPath OutputDirectory; + + /** + * Create timestamped subfolder for each export + * Recommended to keep export history + */ + UPROPERTY(config, EditAnywhere, Category = "Export Settings") + bool bCreateTimestampedFolder; + + /** + * Export DataTable assets + */ + UPROPERTY(config, EditAnywhere, Category = "Asset Types") + bool bExportDataTables; + + /** + * Export Blueprint assets + */ + UPROPERTY(config, EditAnywhere, Category = "Asset Types") + bool bExportBlueprints; + + /** + * Export AnimMontage assets + */ + UPROPERTY(config, EditAnywhere, Category = "Asset Types") + bool bExportAnimMontages; + + /** + * Export CurveTable assets + */ + UPROPERTY(config, EditAnywhere, Category = "Asset Types") + bool bExportCurveTables; + + // UDeveloperSettings interface + virtual FName GetCategoryName() const override; + virtual FText GetSectionText() const override; + +#if WITH_EDITOR + virtual FText GetSectionDescription() const override; +#endif +}; diff --git a/AssetExporterToJSON.cpp b/AssetExporterToJSON.cpp new file mode 100644 index 0000000..56743e3 --- /dev/null +++ b/AssetExporterToJSON.cpp @@ -0,0 +1,917 @@ +// 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; +} diff --git a/AssetExporterToJSON.h b/AssetExporterToJSON.h new file mode 100644 index 0000000..fd4c585 --- /dev/null +++ b/AssetExporterToJSON.h @@ -0,0 +1,99 @@ +// Copyright Epic Games, Inc. All Rights Reserved. + +#pragma once + +#include "CoreMinimal.h" + +/** + * Utility class for exporting various asset types to JSON format + * Supports: DataTable, Blueprint, AnimMontage, CurveTable + * Purpose: Extract asset data for LLM-based combat balance analysis + */ +class WORLDSTALKEREDITOR_API FAssetExporterToJSON +{ +public: + /** + * Main entry point for JSON export + * Opens folder picker dialog and exports all supported assets from selected folder + */ + static void ExportAssetsToJSON(); + +private: + /** + * Export all DataTables from the specified path to JSON + * @param FolderPath - Root folder path to search (recursively) + * @param OutJsonArray - Output array to store exported data + * @return Number of assets exported + */ + static int32 ExportDataTables(const FString& FolderPath, TArray>& OutJsonArray); + + /** + * Export all Blueprints from the specified path to JSON + * Extracts: Variables, Functions, Components, Parent Class + * @param FolderPath - Root folder path to search (recursively) + * @param OutJsonArray - Output array to store exported data + * @return Number of assets exported + */ + static int32 ExportBlueprints(const FString& FolderPath, TArray>& OutJsonArray); + + /** + * Export all AnimMontages from the specified path to JSON + * @param FolderPath - Root folder path to search (recursively) + * @param OutJsonArray - Output array to store exported data + * @return Number of assets exported + */ + static int32 ExportAnimMontages(const FString& FolderPath, TArray>& OutJsonArray); + + /** + * Export all CurveTables from the specified path to JSON + * @param FolderPath - Root folder path to search (recursively) + * @param OutJsonArray - Output array to store exported data + * @return Number of assets exported + */ + static int32 ExportCurveTables(const FString& FolderPath, TArray>& OutJsonArray); + + /** + * Helper: Extract detailed Blueprint information + * @param Blueprint - Blueprint asset to extract from + * @return JSON object containing blueprint data + */ + static TSharedPtr ExtractBlueprintDetails(class UBlueprint* Blueprint); + + /** + * Helper: Extract Blueprint variables + * @param Blueprint - Blueprint asset + * @return JSON array of variables with name, type, default value + */ + static TArray> ExtractBlueprintVariables(class UBlueprint* Blueprint); + + /** + * Helper: Extract Blueprint functions + * @param Blueprint - Blueprint asset + * @return JSON array of functions with name, inputs, outputs + */ + static TArray> ExtractBlueprintFunctions(class UBlueprint* Blueprint); + + /** + * Helper: Extract Blueprint component hierarchy + * @param Blueprint - Blueprint asset + * @return JSON array of components + */ + static TArray> ExtractBlueprintComponents(class UBlueprint* Blueprint); + + /** + * Helper: Extract Blueprint event graphs + * @param Blueprint - Blueprint asset + * @return JSON array of event graphs with nodes and connections + */ + static TArray> ExtractBlueprintEventGraphs(class UBlueprint* Blueprint); + + /** + * Helper: Save JSON array to file + * @param JsonArray - Array of JSON values to save + * @param FileName - Name of output file (e.g., "DataTable.json") + * @param OutputPath - Directory to save the file + * @return true if successful + */ + static bool SaveJsonToFile(const TArray>& JsonArray, const FString& FileName, const FString& OutputPath); + +}; diff --git a/MERGE_INSTRUCTIONS.md b/MERGE_INSTRUCTIONS.md new file mode 100644 index 0000000..916a1a0 --- /dev/null +++ b/MERGE_INSTRUCTIONS.md @@ -0,0 +1,231 @@ +# Merge Instructions for Existing Files + +This document describes the changes that need to be merged into existing WorldStalkerEditor module files. + +## Overview + +The Asset Export to JSON feature requires minimal modifications to existing files: +- **WorldStalkerEditor.h**: Add two private method declarations +- **WorldStalkerEditor.cpp**: Add include directive and implement menu registration + +## File: WorldStalkerEditor.h + +### Location of Changes +Line 18-20 (after existing private member variables) + +### Changes to Add + +```cpp +private: + void RegisterMenuExtensions(); + void AddToolbarMenuEntry(class FMenuBuilder& MenuBuilder); +``` + +### Complete Modified Section + +```cpp +class FWorldStalkerEditorModule : public IModuleInterface +{ +public: + virtual void StartupModule() override; + virtual void ShutdownModule() override; + +private: + // ADD THESE TWO LINES: + void RegisterMenuExtensions(); + void AddToolbarMenuEntry(class FMenuBuilder& MenuBuilder); + + TSharedPtr PluginCommands; + TSharedPtr DataTableListener; + TSharedPtr ObjectSaveEventListener; +}; +``` + +**Note**: The `AddToolbarMenuEntry` method is kept for backward compatibility but is not actively used. + +--- + +## File: WorldStalkerEditor.cpp + +### Change 1: Add Include Directive + +**Location**: Line 4 (after existing includes) + +**Add this line**: +```cpp +#include "Utility/AssetExporterToJSON.h" +``` + +**Complete Include Section**: +```cpp +#include "WorldStalkerEditor.h" +#include "DataTable/DataTableNoticeListener.h" +#include "Asset/ObjectSaveEventListener.h" +#include "Utility/AssetExporterToJSON.h" // ADD THIS LINE +#include "ToolMenus.h" +``` + +### Change 2: Register Menu Extensions in StartupModule + +**Location**: Inside `StartupModule()` method (after existing initialization) + +**Add these lines**: +```cpp +// Register editor menu extensions +UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FWorldStalkerEditorModule::RegisterMenuExtensions)); +``` + +**Complete StartupModule Method**: +```cpp +void FWorldStalkerEditorModule::StartupModule() +{ + DataTableListener = MakeShared(); + ObjectSaveEventListener = MakeShared(); + + // ADD THESE TWO LINES: + // Register editor menu extensions + UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FWorldStalkerEditorModule::RegisterMenuExtensions)); +} +``` + +### Change 3: Cleanup in ShutdownModule + +**Location**: Inside `ShutdownModule()` method (after existing cleanup) + +**Add these lines**: +```cpp +// Unregister all tool menus +UToolMenus::UnRegisterStartupCallback(this); +UToolMenus::UnregisterOwner(this); +``` + +**Complete ShutdownModule Method**: +```cpp +void FWorldStalkerEditorModule::ShutdownModule() +{ + DataTableListener.Reset(); + ObjectSaveEventListener.Reset(); + + // ADD THESE TWO LINES: + // Unregister all tool menus + UToolMenus::UnRegisterStartupCallback(this); + UToolMenus::UnregisterOwner(this); +} +``` + +### Change 4: Add New Method Implementations + +**Location**: After `ShutdownModule()` method (before `#undef LOCTEXT_NAMESPACE`) + +**Add these two complete methods**: + +```cpp +void FWorldStalkerEditorModule::RegisterMenuExtensions() +{ + UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"); + if (!ToolsMenu) + { + return; + } + + FToolMenuSection& Section = ToolsMenu->FindOrAddSection("WorldStalkerTools"); + Section.Label = LOCTEXT("WorldStalkerToolsSection", "WorldStalker"); + + Section.AddMenuEntry( + "ExportAssetsToJSON", + LOCTEXT("ExportAssetsToJSON", "Export Assets to JSON"), + LOCTEXT("ExportAssetsToJSONTooltip", "Export DataTable, Blueprint, AnimMontage, and CurveTable assets to JSON format for LLM analysis"), + FSlateIcon(FAppStyle::GetAppStyleSetName(), "LevelEditor.Tabs.StatsViewer"), + FUIAction(FExecuteAction::CreateStatic(&FAssetExporterToJSON::ExportAssetsToJSON)) + ); +} + +void FWorldStalkerEditorModule::AddToolbarMenuEntry(FMenuBuilder& MenuBuilder) +{ + // This method is no longer used but kept for backward compatibility +} +``` + +--- + +## Build Configuration + +Ensure `WorldStalkerEditor.Build.cs` includes these dependencies: + +```csharp +PublicDependencyModuleNames.AddRange(new string[] { + "AssetRegistry", + "Json", + "JsonUtilities" +}); + +PrivateDependencyModuleNames.AddRange(new string[] { + "UnrealEd", + "Engine", + "Slate", + "SlateCore", + "ToolMenus", + "DeveloperSettings" +}); +``` + +**Note**: Most of these dependencies likely already exist in your module. Only add the ones that are missing. + +--- + +## Summary of Changes + +### WorldStalkerEditor.h +- **Lines Added**: 2 method declarations +- **Purpose**: Declare menu registration methods + +### WorldStalkerEditor.cpp +- **Lines Added**: + - 1 include directive + - 2 lines in StartupModule() + - 2 lines in ShutdownModule() + - 2 complete method implementations (~24 lines) +- **Total**: ~29 lines added +- **Purpose**: Implement Tools menu integration + +--- + +## Testing After Merge + +1. Rebuild the WorldStalkerEditor module +2. Launch Unreal Editor +3. Navigate to `Tools → WorldStalker → Export Assets to JSON` +4. Configure settings in `Edit → Project Settings → Plugins → Asset Export to JSON` +5. Run export and verify JSON files are created in `Content/Exports/` + +--- + +## Rollback Instructions + +If you need to remove this feature: + +1. Delete the four new files: + - `AssetExportSettings.h` + - `AssetExportSettings.cpp` + - `AssetExporterToJSON.h` + - `AssetExporterToJSON.cpp` + +2. Remove changes from `WorldStalkerEditor.h`: + - Delete the two method declarations + +3. Remove changes from `WorldStalkerEditor.cpp`: + - Remove the `#include "Utility/AssetExporterToJSON.h"` line + - Remove UToolMenus registration from StartupModule() + - Remove UToolMenus cleanup from ShutdownModule() + - Delete the two method implementations + +4. Rebuild the module + +--- + +## Contact + +For questions about merging these changes: +- Author: jinilkim@actionsquare.com +- Project: WorldStalker (DungeonStalkers) +- Module: WorldStalkerEditor diff --git a/README.md b/README.md new file mode 100644 index 0000000..4b15add --- /dev/null +++ b/README.md @@ -0,0 +1,348 @@ +# Asset Export to JSON - Unreal Engine Editor Extension + +## Overview + +This editor extension provides a comprehensive asset export system that converts Unreal Engine assets to JSON format for LLM-based combat balance analysis. The feature extracts detailed data from various asset types to create text-based documentation that can be analyzed by AI systems. + +**Purpose**: Enable comprehensive combat balance analysis by providing structured, text-based representations of game assets suitable for Large Language Model (LLM) processing. + +**Supported Asset Types**: +- **DataTable** - Complete row and column data +- **Blueprint** - Variables, functions, components, event graphs with node connections +- **AnimMontage** - Sections, notifies, custom properties, slot animations +- **CurveTable** - Key-value curve data (RichCurves and SimpleCurves) + +## Features + +### Configuration-Based Export +- **Project Settings Integration**: Configure export paths in `Edit → Project Settings → Plugins → Asset Export to JSON` +- **Persistent Configuration**: Settings saved to `Config/DefaultEditor.ini` for team sharing via SVN/Git +- **Multi-Folder Support**: Export from multiple folder paths with single click +- **Asset Type Filtering**: Enable/disable specific asset types via settings + +### Comprehensive Data Extraction + +#### Blueprint Export +- Variables with types and default values +- Functions with inputs/outputs +- Component hierarchy +- **Event Graphs**: Complete node graph with: + - Node types, titles, positions, comments + - Pin types, directions, default values + - Connection graph between pins + - Node-specific properties + +#### AnimMontage Export +- Sections with start/end times +- Slot animation tracks with segments +- **Notify Events**: Complete notify data including: + - Notify type and trigger times + - Custom properties from notify classes + - Blend in/out settings + - Track indices and sync markers + +#### CurveTable Export +- Support for both RichCurves and SimpleCurves +- Key data with time, value, tangent information +- Curve interpolation modes + +### Output Management +- **Timestamped Exports**: Optional timestamped subfolders for export history +- **Type-Separated Files**: Separate JSON files per asset type (DataTable.json, Blueprint.json, etc.) +- **Pretty-Printed JSON**: Human-readable formatting with indentation +- **Duplicate Detection**: Prevents duplicate exports when folders overlap + +## Installation + +### 1. File Structure + +Copy the following files to your project: + +**New Files** (copy to `Source/WorldStalkerEditor/Utility/`): +``` +AssetExportSettings.h +AssetExportSettings.cpp +AssetExporterToJSON.h +AssetExporterToJSON.cpp +``` + +**Modified Files** (merge changes manually): +- `WorldStalkerEditor.h` - See MERGE_INSTRUCTIONS.md +- `WorldStalkerEditor.cpp` - See MERGE_INSTRUCTIONS.md + +### 2. Build Configuration + +Add to your `WorldStalkerEditor.Build.cs`: + +```csharp +PublicDependencyModuleNames.AddRange(new string[] { + "AssetRegistry", + "Json", + "JsonUtilities" +}); + +PrivateDependencyModuleNames.AddRange(new string[] { + "UnrealEd", + "Engine", + "Slate", + "SlateCore", + "ToolMenus", + "DeveloperSettings" +}); +``` + +### 3. Rebuild Project + +```batch +# Generate Visual Studio project files +GenerateVSProjectFile.bat + +# Build the editor module +# Or rebuild from Visual Studio +``` + +## Configuration + +### Step 1: Open Project Settings + +Navigate to: `Edit → Project Settings → Plugins → Asset Export to JSON` + +### Step 2: Configure Export Paths + +**Export Folder Paths** (Array): +- Add folder paths relative to `/Game/` content directory +- Example paths: + - `Blueprints/Enemy` + - `Blueprints/Characters` + - `Blueprints/Abilities` + - `DataTables` +- Use the `+` button to add new paths +- Use the `-` button to remove paths + +**Default Paths** (automatically configured): +``` +Blueprints/Enemy +Blueprints/Characters +Blueprints/Abilities +DataTables +``` + +### Step 3: Configure Output Settings + +**Output Directory**: +- Default: `Exports` (relative to Content folder) +- Set custom path if desired + +**Create Timestamped Folder**: +- Enabled: Creates subfolder like `Export_2025_01_15_143022` +- Disabled: Overwrites files in output directory + +### Step 4: Select Asset Types + +Enable or disable export for specific asset types: +- ☑ Export DataTables +- ☑ Export Blueprints +- ☑ Export AnimMontages +- ☑ Export CurveTables + +## Usage + +### Quick Export + +1. Open Unreal Editor +2. Navigate to: `Tools → WorldStalker → Export Assets to JSON` +3. Wait for export to complete (notification dialog will appear) + +### Export Output + +Files are saved to: `Content/Exports/` (or configured output directory) + +**Output Structure** (with timestamped folder): +``` +Content/ +└── Exports/ + └── Export_2025_01_15_143022/ + ├── DataTable.json + ├── Blueprint.json + ├── AnimMontage.json + └── CurveTable.json +``` + +### Output Format Examples + +#### DataTable.json +```json +[ + { + "AssetName": "DT_CharacterStats", + "AssetPath": "/Game/DataTables/DT_CharacterStats", + "RowCount": 10, + "Columns": ["Name", "Health", "Attack", "Defense"], + "Rows": { + "Warrior": { + "Name": "Warrior", + "Health": "1000", + "Attack": "150", + "Defense": "100" + } + } + } +] +``` + +#### Blueprint.json +```json +[ + { + "AssetName": "BP_Enemy_Goblin", + "AssetPath": "/Game/Blueprints/Enemy/BP_Enemy_Goblin", + "ParentClass": "WSCharacterBase", + "Variables": [ + { + "Name": "MaxHealth", + "Type": "float", + "DefaultValue": "500.0" + } + ], + "EventGraphs": [ + { + "GraphName": "EventGraph", + "Nodes": [ + { + "NodeTitle": "Event BeginPlay", + "NodeClass": "K2Node_Event", + "Pins": [...] + } + ], + "Connections": [ + { + "SourceNode": "Event BeginPlay", + "SourcePin": "then", + "TargetNode": "Set Max Health", + "TargetPin": "execute" + } + ] + } + ] + } +] +``` + +#### AnimMontage.json +```json +[ + { + "AssetName": "AM_Attack_Combo", + "AssetPath": "/Game/Animations/AM_Attack_Combo", + "SequenceLength": 2.5, + "Sections": [ + { + "SectionName": "Combo1", + "StartTime": 0.0, + "EndTime": 0.8 + } + ], + "Notifies": [ + { + "NotifyName": "DamageNotify", + "TriggerTime": 0.5, + "NotifyType": "AnimNotify", + "CustomProperties": { + "DamageMultiplier": "1.5", + "HitboxSize": "100.0" + } + } + ], + "SlotAnimTracks": [...] + } +] +``` + +## Troubleshooting + +### No Output Files Generated + +**Problem**: Export completes but no JSON files are created +**Solution**: Check that export paths contain assets of the enabled types + +### Duplicate Assets in Output + +**Problem**: Same asset appears multiple times in JSON +**Solution**: This is now prevented by duplicate detection - check for overlapping folder paths in settings + +### Build Errors After Integration + +**Problem**: Compilation errors about missing headers +**Solution**: Ensure all dependencies are added to `.Build.cs` file (see Installation section) + +### Export Hangs on Large Projects + +**Problem**: Export takes very long time +**Solution**: Reduce number of export paths or disable asset types not needed + +### Settings Not Persisting + +**Problem**: Configuration resets after editor restart +**Solution**: Check that `Config/DefaultEditor.ini` is writable and not locked + +## Technical Details + +### Unreal Engine Version +- **Tested on**: Unreal Engine 5.5.4 (Custom Build) +- **API Compatibility**: Uses UE 5.5+ CurveTable API (GetRichCurveRowMap/GetSimpleCurveRowMap) + +### Module Dependencies +- **AssetRegistry** - Asset discovery and filtering +- **Json/JsonUtilities** - JSON serialization +- **UnrealEd** - Editor integration +- **ToolMenus** - Menu extension system +- **DeveloperSettings** - Project settings integration + +### Performance Characteristics +- **Recursive Search**: Searches all subfolders of configured paths +- **Memory Usage**: Loads assets one at a time to minimize memory footprint +- **Export Speed**: ~10-50 assets per second depending on asset complexity + +### Blueprint Event Graph Extraction +The system traverses the Blueprint graph structure to extract: +- All nodes from `UbergraphPages` (event graphs) +- Node metadata (class, title, position, comments) +- Pin information (type, direction, default values) +- Connection graph between pins (LinkedTo relationships) +- Property values from graph nodes + +### AnimMontage Custom Property Extraction +Custom properties are extracted from Notify objects by: +1. Iterating through all `AnimNotifyEvent` entries +2. Reflecting on Notify object class properties +3. Filtering for editable properties (`CPF_Edit` flag) +4. Serializing property values to JSON + +## Known Limitations + +1. **Blueprint Visual Script Logic**: Exports node graph structure but not visual script bytecode execution flow +2. **Binary Assets**: Cannot export binary data (meshes, textures, audio) +3. **Complex Property Types**: Some complex UObject properties may export as object references only +4. **Large Blueprints**: Very large blueprints with thousands of nodes may take significant time to export + +## Development History + +**Initial Release**: INI-based configuration system with comprehensive asset extraction + +**Key Design Decisions**: +- INI-based configuration chosen over checkbox UI for repeated use convenience +- UDeveloperSettings integration for team collaboration via version control +- Duplicate detection prevents redundant exports from overlapping folder paths +- Pretty-printed JSON for human readability and LLM processing + +## Support + +For issues, questions, or feature requests: +1. Check this README and MERGE_INSTRUCTIONS.md +2. Review Output Log in Unreal Editor for error messages +3. Contact: jinilkim@actionsquare.com + +## License + +Copyright Epic Games, Inc. All Rights Reserved. +Part of the WorldStalker (DungeonStalkers) project.