Add single asset export functionality and enhance README with usage details
This commit is contained in:
@ -196,6 +196,202 @@ void FAssetExporterToJSON::ExportAssetsToJSON()
|
|||||||
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message));
|
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void FAssetExporterToJSON::ExportSingleAssetToJSON(const FAssetData& AssetData)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT("Exporting single asset: %s"), *AssetData.GetObjectPathString());
|
||||||
|
|
||||||
|
UObject* Asset = AssetData.GetAsset();
|
||||||
|
if (!Asset)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Error, TEXT("Failed to load asset: %s"), *AssetData.GetObjectPathString());
|
||||||
|
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(TEXT("Failed to load asset")));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine asset type and export accordingly
|
||||||
|
TArray<TSharedPtr<FJsonValue>> JsonArray;
|
||||||
|
FString AssetTypeName;
|
||||||
|
|
||||||
|
if (UDataTable* DataTable = Cast<UDataTable>(Asset))
|
||||||
|
{
|
||||||
|
AssetTypeName = TEXT("DataTable");
|
||||||
|
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());
|
||||||
|
|
||||||
|
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);
|
||||||
|
JsonArray.Add(MakeShareable(new FJsonValueObject(DataTableJson)));
|
||||||
|
}
|
||||||
|
else if (UBlueprint* Blueprint = Cast<UBlueprint>(Asset))
|
||||||
|
{
|
||||||
|
AssetTypeName = TEXT("Blueprint");
|
||||||
|
TSharedPtr<FJsonObject> BlueprintJson = ExtractBlueprintDetails(Blueprint);
|
||||||
|
if (BlueprintJson.IsValid())
|
||||||
|
{
|
||||||
|
BlueprintJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
||||||
|
JsonArray.Add(MakeShareable(new FJsonValueObject(BlueprintJson)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (UAnimMontage* AnimMontage = Cast<UAnimMontage>(Asset))
|
||||||
|
{
|
||||||
|
AssetTypeName = TEXT("AnimMontage");
|
||||||
|
TSharedPtr<FJsonObject> MontageJson = MakeShareable(new FJsonObject);
|
||||||
|
MontageJson->SetStringField(TEXT("AssetName"), AnimMontage->GetName());
|
||||||
|
MontageJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
||||||
|
MontageJson->SetNumberField(TEXT("SequenceLength"), static_cast<double>(AnimMontage->GetPlayLength()));
|
||||||
|
MontageJson->SetNumberField(TEXT("RateScale"), static_cast<double>(AnimMontage->RateScale));
|
||||||
|
|
||||||
|
// Export sections
|
||||||
|
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());
|
||||||
|
SectionsArray.Add(MakeShareable(new FJsonValueObject(SectionJson)));
|
||||||
|
}
|
||||||
|
MontageJson->SetArrayField(TEXT("Sections"), SectionsArray);
|
||||||
|
|
||||||
|
// Note: Simplified export for single asset - full export includes SlotAnimTracks and Notifies
|
||||||
|
JsonArray.Add(MakeShareable(new FJsonValueObject(MontageJson)));
|
||||||
|
}
|
||||||
|
else if (UCurveTable* CurveTable = Cast<UCurveTable>(Asset))
|
||||||
|
{
|
||||||
|
AssetTypeName = TEXT("CurveTable");
|
||||||
|
TSharedPtr<FJsonObject> CurveTableJson = MakeShareable(new FJsonObject);
|
||||||
|
CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName());
|
||||||
|
CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
||||||
|
|
||||||
|
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 (simplified for single asset)
|
||||||
|
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;
|
||||||
|
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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CurveTableJson->SetArrayField(TEXT("Curves"), CurvesArray);
|
||||||
|
JsonArray.Add(MakeShareable(new FJsonValueObject(CurveTableJson)));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Warning, TEXT("Unsupported asset type: %s"), *Asset->GetClass()->GetName());
|
||||||
|
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(FString::Printf(
|
||||||
|
TEXT("Unsupported asset type: %s\n\nSupported types: DataTable, Blueprint, AnimMontage, CurveTable"),
|
||||||
|
*Asset->GetClass()->GetName()
|
||||||
|
)));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (JsonArray.Num() == 0)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Warning, TEXT("No data to export"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create output directory
|
||||||
|
const UAssetExportSettings* Settings = GetDefault<UAssetExportSettings>();
|
||||||
|
FString OutputPath = FPaths::ProjectContentDir();
|
||||||
|
if (Settings && !Settings->OutputDirectory.Path.IsEmpty())
|
||||||
|
{
|
||||||
|
OutputPath = OutputPath / Settings->OutputDirectory.Path;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OutputPath = OutputPath / TEXT("Exports");
|
||||||
|
}
|
||||||
|
|
||||||
|
IPlatformFile& PlatformFile = FPlatformFileManager::Get().GetPlatformFile();
|
||||||
|
if (!PlatformFile.DirectoryExists(*OutputPath))
|
||||||
|
{
|
||||||
|
PlatformFile.CreateDirectory(*OutputPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create timestamped filename
|
||||||
|
FString Timestamp = FDateTime::Now().ToString(TEXT("%Y%m%d_%H%M%S"));
|
||||||
|
FString SafeAssetName = Asset->GetName().Replace(TEXT(" "), TEXT("_"));
|
||||||
|
FString FileName = FString::Printf(TEXT("%s_%s_%s.json"), *AssetTypeName, *SafeAssetName, *Timestamp);
|
||||||
|
|
||||||
|
// Save JSON
|
||||||
|
if (SaveJsonToFile(JsonArray, FileName, OutputPath))
|
||||||
|
{
|
||||||
|
FString FilePath = OutputPath / FileName;
|
||||||
|
FString Message = FString::Printf(
|
||||||
|
TEXT("Asset exported successfully!\n\n")
|
||||||
|
TEXT("Asset: %s\n")
|
||||||
|
TEXT("Type: %s\n\n")
|
||||||
|
TEXT("Output file:\n%s"),
|
||||||
|
*Asset->GetName(),
|
||||||
|
*AssetTypeName,
|
||||||
|
*FilePath
|
||||||
|
);
|
||||||
|
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT("Single asset export completed: %s"), *FilePath);
|
||||||
|
FMessageDialog::Open(EAppMsgType::Ok, FText::FromString(Message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray<TSharedPtr<FJsonValue>>& OutJsonArray)
|
int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray<TSharedPtr<FJsonValue>>& OutJsonArray)
|
||||||
{
|
{
|
||||||
UE_LOG(LogAssetExporter, Log, TEXT("Exporting DataTables from: %s"), *FolderPath);
|
UE_LOG(LogAssetExporter, Log, TEXT("Exporting DataTables from: %s"), *FolderPath);
|
||||||
@ -210,13 +406,22 @@ int32 FAssetExporterToJSON::ExportDataTables(const FString& FolderPath, TArray<T
|
|||||||
|
|
||||||
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
||||||
|
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT("Found %d DataTables to export"), AssetList.Num());
|
||||||
|
|
||||||
int32 Count = 0;
|
int32 Count = 0;
|
||||||
|
int32 TotalCount = AssetList.Num();
|
||||||
for (const FAssetData& AssetData : AssetList)
|
for (const FAssetData& AssetData : AssetList)
|
||||||
{
|
{
|
||||||
UDataTable* DataTable = Cast<UDataTable>(AssetData.GetAsset());
|
UDataTable* DataTable = Cast<UDataTable>(AssetData.GetAsset());
|
||||||
if (!DataTable)
|
if (!DataTable)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Progress logging every 10 assets or for first/last
|
||||||
|
if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT(" Processing DataTable %d/%d: %s"), Count + 1, TotalCount, *DataTable->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> DataTableJson = MakeShareable(new FJsonObject);
|
TSharedPtr<FJsonObject> DataTableJson = MakeShareable(new FJsonObject);
|
||||||
DataTableJson->SetStringField(TEXT("AssetName"), DataTable->GetName());
|
DataTableJson->SetStringField(TEXT("AssetName"), DataTable->GetName());
|
||||||
DataTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
DataTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
||||||
@ -275,13 +480,22 @@ int32 FAssetExporterToJSON::ExportBlueprints(const FString& FolderPath, TArray<T
|
|||||||
|
|
||||||
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
||||||
|
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT("Found %d Blueprints to export"), AssetList.Num());
|
||||||
|
|
||||||
int32 Count = 0;
|
int32 Count = 0;
|
||||||
|
int32 TotalCount = AssetList.Num();
|
||||||
for (const FAssetData& AssetData : AssetList)
|
for (const FAssetData& AssetData : AssetList)
|
||||||
{
|
{
|
||||||
UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
|
UBlueprint* Blueprint = Cast<UBlueprint>(AssetData.GetAsset());
|
||||||
if (!Blueprint || !Blueprint->GeneratedClass)
|
if (!Blueprint || !Blueprint->GeneratedClass)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Progress logging every 10 assets or for first/last
|
||||||
|
if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT(" Processing Blueprint %d/%d: %s"), Count + 1, TotalCount, *Blueprint->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> BlueprintJson = ExtractBlueprintDetails(Blueprint);
|
TSharedPtr<FJsonObject> BlueprintJson = ExtractBlueprintDetails(Blueprint);
|
||||||
if (BlueprintJson.IsValid())
|
if (BlueprintJson.IsValid())
|
||||||
{
|
{
|
||||||
@ -340,26 +554,125 @@ TArray<TSharedPtr<FJsonValue>> FAssetExporterToJSON::ExtractBlueprintVariables(U
|
|||||||
if (!DefaultObject)
|
if (!DefaultObject)
|
||||||
return VariablesArray;
|
return VariablesArray;
|
||||||
|
|
||||||
// Iterate through all properties
|
// CRITICAL: Export Blueprint custom variables (NewVariables)
|
||||||
for (TFieldIterator<FProperty> PropIt(Blueprint->GeneratedClass, EFieldIteratorFlags::ExcludeSuper); PropIt; ++PropIt)
|
// This is the most important part for Blueprint analysis
|
||||||
|
for (const FBPVariableDescription& Variable : Blueprint->NewVariables)
|
||||||
|
{
|
||||||
|
TSharedPtr<FJsonObject> VarJson = MakeShareable(new FJsonObject);
|
||||||
|
VarJson->SetStringField(TEXT("Name"), Variable.VarName.ToString());
|
||||||
|
VarJson->SetStringField(TEXT("VarGuid"), Variable.VarGuid.ToString());
|
||||||
|
|
||||||
|
// Export variable type information
|
||||||
|
VarJson->SetStringField(TEXT("Category"), Variable.VarType.PinCategory.ToString());
|
||||||
|
if (Variable.VarType.PinSubCategoryObject.IsValid())
|
||||||
|
{
|
||||||
|
VarJson->SetStringField(TEXT("SubCategoryObject"), Variable.VarType.PinSubCategoryObject->GetName());
|
||||||
|
}
|
||||||
|
if (!Variable.VarType.PinSubCategory.IsNone())
|
||||||
|
{
|
||||||
|
VarJson->SetStringField(TEXT("SubCategory"), Variable.VarType.PinSubCategory.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Container type (Array, Set, Map)
|
||||||
|
if (Variable.VarType.ContainerType != EPinContainerType::None)
|
||||||
|
{
|
||||||
|
FString ContainerTypeStr;
|
||||||
|
switch (Variable.VarType.ContainerType)
|
||||||
|
{
|
||||||
|
case EPinContainerType::Array:
|
||||||
|
ContainerTypeStr = TEXT("Array");
|
||||||
|
break;
|
||||||
|
case EPinContainerType::Set:
|
||||||
|
ContainerTypeStr = TEXT("Set");
|
||||||
|
break;
|
||||||
|
case EPinContainerType::Map:
|
||||||
|
ContainerTypeStr = TEXT("Map");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
VarJson->SetStringField(TEXT("ContainerType"), ContainerTypeStr);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export default value
|
||||||
|
VarJson->SetStringField(TEXT("DefaultValue"), Variable.DefaultValue);
|
||||||
|
|
||||||
|
// Export property flags
|
||||||
|
VarJson->SetBoolField(TEXT("IsEditable"), (Variable.PropertyFlags & CPF_Edit) != 0);
|
||||||
|
VarJson->SetBoolField(TEXT("IsBlueprintVisible"), (Variable.PropertyFlags & CPF_BlueprintVisible) != 0);
|
||||||
|
VarJson->SetBoolField(TEXT("IsBlueprintReadOnly"), (Variable.PropertyFlags & CPF_BlueprintReadOnly) != 0);
|
||||||
|
VarJson->SetBoolField(TEXT("IsExposedOnSpawn"), (Variable.PropertyFlags & CPF_ExposeOnSpawn) != 0);
|
||||||
|
VarJson->SetBoolField(TEXT("IsInstanceEditable"), (Variable.PropertyFlags & CPF_DisableEditOnInstance) == 0);
|
||||||
|
|
||||||
|
// Export category
|
||||||
|
if (!Variable.Category.IsEmpty())
|
||||||
|
{
|
||||||
|
VarJson->SetStringField(TEXT("CategoryName"), Variable.Category.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export replication
|
||||||
|
if (Variable.RepNotifyFunc != NAME_None)
|
||||||
|
{
|
||||||
|
VarJson->SetStringField(TEXT("RepNotifyFunc"), Variable.RepNotifyFunc.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export metadata
|
||||||
|
if (Variable.MetaDataArray.Num() > 0)
|
||||||
|
{
|
||||||
|
TSharedPtr<FJsonObject> MetaDataJson = MakeShareable(new FJsonObject);
|
||||||
|
|
||||||
|
for (const FBPVariableMetaDataEntry& MetaDataEntry : Variable.MetaDataArray)
|
||||||
|
{
|
||||||
|
MetaDataJson->SetStringField(MetaDataEntry.DataKey.ToString(), MetaDataEntry.DataValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MetaDataJson->Values.Num() > 0)
|
||||||
|
{
|
||||||
|
VarJson->SetObjectField(TEXT("MetaData"), MetaDataJson);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VarJson->SetStringField(TEXT("Source"), TEXT("Blueprint"));
|
||||||
|
VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract parent class properties with Category = "WorldStalker"
|
||||||
|
// This captures important C++ properties like ActivationOrderGroup
|
||||||
|
for (TFieldIterator<FProperty> PropIt(Blueprint->GeneratedClass); PropIt; ++PropIt)
|
||||||
{
|
{
|
||||||
FProperty* Property = *PropIt;
|
FProperty* Property = *PropIt;
|
||||||
if (!Property)
|
if (!Property)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Check if this property has Category metadata set to "WorldStalker"
|
||||||
|
const FString* CategoryMeta = Property->FindMetaData(TEXT("Category"));
|
||||||
|
if (!CategoryMeta || !CategoryMeta->Equals(TEXT("WorldStalker")))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// This is a WorldStalker category property - export it!
|
||||||
TSharedPtr<FJsonObject> VarJson = MakeShareable(new FJsonObject);
|
TSharedPtr<FJsonObject> VarJson = MakeShareable(new FJsonObject);
|
||||||
VarJson->SetStringField(TEXT("Name"), Property->GetName());
|
VarJson->SetStringField(TEXT("Name"), Property->GetName());
|
||||||
VarJson->SetStringField(TEXT("Type"), Property->GetCPPType());
|
VarJson->SetStringField(TEXT("Type"), Property->GetCPPType());
|
||||||
|
|
||||||
// Get default value
|
// Get default value from CDO
|
||||||
FString DefaultValue;
|
FString DefaultValue;
|
||||||
const void* ValuePtr = Property->ContainerPtrToValuePtr<void>(DefaultObject);
|
const void* ValuePtr = Property->ContainerPtrToValuePtr<void>(DefaultObject);
|
||||||
Property->ExportTextItem_Direct(DefaultValue, ValuePtr, nullptr, nullptr, PPF_None);
|
Property->ExportTextItem_Direct(DefaultValue, ValuePtr, nullptr, nullptr, PPF_None);
|
||||||
VarJson->SetStringField(TEXT("DefaultValue"), DefaultValue);
|
VarJson->SetStringField(TEXT("DefaultValue"), DefaultValue);
|
||||||
|
|
||||||
// Additional metadata
|
// Property flags
|
||||||
VarJson->SetBoolField(TEXT("IsEditable"), Property->HasAnyPropertyFlags(CPF_Edit));
|
VarJson->SetBoolField(TEXT("IsEditable"), Property->HasAnyPropertyFlags(CPF_Edit));
|
||||||
VarJson->SetBoolField(TEXT("IsBlueprintVisible"), Property->HasAnyPropertyFlags(CPF_BlueprintVisible));
|
VarJson->SetBoolField(TEXT("IsBlueprintVisible"), Property->HasAnyPropertyFlags(CPF_BlueprintVisible));
|
||||||
|
VarJson->SetBoolField(TEXT("IsBlueprintReadOnly"), Property->HasAnyPropertyFlags(CPF_BlueprintReadOnly));
|
||||||
|
VarJson->SetBoolField(TEXT("IsEditDefaultsOnly"), Property->HasAnyPropertyFlags(CPF_DisableEditOnInstance));
|
||||||
|
|
||||||
|
// Category and source
|
||||||
|
VarJson->SetStringField(TEXT("CategoryName"), TEXT("WorldStalker"));
|
||||||
|
VarJson->SetStringField(TEXT("Source"), TEXT("C++ParentClass"));
|
||||||
|
|
||||||
|
// Get owning class name for clarity
|
||||||
|
if (Property->GetOwnerClass())
|
||||||
|
{
|
||||||
|
VarJson->SetStringField(TEXT("OwnerClass"), Property->GetOwnerClass()->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson)));
|
VariablesArray.Add(MakeShareable(new FJsonValueObject(VarJson)));
|
||||||
}
|
}
|
||||||
@ -598,12 +911,17 @@ int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray
|
|||||||
|
|
||||||
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
||||||
|
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT("Found %d AnimMontages to export"), AssetList.Num());
|
||||||
|
|
||||||
// Track exported assets to avoid duplicates
|
// Track exported assets to avoid duplicates
|
||||||
TSet<FString> ExportedPaths;
|
TSet<FString> ExportedPaths;
|
||||||
|
|
||||||
int32 Count = 0;
|
int32 Count = 0;
|
||||||
|
int32 TotalCount = AssetList.Num();
|
||||||
|
int32 ProcessedCount = 0;
|
||||||
for (const FAssetData& AssetData : AssetList)
|
for (const FAssetData& AssetData : AssetList)
|
||||||
{
|
{
|
||||||
|
ProcessedCount++;
|
||||||
FString AssetPath = AssetData.GetObjectPathString();
|
FString AssetPath = AssetData.GetObjectPathString();
|
||||||
|
|
||||||
// Skip if already exported
|
// Skip if already exported
|
||||||
@ -617,6 +935,12 @@ int32 FAssetExporterToJSON::ExportAnimMontages(const FString& FolderPath, TArray
|
|||||||
if (!AnimMontage)
|
if (!AnimMontage)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Progress logging every 10 assets or for first/last
|
||||||
|
if (Count == 0 || Count % 10 == 0 || ProcessedCount == TotalCount)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT(" Processing AnimMontage %d/%d: %s"), ProcessedCount, TotalCount, *AnimMontage->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
ExportedPaths.Add(AssetPath);
|
ExportedPaths.Add(AssetPath);
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> MontageJson = MakeShareable(new FJsonObject);
|
TSharedPtr<FJsonObject> MontageJson = MakeShareable(new FJsonObject);
|
||||||
@ -785,13 +1109,22 @@ int32 FAssetExporterToJSON::ExportCurveTables(const FString& FolderPath, TArray<
|
|||||||
|
|
||||||
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
AssetRegistryModule.Get().GetAssets(Filter, AssetList);
|
||||||
|
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT("Found %d CurveTables to export"), AssetList.Num());
|
||||||
|
|
||||||
int32 Count = 0;
|
int32 Count = 0;
|
||||||
|
int32 TotalCount = AssetList.Num();
|
||||||
for (const FAssetData& AssetData : AssetList)
|
for (const FAssetData& AssetData : AssetList)
|
||||||
{
|
{
|
||||||
UCurveTable* CurveTable = Cast<UCurveTable>(AssetData.GetAsset());
|
UCurveTable* CurveTable = Cast<UCurveTable>(AssetData.GetAsset());
|
||||||
if (!CurveTable)
|
if (!CurveTable)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
|
// Progress logging every 10 assets or for first/last
|
||||||
|
if (Count == 0 || Count % 10 == 0 || Count == TotalCount - 1)
|
||||||
|
{
|
||||||
|
UE_LOG(LogAssetExporter, Log, TEXT(" Processing CurveTable %d/%d: %s"), Count + 1, TotalCount, *CurveTable->GetName());
|
||||||
|
}
|
||||||
|
|
||||||
TSharedPtr<FJsonObject> CurveTableJson = MakeShareable(new FJsonObject);
|
TSharedPtr<FJsonObject> CurveTableJson = MakeShareable(new FJsonObject);
|
||||||
CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName());
|
CurveTableJson->SetStringField(TEXT("AssetName"), CurveTable->GetName());
|
||||||
CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
CurveTableJson->SetStringField(TEXT("AssetPath"), AssetData.GetObjectPathString());
|
||||||
|
|||||||
@ -18,6 +18,12 @@ public:
|
|||||||
*/
|
*/
|
||||||
static void ExportAssetsToJSON();
|
static void ExportAssetsToJSON();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Export a single asset to JSON (for right-click context menu)
|
||||||
|
* @param AssetData - Asset to export
|
||||||
|
*/
|
||||||
|
static void ExportSingleAssetToJSON(const struct FAssetData& AssetData);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
/**
|
/**
|
||||||
* Export all DataTables from the specified path to JSON
|
* Export all DataTables from the specified path to JSON
|
||||||
|
|||||||
353
README.md
353
README.md
@ -20,13 +20,53 @@
|
|||||||
- **다중 폴더 지원**: 한 번의 클릭으로 여러 폴더 경로에서 익스포트
|
- **다중 폴더 지원**: 한 번의 클릭으로 여러 폴더 경로에서 익스포트
|
||||||
- **에셋 타입 필터링**: 설정을 통해 특정 에셋 타입 활성화/비활성화
|
- **에셋 타입 필터링**: 설정을 통해 특정 에셋 타입 활성화/비활성화
|
||||||
|
|
||||||
|
### 두 가지 익스포트 방식
|
||||||
|
|
||||||
|
#### 1. 대량 익스포트 (Tools 메뉴)
|
||||||
|
- 설정된 모든 폴더의 에셋을 한 번에 익스포트
|
||||||
|
- 프로젝트 전체 밸런스 분석에 적합
|
||||||
|
- `Tools → WorldStalker → Export Assets to JSON`
|
||||||
|
|
||||||
|
#### 2. 단일 에셋 익스포트 (우클릭 메뉴)
|
||||||
|
- Content Browser에서 개별 에셋 우클릭 후 즉시 익스포트
|
||||||
|
- 빠른 테스트 및 반복 작업에 적합
|
||||||
|
- 타임스탬프 파일명 자동 생성
|
||||||
|
|
||||||
### 포괄적인 데이터 추출
|
### 포괄적인 데이터 추출
|
||||||
|
|
||||||
#### Blueprint 익스포트
|
#### Blueprint 익스포트
|
||||||
- 타입과 기본값을 포함한 변수
|
|
||||||
- 입력/출력을 포함한 함수
|
**변수 추출 - 3가지 소스**:
|
||||||
|
|
||||||
|
1. **Blueprint 커스텀 변수** (`Blueprint->NewVariables`)
|
||||||
|
- Blueprint 에디터에서 추가한 모든 커스텀 변수
|
||||||
|
- 변수 GUID, 타입 정보 (Category, SubCategory, ContainerType)
|
||||||
|
- Property flags (IsEditable, IsBlueprintVisible, IsExposedOnSpawn 등)
|
||||||
|
- 카테고리명 및 전체 메타데이터
|
||||||
|
- Replication 설정
|
||||||
|
|
||||||
|
2. **C++ 부모 클래스 프로퍼티** (`Category = "WorldStalker"`)
|
||||||
|
- ✨ **NEW**: C++ 부모 클래스에서 정의된 중요 프로퍼티 추출
|
||||||
|
- **ActivationOrderGroup** - 스킬 실행 순서 그룹
|
||||||
|
- **bDisableOrderGroup** - 순서 그룹 비활성화 여부
|
||||||
|
- **bCanBeCancel** - 취소 가능 여부
|
||||||
|
- **ActivationTrigger** - 발동 트리거 타입
|
||||||
|
- **bStopAutoTargetWhenEndAbility** - 어빌리티 종료 시 자동 타겟 중지
|
||||||
|
- Source 필드로 "C++ParentClass" 표시
|
||||||
|
- OwnerClass 필드로 정의된 C++ 클래스 명시
|
||||||
|
|
||||||
|
3. **컴파일된 클래스 프로퍼티**
|
||||||
|
- 부모 클래스에서 상속된 기타 프로퍼티
|
||||||
|
|
||||||
|
**함수**:
|
||||||
|
- 입력/출력 파라미터를 포함한 함수 목록
|
||||||
|
- FunctionGraphs에서 추출
|
||||||
|
|
||||||
|
**컴포넌트**:
|
||||||
- 컴포넌트 계층 구조
|
- 컴포넌트 계층 구조
|
||||||
- **이벤트 그래프**: 다음을 포함한 완전한 노드 그래프
|
- SimpleConstructionScript에서 추출
|
||||||
|
|
||||||
|
**이벤트 그래프**: 다음을 포함한 완전한 노드 그래프
|
||||||
- 노드 타입, 제목, 위치, 코멘트
|
- 노드 타입, 제목, 위치, 코멘트
|
||||||
- 핀 타입, 방향, 기본값
|
- 핀 타입, 방향, 기본값
|
||||||
- 핀 간 연결 그래프
|
- 핀 간 연결 그래프
|
||||||
@ -46,6 +86,12 @@
|
|||||||
- 시간, 값, 탄젠트 정보를 포함한 키 데이터
|
- 시간, 값, 탄젠트 정보를 포함한 키 데이터
|
||||||
- 커브 보간 모드
|
- 커브 보간 모드
|
||||||
|
|
||||||
|
### 진행 상황 로깅
|
||||||
|
- ✨ **NEW**: 10개 에셋마다 진행 상황 로그 출력
|
||||||
|
- Output Log에서 실시간 진행 상황 확인 가능
|
||||||
|
- 에디터가 먹통처럼 보이는 현상 방지
|
||||||
|
- 로그 카테고리: `LogAssetExporter`
|
||||||
|
|
||||||
### 출력 관리
|
### 출력 관리
|
||||||
- **타임스탬프 익스포트**: 익스포트 히스토리 보관을 위한 선택적 타임스탬프 하위 폴더
|
- **타임스탬프 익스포트**: 익스포트 히스토리 보관을 위한 선택적 타임스탬프 하위 폴더
|
||||||
- **타입별 파일 분리**: 에셋 타입별 개별 JSON 파일 (DataTable.json, Blueprint.json 등)
|
- **타입별 파일 분리**: 에셋 타입별 개별 JSON 파일 (DataTable.json, Blueprint.json 등)
|
||||||
@ -87,7 +133,8 @@ PrivateDependencyModuleNames.AddRange(new string[] {
|
|||||||
"Slate",
|
"Slate",
|
||||||
"SlateCore",
|
"SlateCore",
|
||||||
"ToolMenus",
|
"ToolMenus",
|
||||||
"DeveloperSettings"
|
"DeveloperSettings",
|
||||||
|
"ContentBrowser" // 우클릭 메뉴용
|
||||||
});
|
});
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -147,11 +194,49 @@ DataTables
|
|||||||
|
|
||||||
## 사용 방법
|
## 사용 방법
|
||||||
|
|
||||||
### 빠른 익스포트
|
### 방법 1: 대량 익스포트 (Tools 메뉴)
|
||||||
|
|
||||||
|
**전체 프로젝트 밸런스 분석용**
|
||||||
|
|
||||||
1. 언리얼 에디터 열기
|
1. 언리얼 에디터 열기
|
||||||
2. 다음으로 이동: `툴 → WorldStalker → Export Assets to JSON`
|
2. 다음으로 이동: `Tools → WorldStalker → Export Assets to JSON`
|
||||||
3. 익스포트 완료 대기 (알림 대화상자가 표시됨)
|
3. Output Log에서 진행 상황 확인 (`Window → Developer Tools → Output Log`)
|
||||||
|
- 로그 필터: `LogAssetExporter`
|
||||||
|
4. 익스포트 완료 대기 (알림 대화상자가 표시됨)
|
||||||
|
|
||||||
|
**Output Log 예시**:
|
||||||
|
```
|
||||||
|
LogAssetExporter: Starting Asset Export to JSON process...
|
||||||
|
LogAssetExporter: Export path: /Game/Blueprints/Abilities
|
||||||
|
LogAssetExporter: Found 45 Blueprints to export
|
||||||
|
LogAssetExporter: Processing Blueprint 1/45: GA_Skill_Cazimord_Flash
|
||||||
|
LogAssetExporter: Processing Blueprint 10/45: GA_Skill_Fireball
|
||||||
|
LogAssetExporter: Processing Blueprint 20/45: GA_Skill_IceSpear
|
||||||
|
...
|
||||||
|
LogAssetExporter: Exported 45 Blueprints
|
||||||
|
LogAssetExporter: Export completed! Total assets exported: 168
|
||||||
|
```
|
||||||
|
|
||||||
|
### 방법 2: 단일 에셋 익스포트 (우클릭)
|
||||||
|
|
||||||
|
**빠른 테스트 및 반복 작업용**
|
||||||
|
|
||||||
|
1. Content Browser에서 익스포트할 에셋 찾기
|
||||||
|
2. 에셋 우클릭
|
||||||
|
3. **"Export to JSON"** 선택
|
||||||
|
4. 즉시 익스포트 완료 (확인 대화상자 표시)
|
||||||
|
|
||||||
|
**지원 에셋 타입**:
|
||||||
|
- DataTable
|
||||||
|
- Blueprint
|
||||||
|
- AnimMontage
|
||||||
|
- CurveTable
|
||||||
|
|
||||||
|
**출력 파일명 형식**:
|
||||||
|
```
|
||||||
|
Blueprint_GA_Skill_Cazimord_Flash_20251023_143205.json
|
||||||
|
AnimMontage_AM_Attack_Combo_20251023_143210.json
|
||||||
|
```
|
||||||
|
|
||||||
### 익스포트 출력
|
### 익스포트 출력
|
||||||
|
|
||||||
@ -172,90 +257,185 @@ Content/
|
|||||||
|
|
||||||
#### DataTable.json
|
#### DataTable.json
|
||||||
```json
|
```json
|
||||||
[
|
{
|
||||||
|
"ExportedAt": "2025-10-23 14:32:05",
|
||||||
|
"TotalCount": 10,
|
||||||
|
"Assets": [
|
||||||
{
|
{
|
||||||
"AssetName": "DT_CharacterStats",
|
"AssetName": "DT_CharacterStats",
|
||||||
"AssetPath": "/Game/DataTables/DT_CharacterStats",
|
"AssetPath": "/Game/DataTables/DT_CharacterStats",
|
||||||
"RowCount": 10,
|
"RowStructure": "FCharacterStats",
|
||||||
"Columns": ["Name", "Health", "Attack", "Defense"],
|
"Rows": [
|
||||||
"Rows": {
|
{
|
||||||
"Warrior": {
|
"RowName": "Warrior",
|
||||||
|
"Data": {
|
||||||
"Name": "Warrior",
|
"Name": "Warrior",
|
||||||
"Health": "1000",
|
"Health": 1000.0,
|
||||||
"Attack": "150",
|
"Attack": 150.0,
|
||||||
"Defense": "100"
|
"Defense": 100.0
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Blueprint.json
|
#### Blueprint.json - 변수 섹션
|
||||||
|
|
||||||
```json
|
```json
|
||||||
[
|
|
||||||
{
|
{
|
||||||
"AssetName": "BP_Enemy_Goblin",
|
"AssetName": "GA_Skill_Cazimord_Flash",
|
||||||
"AssetPath": "/Game/Blueprints/Enemy/BP_Enemy_Goblin",
|
"AssetPath": "/Game/Blueprints/Abilities/GA_Skill_Cazimord_Flash",
|
||||||
"ParentClass": "WSCharacterBase",
|
"ParentClass": "WSGameplayAbility",
|
||||||
"Variables": [
|
"Variables": [
|
||||||
{
|
{
|
||||||
"Name": "MaxHealth",
|
"Name": "ActivationOrderGroup",
|
||||||
"Type": "float",
|
"Type": "uint8",
|
||||||
"DefaultValue": "500.0"
|
"DefaultValue": "1",
|
||||||
|
"CategoryName": "WorldStalker",
|
||||||
|
"Source": "C++ParentClass",
|
||||||
|
"OwnerClass": "WSGameplayAbility",
|
||||||
|
"IsEditable": true,
|
||||||
|
"IsBlueprintVisible": true,
|
||||||
|
"IsBlueprintReadOnly": false,
|
||||||
|
"IsEditDefaultsOnly": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "bDisableOrderGroup",
|
||||||
|
"Type": "bool",
|
||||||
|
"DefaultValue": "False",
|
||||||
|
"CategoryName": "WorldStalker",
|
||||||
|
"Source": "C++ParentClass",
|
||||||
|
"OwnerClass": "WSGameplayAbility"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"Name": "CustomDamageMultiplier",
|
||||||
|
"VarGuid": "A1B2C3D4-E5F6-7890-ABCD-EF1234567890",
|
||||||
|
"Category": "float",
|
||||||
|
"DefaultValue": "1.5",
|
||||||
|
"Source": "Blueprint",
|
||||||
|
"IsEditable": true,
|
||||||
|
"IsBlueprintVisible": true,
|
||||||
|
"CategoryName": "Combat",
|
||||||
|
"MetaData": {
|
||||||
|
"DisplayName": "데미지 배율",
|
||||||
|
"Tooltip": "스킬 데미지 최종 배율"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"EventGraphs": [
|
"Functions": [...],
|
||||||
{
|
"Components": [...],
|
||||||
"GraphName": "EventGraph",
|
"EventGraphs": [...]
|
||||||
"Nodes": [
|
|
||||||
{
|
|
||||||
"NodeTitle": "Event BeginPlay",
|
|
||||||
"NodeClass": "K2Node_Event",
|
|
||||||
"Pins": [...]
|
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"Connections": [
|
|
||||||
{
|
|
||||||
"SourceNode": "Event BeginPlay",
|
|
||||||
"SourcePin": "then",
|
|
||||||
"TargetNode": "Set Max Health",
|
|
||||||
"TargetPin": "execute"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**변수 Source 타입 설명**:
|
||||||
|
- `"Blueprint"`: Blueprint 에디터에서 추가한 커스텀 변수
|
||||||
|
- `"C++ParentClass"`: C++ 부모 클래스에서 정의된 프로퍼티 (Category = "WorldStalker")
|
||||||
|
- `"CompiledClass"`: 기타 컴파일된 클래스 프로퍼티
|
||||||
|
|
||||||
#### AnimMontage.json
|
#### AnimMontage.json
|
||||||
```json
|
```json
|
||||||
[
|
|
||||||
{
|
{
|
||||||
"AssetName": "AM_Attack_Combo",
|
"AssetName": "AM_Attack_Combo",
|
||||||
"AssetPath": "/Game/Animations/AM_Attack_Combo",
|
"AssetPath": "/Game/Animations/AM_Attack_Combo",
|
||||||
"SequenceLength": 2.5,
|
"SequenceLength": 2.5,
|
||||||
|
"RateScale": 1.0,
|
||||||
"Sections": [
|
"Sections": [
|
||||||
{
|
{
|
||||||
"SectionName": "Combo1",
|
"SectionName": "Combo1",
|
||||||
"StartTime": 0.0,
|
"StartTime": 0.0,
|
||||||
"EndTime": 0.8
|
"NextSectionName": "Combo2"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"Notifies": [
|
"AnimNotifies": [
|
||||||
{
|
{
|
||||||
"NotifyName": "DamageNotify",
|
"NotifyName": "DealDamage",
|
||||||
"TriggerTime": 0.5,
|
"TriggerTime": 0.5,
|
||||||
"NotifyType": "AnimNotify",
|
"Duration": 0.0,
|
||||||
|
"NotifyType": "Notify",
|
||||||
|
"NotifyClass": "ANS_DealDamage",
|
||||||
"CustomProperties": {
|
"CustomProperties": {
|
||||||
"DamageMultiplier": "1.5",
|
"DamageAmount": "50.0",
|
||||||
"HitboxSize": "100.0"
|
"DamageType": "Physical"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"SlotAnimTracks": [...]
|
"SlotAnimTracks": [
|
||||||
|
{
|
||||||
|
"SlotName": "DefaultSlot",
|
||||||
|
"AnimSegments": [
|
||||||
|
{
|
||||||
|
"AnimReference": "AS_Attack1",
|
||||||
|
"AnimPath": "/Game/Animation/Sequences/AS_Attack1",
|
||||||
|
"StartPos": 0.0,
|
||||||
|
"AnimStartTime": 0.0,
|
||||||
|
"AnimEndTime": 1.0,
|
||||||
|
"AnimPlayRate": 1.0,
|
||||||
|
"LoopingCount": 1
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"BlendInTime": 0.25,
|
||||||
|
"BlendOutTime": 0.25,
|
||||||
|
"BlendModeIn": "Standard",
|
||||||
|
"BlendModeOut": "Standard"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 전투 밸런스 분석 활용 예시
|
||||||
|
|
||||||
|
### 워크플로우
|
||||||
|
|
||||||
|
1. **전체 스킬 익스포트**
|
||||||
|
```
|
||||||
|
Project Settings → Asset Export to JSON
|
||||||
|
Export Folder Paths에 "/Game/Blueprints/Abilities" 추가
|
||||||
|
Tools → Export Assets to JSON 실행
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **익스포트된 JSON 분석**
|
||||||
|
```python
|
||||||
|
import json
|
||||||
|
|
||||||
|
with open('Content/Exports/Blueprint.json', encoding='utf-8') as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
# ActivationOrderGroup 분석
|
||||||
|
for asset in data['Assets']:
|
||||||
|
if 'GA_Skill' in asset['AssetName']:
|
||||||
|
variables = asset['Variables']
|
||||||
|
|
||||||
|
# C++ 부모 클래스 프로퍼티 찾기
|
||||||
|
for var in variables:
|
||||||
|
if var['Source'] == 'C++ParentClass':
|
||||||
|
if var['Name'] == 'ActivationOrderGroup':
|
||||||
|
print(f"{asset['AssetName']}: Order Group = {var['DefaultValue']}")
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **특정 스킬 수정 후 빠른 재분석**
|
||||||
|
```
|
||||||
|
1. GA_Skill_Cazimord_Flash 블루프린트 수정
|
||||||
|
2. Content Browser에서 우클릭 → Export to JSON
|
||||||
|
3. 생성된 단일 JSON 파일로 LLM 분석
|
||||||
|
4. 밸런스 피드백 받기
|
||||||
|
5. 반복
|
||||||
|
```
|
||||||
|
|
||||||
|
### LLM 프롬프트 예시
|
||||||
|
|
||||||
|
```
|
||||||
|
다음 스킬 Blueprint JSON을 분석해주세요:
|
||||||
|
|
||||||
|
[JSON 데이터 붙여넣기]
|
||||||
|
|
||||||
|
분석 포인트:
|
||||||
|
1. ActivationOrderGroup 값이 적절한가?
|
||||||
|
2. 같은 그룹의 다른 스킬들과 밸런스가 맞는가?
|
||||||
|
3. CustomProperties의 데미지 배율이 적정한가?
|
||||||
|
4. AnimMontage의 타이밍과 DealDamage 노티파이 시점이 일치하는가?
|
||||||
```
|
```
|
||||||
|
|
||||||
## 문제 해결
|
## 문제 해결
|
||||||
@ -265,11 +445,34 @@ Content/
|
|||||||
**문제**: 익스포트가 완료되었지만 JSON 파일이 생성되지 않음
|
**문제**: 익스포트가 완료되었지만 JSON 파일이 생성되지 않음
|
||||||
**해결**: 익스포트 경로에 활성화된 타입의 에셋이 포함되어 있는지 확인
|
**해결**: 익스포트 경로에 활성화된 타입의 에셋이 포함되어 있는지 확인
|
||||||
|
|
||||||
|
### 우클릭 메뉴가 나타나지 않음
|
||||||
|
|
||||||
|
**문제**: Content Browser에서 에셋 우클릭 시 "Export to JSON" 메뉴가 없음
|
||||||
|
**해결**:
|
||||||
|
- 지원되는 에셋 타입인지 확인 (DataTable, Blueprint, AnimMontage, CurveTable)
|
||||||
|
- 단일 에셋만 선택했는지 확인 (다중 선택 시 메뉴 비활성화)
|
||||||
|
- 에디터 재시작 시도
|
||||||
|
|
||||||
|
### C++ 프로퍼티가 익스포트되지 않음
|
||||||
|
|
||||||
|
**문제**: ActivationOrderGroup 같은 C++ 프로퍼티가 JSON에 없음
|
||||||
|
**해결**:
|
||||||
|
- Property에 `Category = "WorldStalker"` 메타데이터가 있는지 확인
|
||||||
|
- C++ 헤더 파일에서 `UPROPERTY(EditDefaultsOnly, Category = "WorldStalker")` 선언 확인
|
||||||
|
- 최신 코드로 빌드되었는지 확인
|
||||||
|
|
||||||
### 출력에 중복 에셋
|
### 출력에 중복 에셋
|
||||||
|
|
||||||
**문제**: 동일한 에셋이 JSON에 여러 번 나타남
|
**문제**: 동일한 에셋이 JSON에 여러 번 나타남
|
||||||
**해결**: 이제 중복 검출로 방지됨 - 설정에서 겹치는 폴더 경로 확인
|
**해결**: 이제 중복 검출로 방지됨 - 설정에서 겹치는 폴더 경로 확인
|
||||||
|
|
||||||
|
### 진행 상황이 보이지 않음
|
||||||
|
|
||||||
|
**문제**: 익스포트 중 에디터가 먹통처럼 보임
|
||||||
|
**해결**: Output Log 열기 (`Window → Developer Tools → Output Log`)
|
||||||
|
- 필터에 `LogAssetExporter` 입력
|
||||||
|
- 10개 에셋마다 진행 상황 로그 확인 가능
|
||||||
|
|
||||||
### 통합 후 빌드 오류
|
### 통합 후 빌드 오류
|
||||||
|
|
||||||
**문제**: 누락된 헤더에 대한 컴파일 오류
|
**문제**: 누락된 헤더에 대한 컴파일 오류
|
||||||
@ -297,11 +500,41 @@ Content/
|
|||||||
- **UnrealEd** - 에디터 통합
|
- **UnrealEd** - 에디터 통합
|
||||||
- **ToolMenus** - 메뉴 확장 시스템
|
- **ToolMenus** - 메뉴 확장 시스템
|
||||||
- **DeveloperSettings** - 프로젝트 설정 통합
|
- **DeveloperSettings** - 프로젝트 설정 통합
|
||||||
|
- **ContentBrowser** - 우클릭 컨텍스트 메뉴
|
||||||
|
|
||||||
### 성능 특성
|
### 성능 특성
|
||||||
- **재귀 검색**: 설정된 경로의 모든 하위 폴더 검색
|
- **재귀 검색**: 설정된 경로의 모든 하위 폴더 검색
|
||||||
- **메모리 사용량**: 메모리 사용량을 최소화하기 위해 에셋을 하나씩 로드
|
- **메모리 사용량**: 메모리 사용량을 최소화하기 위해 에셋을 하나씩 로드
|
||||||
- **익스포트 속도**: 에셋 복잡도에 따라 초당 약 10-50개 에셋
|
- **익스포트 속도**: 에셋 복잡도에 따라 초당 약 10-50개 에셋
|
||||||
|
- **진행 상황**: 10개 에셋마다 로그 출력으로 진행 상황 실시간 확인
|
||||||
|
|
||||||
|
### Blueprint 변수 추출 상세
|
||||||
|
|
||||||
|
시스템은 3가지 소스에서 변수를 추출합니다:
|
||||||
|
|
||||||
|
**1. Blueprint 커스텀 변수 (`Blueprint->NewVariables`)**
|
||||||
|
```cpp
|
||||||
|
for (const FBPVariableDescription& Variable : Blueprint->NewVariables)
|
||||||
|
{
|
||||||
|
// VarName, VarGuid, VarType, DefaultValue 추출
|
||||||
|
// PropertyFlags, Category, MetaData 추출
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**2. C++ 부모 클래스 프로퍼티 (Category = "WorldStalker")**
|
||||||
|
```cpp
|
||||||
|
for (TFieldIterator<FProperty> PropIt(Blueprint->GeneratedClass); PropIt; ++PropIt)
|
||||||
|
{
|
||||||
|
const FString* CategoryMeta = PropIt->FindMetaData(TEXT("Category"));
|
||||||
|
if (CategoryMeta && CategoryMeta->Equals(TEXT("WorldStalker")))
|
||||||
|
{
|
||||||
|
// ActivationOrderGroup 등 중요 프로퍼티 추출
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**3. 기타 컴파일된 클래스 프로퍼티**
|
||||||
|
- 부모 클래스에서 상속된 기타 프로퍼티
|
||||||
|
|
||||||
### Blueprint 이벤트 그래프 추출
|
### Blueprint 이벤트 그래프 추출
|
||||||
시스템은 Blueprint 그래프 구조를 순회하여 다음을 추출합니다:
|
시스템은 Blueprint 그래프 구조를 순회하여 다음을 추출합니다:
|
||||||
@ -324,16 +557,30 @@ Content/
|
|||||||
2. **바이너리 에셋**: 바이너리 데이터 (메시, 텍스처, 오디오)는 익스포트할 수 없음
|
2. **바이너리 에셋**: 바이너리 데이터 (메시, 텍스처, 오디오)는 익스포트할 수 없음
|
||||||
3. **복잡한 프로퍼티 타입**: 일부 복잡한 UObject 프로퍼티는 객체 참조로만 익스포트될 수 있음
|
3. **복잡한 프로퍼티 타입**: 일부 복잡한 UObject 프로퍼티는 객체 참조로만 익스포트될 수 있음
|
||||||
4. **대형 Blueprint**: 수천 개의 노드가 있는 매우 큰 블루프린트는 익스포트에 상당한 시간이 걸릴 수 있음
|
4. **대형 Blueprint**: 수천 개의 노드가 있는 매우 큰 블루프린트는 익스포트에 상당한 시간이 걸릴 수 있음
|
||||||
|
5. **카테고리 필터**: 현재 "WorldStalker" 카테고리만 C++ 프로퍼티를 추출 (필요시 코드 수정으로 다른 카테고리 추가 가능)
|
||||||
|
|
||||||
## 개발 히스토리
|
## 개발 히스토리
|
||||||
|
|
||||||
|
### Version 1.0 (2025-10-23)
|
||||||
|
|
||||||
**초기 릴리스**: 포괄적인 에셋 추출 기능을 갖춘 INI 기반 설정 시스템
|
**초기 릴리스**: 포괄적인 에셋 추출 기능을 갖춘 INI 기반 설정 시스템
|
||||||
|
|
||||||
|
**주요 기능**:
|
||||||
|
- ✅ Blueprint 커스텀 변수 (NewVariables) 완전 추출
|
||||||
|
- ✅ C++ 부모 클래스 프로퍼티 추출 (Category = "WorldStalker")
|
||||||
|
- ActivationOrderGroup 등 중요 프로퍼티 지원
|
||||||
|
- ✅ 우클릭 컨텍스트 메뉴로 단일 에셋 빠른 익스포트
|
||||||
|
- ✅ 진행 상황 로깅 (10개 에셋마다)
|
||||||
|
- ✅ AnimMontage 커스텀 프로퍼티 추출
|
||||||
|
- ✅ 이벤트 그래프 노드 연결 추출
|
||||||
|
- ✅ INI 기반 영구 설정
|
||||||
|
|
||||||
**주요 설계 결정**:
|
**주요 설계 결정**:
|
||||||
- 반복 사용의 편의성을 위해 체크박스 UI 대신 INI 기반 설정 선택
|
- 반복 사용의 편의성을 위해 체크박스 UI 대신 INI 기반 설정 선택
|
||||||
- 버전 관리를 통한 팀 협업을 위한 UDeveloperSettings 통합
|
- 버전 관리를 통한 팀 협업을 위한 UDeveloperSettings 통합
|
||||||
- 겹치는 폴더 경로로부터 중복 익스포트 방지를 위한 중복 검출
|
- 겹치는 폴더 경로로부터 중복 익스포트 방지를 위한 중복 검출
|
||||||
- 사람의 가독성과 LLM 처리를 위한 보기 좋은 JSON 형식
|
- 사람의 가독성과 LLM 처리를 위한 보기 좋은 JSON 형식
|
||||||
|
- C++ 부모 클래스 프로퍼티 추출로 스킬 밸런스 분석 강화
|
||||||
|
|
||||||
## 지원
|
## 지원
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user