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
This commit is contained in:
2025-10-22 15:14:50 +09:00
commit 3d9d6cc664
6 changed files with 1716 additions and 0 deletions

43
AssetExportSettings.cpp Normal file
View File

@ -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

78
AssetExportSettings.h Normal file
View File

@ -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<FDirectoryPath> 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
};

917
AssetExporterToJSON.cpp Normal file
View File

@ -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<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;
}

99
AssetExporterToJSON.h Normal file
View File

@ -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<TSharedPtr<FJsonValue>>& 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<TSharedPtr<FJsonValue>>& 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<TSharedPtr<FJsonValue>>& 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<TSharedPtr<FJsonValue>>& OutJsonArray);
/**
* Helper: Extract detailed Blueprint information
* @param Blueprint - Blueprint asset to extract from
* @return JSON object containing blueprint data
*/
static TSharedPtr<FJsonObject> ExtractBlueprintDetails(class UBlueprint* Blueprint);
/**
* Helper: Extract Blueprint variables
* @param Blueprint - Blueprint asset
* @return JSON array of variables with name, type, default value
*/
static TArray<TSharedPtr<FJsonValue>> ExtractBlueprintVariables(class UBlueprint* Blueprint);
/**
* Helper: Extract Blueprint functions
* @param Blueprint - Blueprint asset
* @return JSON array of functions with name, inputs, outputs
*/
static TArray<TSharedPtr<FJsonValue>> ExtractBlueprintFunctions(class UBlueprint* Blueprint);
/**
* Helper: Extract Blueprint component hierarchy
* @param Blueprint - Blueprint asset
* @return JSON array of components
*/
static TArray<TSharedPtr<FJsonValue>> 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<TSharedPtr<FJsonValue>> 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<TSharedPtr<FJsonValue>>& JsonArray, const FString& FileName, const FString& OutputPath);
};

231
MERGE_INSTRUCTIONS.md Normal file
View File

@ -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<class FUICommandList> PluginCommands;
TSharedPtr<class FDataTableNoticeListener> DataTableListener;
TSharedPtr<class FObjectSaveEventListener> 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<FDataTableNoticeListener>();
ObjectSaveEventListener = MakeShared<FObjectSaveEventListener>();
// 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

348
README.md Normal file
View File

@ -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.