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:
43
AssetExportSettings.cpp
Normal file
43
AssetExportSettings.cpp
Normal 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
78
AssetExportSettings.h
Normal 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
917
AssetExporterToJSON.cpp
Normal 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
99
AssetExporterToJSON.h
Normal 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
231
MERGE_INSTRUCTIONS.md
Normal 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
348
README.md
Normal 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.
|
||||||
Reference in New Issue
Block a user