2184 lines
85 KiB
C++
2184 lines
85 KiB
C++
|
|
// Copyright Epic Games, Inc. All Rights Reserved.
|
||
|
|
|
||
|
|
#include "UObject/SavePackage/SavePackageUtilities.h"
|
||
|
|
|
||
|
|
#include "Algo/Sort.h"
|
||
|
|
#include "Algo/Unique.h"
|
||
|
|
#include "Blueprint/BlueprintSupport.h"
|
||
|
|
#include "CoreMinimal.h"
|
||
|
|
#include "HAL/FileManager.h"
|
||
|
|
#include "Interfaces/ITargetPlatform.h"
|
||
|
|
#include "Misc/AssetRegistryInterface.h"
|
||
|
|
#include "Misc/ConfigCacheIni.h"
|
||
|
|
#include "Misc/CommandLine.h"
|
||
|
|
#include "Misc/Paths.h"
|
||
|
|
#include "Misc/ScopedSlowTask.h"
|
||
|
|
#include "Serialization/BulkData.h"
|
||
|
|
#include "Serialization/BulkDataManifest.h"
|
||
|
|
#include "Serialization/LargeMemoryWriter.h"
|
||
|
|
#include "UObject/AsyncWorkSequence.h"
|
||
|
|
#include "UObject/Class.h"
|
||
|
|
#include "UObject/GCScopeLock.h"
|
||
|
|
#include "UObject/Linker.h"
|
||
|
|
#include "UObject/LinkerLoad.h"
|
||
|
|
#include "UObject/LinkerSave.h"
|
||
|
|
#include "UObject/Object.h"
|
||
|
|
#include "UObject/ObjectRedirector.h"
|
||
|
|
#include "UObject/Package.h"
|
||
|
|
#include "UObject/SavePackage.h"
|
||
|
|
#include "UObject/UnrealType.h"
|
||
|
|
#include "UObject/UObjectHash.h"
|
||
|
|
#include "UObject/UObjectThreadContext.h"
|
||
|
|
#include "Misc/CookSideEffectCollector.h"
|
||
|
|
|
||
|
|
DEFINE_LOG_CATEGORY(LogSavePackage);
|
||
|
|
|
||
|
|
#if ENABLE_COOK_STATS
|
||
|
|
#include "ProfilingDebugging/ScopedTimers.h"
|
||
|
|
|
||
|
|
int32 FSavePackageStats::NumPackagesSaved = 0;
|
||
|
|
double FSavePackageStats::SavePackageTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::TagPackageExportsPresaveTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::TagPackageExportsTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::FullyLoadLoadersTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::ResetLoadersTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::TagPackageExportsGetObjectsWithOuter = 0.0;
|
||
|
|
double FSavePackageStats::TagPackageExportsGetObjectsWithMarks = 0.0;
|
||
|
|
double FSavePackageStats::SerializeImportsTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::SortExportsSeekfreeInnerTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::SerializeExportsTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::SerializeBulkDataTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::AsyncWriteTimeSec = 0.0;
|
||
|
|
double FSavePackageStats::MBWritten = 0.0;
|
||
|
|
TMap<FName, FArchiveDiffStats> FSavePackageStats::PackageDiffStats;
|
||
|
|
int32 FSavePackageStats::NumberOfDifferentPackages = 0;
|
||
|
|
|
||
|
|
FCookStatsManager::FAutoRegisterCallback FSavePackageStats::RegisterCookStats(FSavePackageStats::AddSavePackageStats);
|
||
|
|
|
||
|
|
void FSavePackageStats::AddSavePackageStats(FCookStatsManager::AddStatFuncRef AddStat)
|
||
|
|
{
|
||
|
|
// Don't use FCookStatsManager::CreateKeyValueArray because there's just too many arguments. Don't need to overburden the compiler here.
|
||
|
|
TArray<FCookStatsManager::StringKeyValue> StatsList;
|
||
|
|
StatsList.Empty(15);
|
||
|
|
#define ADD_COOK_STAT(Name) StatsList.Emplace(TEXT(#Name), LexToString(Name))
|
||
|
|
ADD_COOK_STAT(NumPackagesSaved);
|
||
|
|
ADD_COOK_STAT(SavePackageTimeSec);
|
||
|
|
ADD_COOK_STAT(TagPackageExportsPresaveTimeSec);
|
||
|
|
ADD_COOK_STAT(TagPackageExportsTimeSec);
|
||
|
|
ADD_COOK_STAT(FullyLoadLoadersTimeSec);
|
||
|
|
ADD_COOK_STAT(ResetLoadersTimeSec);
|
||
|
|
ADD_COOK_STAT(TagPackageExportsGetObjectsWithOuter);
|
||
|
|
ADD_COOK_STAT(TagPackageExportsGetObjectsWithMarks);
|
||
|
|
ADD_COOK_STAT(SerializeImportsTimeSec);
|
||
|
|
ADD_COOK_STAT(SortExportsSeekfreeInnerTimeSec);
|
||
|
|
ADD_COOK_STAT(SerializeExportsTimeSec);
|
||
|
|
ADD_COOK_STAT(SerializeBulkDataTimeSec);
|
||
|
|
ADD_COOK_STAT(AsyncWriteTimeSec);
|
||
|
|
ADD_COOK_STAT(MBWritten);
|
||
|
|
|
||
|
|
AddStat(TEXT("Package.Save"), StatsList);
|
||
|
|
|
||
|
|
{
|
||
|
|
PackageDiffStats.ValueSort([](const FArchiveDiffStats& Lhs, const FArchiveDiffStats& Rhs)
|
||
|
|
{
|
||
|
|
return Lhs.NewFileTotalSize > Rhs.NewFileTotalSize;
|
||
|
|
});
|
||
|
|
|
||
|
|
StatsList.Empty(15);
|
||
|
|
for (const TPair<FName, FArchiveDiffStats>& Stat: PackageDiffStats)
|
||
|
|
{
|
||
|
|
StatsList.Emplace(Stat.Key.ToString(), LexToString((double)Stat.Value.NewFileTotalSize / 1024.0 / 1024.0));
|
||
|
|
}
|
||
|
|
|
||
|
|
AddStat(TEXT("Package.DifferentPackagesSizeMBPerAsset"), StatsList);
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
PackageDiffStats.ValueSort([](const FArchiveDiffStats& Lhs, const FArchiveDiffStats& Rhs)
|
||
|
|
{
|
||
|
|
return Lhs.NumDiffs > Rhs.NumDiffs;
|
||
|
|
});
|
||
|
|
|
||
|
|
StatsList.Empty(15);
|
||
|
|
for (const TPair<FName, FArchiveDiffStats>& Stat: PackageDiffStats)
|
||
|
|
{
|
||
|
|
StatsList.Emplace(Stat.Key.ToString(), LexToString(Stat.Value.NumDiffs));
|
||
|
|
}
|
||
|
|
|
||
|
|
AddStat(TEXT("Package.NumberOfDifferencesInPackagesPerAsset"), StatsList);
|
||
|
|
}
|
||
|
|
|
||
|
|
{
|
||
|
|
PackageDiffStats.ValueSort([](const FArchiveDiffStats& Lhs, const FArchiveDiffStats& Rhs)
|
||
|
|
{
|
||
|
|
return Lhs.DiffSize > Rhs.DiffSize;
|
||
|
|
});
|
||
|
|
|
||
|
|
StatsList.Empty(15);
|
||
|
|
for (const TPair<FName, FArchiveDiffStats>& Stat: PackageDiffStats)
|
||
|
|
{
|
||
|
|
StatsList.Emplace(Stat.Key.ToString(), LexToString((double)Stat.Value.DiffSize / 1024.0 / 1024.0));
|
||
|
|
}
|
||
|
|
|
||
|
|
AddStat(TEXT("Package.PackageDifferencesSizeMBPerAsset"), StatsList);
|
||
|
|
}
|
||
|
|
|
||
|
|
int64 NewFileTotalSize = 0;
|
||
|
|
int64 NumDiffs = 0;
|
||
|
|
int64 DiffSize = 0;
|
||
|
|
for (const TPair<FName, FArchiveDiffStats>& PackageStat: PackageDiffStats)
|
||
|
|
{
|
||
|
|
NewFileTotalSize += PackageStat.Value.NewFileTotalSize;
|
||
|
|
NumDiffs += PackageStat.Value.NumDiffs;
|
||
|
|
DiffSize += PackageStat.Value.DiffSize;
|
||
|
|
}
|
||
|
|
|
||
|
|
double DifferentPackagesSizeMB = (double)NewFileTotalSize / 1024.0 / 1024.0;
|
||
|
|
int32 NumberOfDifferencesInPackages = NumDiffs;
|
||
|
|
double PackageDifferencesSizeMB = (double)DiffSize / 1024.0 / 1024.0;
|
||
|
|
|
||
|
|
StatsList.Empty(15);
|
||
|
|
ADD_COOK_STAT(NumberOfDifferentPackages);
|
||
|
|
ADD_COOK_STAT(DifferentPackagesSizeMB);
|
||
|
|
ADD_COOK_STAT(NumberOfDifferencesInPackages);
|
||
|
|
ADD_COOK_STAT(PackageDifferencesSizeMB);
|
||
|
|
|
||
|
|
AddStat(TEXT("Package.DiffTotal"), StatsList);
|
||
|
|
|
||
|
|
#undef ADD_COOK_STAT
|
||
|
|
const FString TotalString = TEXT("Total");
|
||
|
|
}
|
||
|
|
|
||
|
|
void FSavePackageStats::MergeStats(const TMap<FName, FArchiveDiffStats>& ToMerge)
|
||
|
|
{
|
||
|
|
for (const TPair<FName, FArchiveDiffStats>& Stat: ToMerge)
|
||
|
|
{
|
||
|
|
PackageDiffStats.FindOrAdd(Stat.Key).DiffSize += Stat.Value.DiffSize;
|
||
|
|
PackageDiffStats.FindOrAdd(Stat.Key).NewFileTotalSize += Stat.Value.NewFileTotalSize;
|
||
|
|
PackageDiffStats.FindOrAdd(Stat.Key).NumDiffs += Stat.Value.NumDiffs;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
#endif
|
||
|
|
|
||
|
|
#if WITH_EDITORONLY_DATA
|
||
|
|
|
||
|
|
void FArchiveObjectCrc32NonEditorProperties::Serialize(void* Data, int64 Length)
|
||
|
|
{
|
||
|
|
int32 NewEditorOnlyProp = EditorOnlyProp + this->IsEditorOnlyPropertyOnTheStack();
|
||
|
|
TGuardValue<int32> Guard(EditorOnlyProp, NewEditorOnlyProp);
|
||
|
|
if (NewEditorOnlyProp == 0)
|
||
|
|
{
|
||
|
|
Super::Serialize(Data, Length);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#endif
|
||
|
|
|
||
|
|
static FThreadSafeCounter OutstandingAsyncWrites;
|
||
|
|
|
||
|
|
namespace SavePackageUtilities
|
||
|
|
{
|
||
|
|
const FName NAME_World("World");
|
||
|
|
const FName NAME_Level("Level");
|
||
|
|
const FName NAME_PrestreamPackage("PrestreamPackage");
|
||
|
|
|
||
|
|
void GetBlueprintNativeCodeGenReplacement(UObject* InObj, UClass*& ObjClass, UObject*& ObjOuter, FName& ObjName, const ITargetPlatform* TargetPlatform)
|
||
|
|
{
|
||
|
|
#if WITH_EDITOR
|
||
|
|
if (const IBlueprintNativeCodeGenCore* Coordinator = IBlueprintNativeCodeGenCore::Get())
|
||
|
|
{
|
||
|
|
const FCompilerNativizationOptions& NativizationOptions = Coordinator->GetNativizationOptionsForPlatform(TargetPlatform);
|
||
|
|
if (UClass* ReplacedClass = Coordinator->FindReplacedClassForObject(InObj, NativizationOptions))
|
||
|
|
{
|
||
|
|
ObjClass = ReplacedClass;
|
||
|
|
}
|
||
|
|
if (UObject* ReplacedOuter = Coordinator->FindReplacedNameAndOuter(InObj, /*out*/ ObjName, NativizationOptions))
|
||
|
|
{
|
||
|
|
ObjOuter = ReplacedOuter;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
void IncrementOutstandingAsyncWrites()
|
||
|
|
{
|
||
|
|
OutstandingAsyncWrites.Increment();
|
||
|
|
}
|
||
|
|
|
||
|
|
void DecrementOutstandingAsyncWrites()
|
||
|
|
{
|
||
|
|
OutstandingAsyncWrites.Decrement();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool HasUnsaveableOuter(UObject* InObj, UPackage* InSavingPackage)
|
||
|
|
{
|
||
|
|
UObject* Obj = InObj;
|
||
|
|
while (Obj)
|
||
|
|
{
|
||
|
|
if (Obj->GetClass()->HasAnyClassFlags(CLASS_Deprecated) && !Obj->HasAnyFlags(RF_ClassDefaultObject))
|
||
|
|
{
|
||
|
|
if (!InObj->IsPendingKill() && InObj->GetOutermost() == InSavingPackage)
|
||
|
|
{
|
||
|
|
UE_LOG(LogSavePackage, Warning, TEXT("%s has a deprecated outer %s, so it will not be saved"), *InObj->GetFullName(), *Obj->GetFullName());
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Obj->IsPendingKill())
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Obj->HasAnyFlags(RF_Transient) && !Obj->IsNative())
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
Obj = Obj->GetOuter();
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
void CheckObjectPriorToSave(FArchiveUObject& Ar, UObject* InObj, UPackage* InSavingPackage)
|
||
|
|
{
|
||
|
|
if (!InObj)
|
||
|
|
{
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
UObject* SerializedObject = nullptr;
|
||
|
|
FUObjectSerializeContext* SaveContext = Ar.GetSerializeContext();
|
||
|
|
check(SaveContext);
|
||
|
|
SerializedObject = SaveContext->SerializedObject;
|
||
|
|
|
||
|
|
if (!InObj->IsValidLowLevelFast() || !InObj->IsValidLowLevel())
|
||
|
|
{
|
||
|
|
UE_LOG(LogLinker, Fatal, TEXT("Attempt to save bogus object %p SaveContext.SerializedObject=%s SerializedProperty=%s"), (void*)InObj, *GetFullNameSafe(SerializedObject), *GetFullNameSafe(Ar.GetSerializedProperty()));
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
// if the object class is abstract or has been marked as deprecated, mark this
|
||
|
|
// object as transient so that it isn't serialized
|
||
|
|
if (InObj->GetClass()->HasAnyClassFlags(CLASS_Abstract | CLASS_Deprecated | CLASS_NewerVersionExists))
|
||
|
|
{
|
||
|
|
if (!InObj->HasAnyFlags(RF_ClassDefaultObject) || InObj->GetClass()->HasAnyClassFlags(CLASS_Deprecated))
|
||
|
|
{
|
||
|
|
InObj->SetFlags(RF_Transient);
|
||
|
|
}
|
||
|
|
if (!InObj->HasAnyFlags(RF_ClassDefaultObject) && InObj->GetClass()->HasAnyClassFlags(CLASS_HasInstancedReference))
|
||
|
|
{
|
||
|
|
TArray<UObject*> ComponentReferences;
|
||
|
|
FReferenceFinder ComponentCollector(ComponentReferences, InObj, false, true, true);
|
||
|
|
ComponentCollector.FindReferences(InObj, SerializedObject, Ar.GetSerializedProperty());
|
||
|
|
|
||
|
|
for (int32 Index = 0; Index < ComponentReferences.Num(); Index++)
|
||
|
|
{
|
||
|
|
ComponentReferences[Index]->SetFlags(RF_Transient);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else if (HasUnsaveableOuter(InObj, InSavingPackage))
|
||
|
|
{
|
||
|
|
InObj->SetFlags(RF_Transient);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (InObj->HasAnyFlags(RF_ClassDefaultObject) && (InObj->GetClass()->ClassGeneratedBy == nullptr || !InObj->GetClass()->HasAnyFlags(RF_Transient)))
|
||
|
|
{
|
||
|
|
// if this is the class default object, make sure it's not
|
||
|
|
// marked transient for any reason, as we need it to be saved
|
||
|
|
// to disk (unless it's associated with a transient generated class)
|
||
|
|
InObj->ClearFlags(RF_Transient);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Determines the set of object marks that should be excluded for the target platform
|
||
|
|
*
|
||
|
|
* @param TargetPlatform The platform being saved for, or null for saving platform-agnostic version
|
||
|
|
*
|
||
|
|
* @return Excluded object marks specific for the particular target platform, objects with any of these marks will be rejected from the cook
|
||
|
|
*/
|
||
|
|
EObjectMark GetExcludedObjectMarksForTargetPlatform(const class ITargetPlatform* TargetPlatform)
|
||
|
|
{
|
||
|
|
EObjectMark ObjectMarks = OBJECTMARK_NOMARKS;
|
||
|
|
|
||
|
|
if (TargetPlatform)
|
||
|
|
{
|
||
|
|
if (!TargetPlatform->HasEditorOnlyData())
|
||
|
|
{
|
||
|
|
ObjectMarks = (EObjectMark)(ObjectMarks | OBJECTMARK_EditorOnly);
|
||
|
|
}
|
||
|
|
|
||
|
|
const bool bIsServerOnly = TargetPlatform->IsServerOnly();
|
||
|
|
const bool bIsClientOnly = TargetPlatform->IsClientOnly();
|
||
|
|
|
||
|
|
if (bIsServerOnly)
|
||
|
|
{
|
||
|
|
ObjectMarks = (EObjectMark)(ObjectMarks | OBJECTMARK_NotForServer);
|
||
|
|
}
|
||
|
|
else if (bIsClientOnly)
|
||
|
|
{
|
||
|
|
ObjectMarks = (EObjectMark)(ObjectMarks | OBJECTMARK_NotForClient);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return ObjectMarks;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Marks object as not for client, not for server, or editor only. Recurses up outer/class chain as necessary
|
||
|
|
*/
|
||
|
|
void ConditionallyExcludeObjectForTarget(UObject* Obj, EObjectMark ExcludedObjectMarks, const ITargetPlatform* TargetPlatform)
|
||
|
|
{
|
||
|
|
#if WITH_EDITOR
|
||
|
|
if (!Obj || Obj->GetOutermost()->GetFName() == GLongCoreUObjectPackageName)
|
||
|
|
{
|
||
|
|
// No object or in CoreUObject, don't exclude
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
auto InheritMarks = [](EObjectMark& MarksToModify, UObject* ObjToCheck, uint32 MarkMask)
|
||
|
|
{
|
||
|
|
EObjectMark ObjToCheckMarks = ObjToCheck->GetAllMarks();
|
||
|
|
|
||
|
|
MarksToModify = (EObjectMark)(MarksToModify | (ObjToCheckMarks & MarkMask));
|
||
|
|
};
|
||
|
|
|
||
|
|
// MarksToProcess is a superset of marks retrieved from UPackage::GetExcludedObjectMarksForTargetPlatform
|
||
|
|
const uint32 MarksToProcess = OBJECTMARK_EditorOnly | OBJECTMARK_NotForClient | OBJECTMARK_NotForServer | OBJECTMARK_KeepForTargetPlatform;
|
||
|
|
check((ExcludedObjectMarks & ~MarksToProcess) == 0);
|
||
|
|
|
||
|
|
EObjectMark CurrentMarks = OBJECTMARK_NOMARKS;
|
||
|
|
InheritMarks(CurrentMarks, Obj, MarksToProcess);
|
||
|
|
|
||
|
|
if ((CurrentMarks & MarksToProcess) != 0)
|
||
|
|
{
|
||
|
|
// Already marked
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
UObject* ObjOuter = Obj->GetOuter();
|
||
|
|
UClass* ObjClass = Obj->GetClass();
|
||
|
|
|
||
|
|
// if TargetPlatorm != nullptr then we are cooking
|
||
|
|
if (TargetPlatform)
|
||
|
|
{
|
||
|
|
// Check for nativization replacement
|
||
|
|
if (const IBlueprintNativeCodeGenCore* Coordinator = IBlueprintNativeCodeGenCore::Get())
|
||
|
|
{
|
||
|
|
const FCompilerNativizationOptions& NativizationOptions = Coordinator->GetNativizationOptionsForPlatform(TargetPlatform);
|
||
|
|
FName UnusedName;
|
||
|
|
if (UClass* ReplacedClass = Coordinator->FindReplacedClassForObject(Obj, NativizationOptions))
|
||
|
|
{
|
||
|
|
ObjClass = ReplacedClass;
|
||
|
|
}
|
||
|
|
if (UObject* ReplacedOuter = Coordinator->FindReplacedNameAndOuter(Obj, /*out*/ UnusedName, NativizationOptions))
|
||
|
|
{
|
||
|
|
ObjOuter = ReplacedOuter;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
EObjectMark NewMarks = CurrentMarks;
|
||
|
|
|
||
|
|
// Recurse into parents, then compute inherited marks
|
||
|
|
ConditionallyExcludeObjectForTarget(ObjClass, ExcludedObjectMarks, TargetPlatform);
|
||
|
|
InheritMarks(NewMarks, ObjClass, OBJECTMARK_EditorOnly | OBJECTMARK_NotForClient | OBJECTMARK_NotForServer);
|
||
|
|
|
||
|
|
if (ObjOuter)
|
||
|
|
{
|
||
|
|
ConditionallyExcludeObjectForTarget(ObjOuter, ExcludedObjectMarks, TargetPlatform);
|
||
|
|
InheritMarks(NewMarks, ObjOuter, OBJECTMARK_EditorOnly | OBJECTMARK_NotForClient | OBJECTMARK_NotForServer);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check parent struct if we have one
|
||
|
|
UStruct* ThisStruct = Cast<UStruct>(Obj);
|
||
|
|
if (ThisStruct && ThisStruct->GetSuperStruct())
|
||
|
|
{
|
||
|
|
UObject* SuperStruct = ThisStruct->GetSuperStruct();
|
||
|
|
ConditionallyExcludeObjectForTarget(SuperStruct, ExcludedObjectMarks, TargetPlatform);
|
||
|
|
InheritMarks(NewMarks, SuperStruct, OBJECTMARK_EditorOnly | OBJECTMARK_NotForClient | OBJECTMARK_NotForServer);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check archetype, this may not have been covered in the case of components
|
||
|
|
UObject* Archetype = Obj->GetArchetype();
|
||
|
|
if (Archetype)
|
||
|
|
{
|
||
|
|
ConditionallyExcludeObjectForTarget(Archetype, ExcludedObjectMarks, TargetPlatform);
|
||
|
|
InheritMarks(NewMarks, Archetype, OBJECTMARK_EditorOnly | OBJECTMARK_NotForClient | OBJECTMARK_NotForServer);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!Obj->HasAnyFlags(RF_ClassDefaultObject))
|
||
|
|
{
|
||
|
|
// CDOs must be included if their class is so only inherit marks, for everything else we check the native overrides as well
|
||
|
|
if (!(NewMarks & OBJECTMARK_EditorOnly) && IsEditorOnlyObject(Obj, false, false))
|
||
|
|
{
|
||
|
|
NewMarks = (EObjectMark)(NewMarks | OBJECTMARK_EditorOnly);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!(NewMarks & OBJECTMARK_NotForClient) && !Obj->NeedsLoadForClient())
|
||
|
|
{
|
||
|
|
NewMarks = (EObjectMark)(NewMarks | OBJECTMARK_NotForClient);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!(NewMarks & OBJECTMARK_NotForServer) && !Obj->NeedsLoadForServer())
|
||
|
|
{
|
||
|
|
NewMarks = (EObjectMark)(NewMarks | OBJECTMARK_NotForServer);
|
||
|
|
}
|
||
|
|
|
||
|
|
if ((!(NewMarks & OBJECTMARK_NotForServer) || !(NewMarks & OBJECTMARK_NotForClient)) && TargetPlatform && !Obj->NeedsLoadForTargetPlatform(TargetPlatform))
|
||
|
|
{
|
||
|
|
NewMarks = (EObjectMark)(NewMarks | OBJECTMARK_NotForClient | OBJECTMARK_NotForServer);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If NotForClient and NotForServer, it is implicitly editor only
|
||
|
|
if ((NewMarks & OBJECTMARK_NotForClient) && (NewMarks & OBJECTMARK_NotForServer))
|
||
|
|
{
|
||
|
|
NewMarks = (EObjectMark)(NewMarks | OBJECTMARK_EditorOnly);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If not excluded after a full set of tests, it is implicitly a keep
|
||
|
|
if (NewMarks == 0)
|
||
|
|
{
|
||
|
|
NewMarks = OBJECTMARK_KeepForTargetPlatform;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If our marks are different than original, set them on the object
|
||
|
|
if (CurrentMarks != NewMarks)
|
||
|
|
{
|
||
|
|
Obj->Mark(NewMarks);
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Find most likely culprit that caused the objects in the passed in array to be considered for saving.
|
||
|
|
*
|
||
|
|
* @param BadObjects array of objects that are considered "bad" (e.g. non- RF_Public, in different map package, ...)
|
||
|
|
* @return UObject that is considered the most likely culprit causing them to be referenced or NULL
|
||
|
|
*/
|
||
|
|
void FindMostLikelyCulprit(TArray<UObject*> BadObjects, UObject*& MostLikelyCulprit, const FProperty*& PropertyRef)
|
||
|
|
{
|
||
|
|
MostLikelyCulprit = nullptr;
|
||
|
|
|
||
|
|
// Iterate over all objects that are marked as unserializable/ bad and print out their referencers.
|
||
|
|
for (int32 BadObjIndex = 0; BadObjIndex < BadObjects.Num(); BadObjIndex++)
|
||
|
|
{
|
||
|
|
UObject* Obj = BadObjects[BadObjIndex];
|
||
|
|
|
||
|
|
UE_LOG(LogSavePackage, Warning, TEXT("\r\nReferencers of %s:"), *Obj->GetFullName());
|
||
|
|
|
||
|
|
FReferencerInformationList Refs;
|
||
|
|
|
||
|
|
if (IsReferenced(Obj, RF_Public, EInternalObjectFlags::Native, true, &Refs))
|
||
|
|
{
|
||
|
|
for (int32 i = 0; i < Refs.ExternalReferences.Num(); i++)
|
||
|
|
{
|
||
|
|
UObject* RefObj = Refs.ExternalReferences[i].Referencer;
|
||
|
|
if (RefObj->HasAnyMarks(EObjectMark(OBJECTMARK_TagExp | OBJECTMARK_TagImp)))
|
||
|
|
{
|
||
|
|
if (RefObj->GetFName() == NAME_PersistentLevel || RefObj->GetClass()->GetFName() == NAME_World)
|
||
|
|
{
|
||
|
|
// these types of references should be ignored
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
UE_LOG(LogSavePackage, Warning, TEXT("\t%s (%i refs)"), *RefObj->GetFullName(), Refs.ExternalReferences[i].TotalReferences);
|
||
|
|
for (int32 j = 0; j < Refs.ExternalReferences[i].ReferencingProperties.Num(); j++)
|
||
|
|
{
|
||
|
|
const FProperty* Prop = Refs.ExternalReferences[i].ReferencingProperties[j];
|
||
|
|
UE_LOG(LogSavePackage, Warning, TEXT("\t\t%i) %s"), j, *Prop->GetFullName());
|
||
|
|
PropertyRef = Prop;
|
||
|
|
}
|
||
|
|
|
||
|
|
MostLikelyCulprit = Obj;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void AddFileToHash(FString const& Filename, FMD5& Hash)
|
||
|
|
{
|
||
|
|
TArray<uint8> LocalScratch;
|
||
|
|
LocalScratch.SetNumUninitialized(1024 * 64);
|
||
|
|
|
||
|
|
FArchive* Ar = IFileManager::Get().CreateFileReader(*Filename);
|
||
|
|
|
||
|
|
const int64 Size = Ar->TotalSize();
|
||
|
|
int64 Position = 0;
|
||
|
|
|
||
|
|
while (Position < Size)
|
||
|
|
{
|
||
|
|
const auto ReadNum = FMath::Min(Size - Position, (int64)LocalScratch.Num());
|
||
|
|
Ar->Serialize(LocalScratch.GetData(), ReadNum);
|
||
|
|
Hash.Update(LocalScratch.GetData(), ReadNum);
|
||
|
|
Position += ReadNum;
|
||
|
|
}
|
||
|
|
delete Ar;
|
||
|
|
}
|
||
|
|
|
||
|
|
void WriteToFile(const FString& Filename, const uint8* InDataPtr, int64 InDataSize)
|
||
|
|
{
|
||
|
|
IFileManager& FileManager = IFileManager::Get();
|
||
|
|
|
||
|
|
for (int tries = 0; tries < 3; ++tries)
|
||
|
|
{
|
||
|
|
if (FArchive* Ar = FileManager.CreateFileWriter(*Filename))
|
||
|
|
{
|
||
|
|
Ar->Serialize(/* grrr */ const_cast<uint8*>(InDataPtr), InDataSize);
|
||
|
|
delete Ar;
|
||
|
|
|
||
|
|
if (FileManager.FileSize(*Filename) != InDataSize)
|
||
|
|
{
|
||
|
|
FileManager.Delete(*Filename);
|
||
|
|
|
||
|
|
UE_LOG(LogSavePackage, Fatal, TEXT("Could not save to %s!"), *Filename);
|
||
|
|
}
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
UE_LOG(LogSavePackage, Fatal, TEXT("Could not write to %s!"), *Filename);
|
||
|
|
}
|
||
|
|
|
||
|
|
void AsyncWriteFile(TAsyncWorkSequence<FMD5>& AsyncWriteAndHashSequence, FLargeMemoryPtr Data, const int64 DataSize, const TCHAR* Filename, EAsyncWriteOptions Options, TArrayView<const FFileRegion> InFileRegions)
|
||
|
|
{
|
||
|
|
OutstandingAsyncWrites.Increment();
|
||
|
|
FString OutputFilename(Filename);
|
||
|
|
AsyncWriteAndHashSequence.AddWork([Data = MoveTemp(Data), DataSize, OutputFilename = MoveTemp(OutputFilename), Options, FileRegions = TArray<FFileRegion>(InFileRegions)](FMD5& State) mutable
|
||
|
|
{
|
||
|
|
if (EnumHasAnyFlags(Options, EAsyncWriteOptions::ComputeHash))
|
||
|
|
{
|
||
|
|
State.Update(Data.Get(), DataSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (EnumHasAnyFlags(Options, EAsyncWriteOptions::WriteFileToDisk))
|
||
|
|
{
|
||
|
|
WriteToFile(OutputFilename, Data.Get(), DataSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (FileRegions.Num() > 0)
|
||
|
|
{
|
||
|
|
TArray<uint8> Memory;
|
||
|
|
FMemoryWriter Ar(Memory);
|
||
|
|
FFileRegion::SerializeFileRegions(Ar, FileRegions);
|
||
|
|
|
||
|
|
WriteToFile(OutputFilename + FFileRegion::RegionsFileExtension, Memory.GetData(), Memory.Num());
|
||
|
|
}
|
||
|
|
|
||
|
|
OutstandingAsyncWrites.Decrement();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
void AsyncWriteFileWithSplitExports(TAsyncWorkSequence<FMD5>& AsyncWriteAndHashSequence, FLargeMemoryPtr Data, const int64 DataSize, const int64 HeaderSize, const TCHAR* Filename, EAsyncWriteOptions Options, TArrayView<const FFileRegion> InFileRegions)
|
||
|
|
{
|
||
|
|
OutstandingAsyncWrites.Increment();
|
||
|
|
FString OutputFilename(Filename);
|
||
|
|
AsyncWriteAndHashSequence.AddWork([Data = MoveTemp(Data), DataSize, HeaderSize, OutputFilename = MoveTemp(OutputFilename), Options, FileRegions = TArray<FFileRegion>(InFileRegions)](FMD5& State) mutable
|
||
|
|
{
|
||
|
|
if (EnumHasAnyFlags(Options, EAsyncWriteOptions::ComputeHash))
|
||
|
|
{
|
||
|
|
State.Update(Data.Get(), DataSize);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (EnumHasAnyFlags(Options, EAsyncWriteOptions::WriteFileToDisk))
|
||
|
|
{
|
||
|
|
// Write .uasset file
|
||
|
|
WriteToFile(OutputFilename, Data.Get(), HeaderSize);
|
||
|
|
|
||
|
|
// Write .uexp file
|
||
|
|
const FString FilenameExports = FPaths::ChangeExtension(OutputFilename, TEXT(".uexp"));
|
||
|
|
WriteToFile(FilenameExports, Data.Get() + HeaderSize, DataSize - HeaderSize);
|
||
|
|
|
||
|
|
if (FileRegions.Num() > 0)
|
||
|
|
{
|
||
|
|
// Adjust regions so they are relative to the start of the uexp file
|
||
|
|
for (FFileRegion& Region: FileRegions)
|
||
|
|
{
|
||
|
|
Region.Offset -= HeaderSize;
|
||
|
|
}
|
||
|
|
|
||
|
|
TArray<uint8> Memory;
|
||
|
|
FMemoryWriter Ar(Memory);
|
||
|
|
FFileRegion::SerializeFileRegions(Ar, FileRegions);
|
||
|
|
|
||
|
|
WriteToFile(FilenameExports + FFileRegion::RegionsFileExtension, Memory.GetData(), Memory.Num());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
OutstandingAsyncWrites.Decrement();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
/** For a CDO get all of the subobjects templates nested inside it or it's class */
|
||
|
|
void GetCDOSubobjects(UObject* CDO, TArray<UObject*>& Subobjects)
|
||
|
|
{
|
||
|
|
TArray<UObject*> CurrentSubobjects;
|
||
|
|
TArray<UObject*> NextSubobjects;
|
||
|
|
|
||
|
|
// Recursively search for subobjects. Only care about ones that have a full subobject chain as some nested objects are set wrong
|
||
|
|
GetObjectsWithOuter(CDO->GetClass(), NextSubobjects, false);
|
||
|
|
GetObjectsWithOuter(CDO, NextSubobjects, false);
|
||
|
|
|
||
|
|
while (NextSubobjects.Num() > 0)
|
||
|
|
{
|
||
|
|
CurrentSubobjects = NextSubobjects;
|
||
|
|
NextSubobjects.Empty();
|
||
|
|
for (UObject* SubObj: CurrentSubobjects)
|
||
|
|
{
|
||
|
|
if (SubObj->HasAnyFlags(RF_DefaultSubObject | RF_ArchetypeObject))
|
||
|
|
{
|
||
|
|
Subobjects.Add(SubObj);
|
||
|
|
GetObjectsWithOuter(SubObj, NextSubobjects, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} // end namespace SavePackageUtilities
|
||
|
|
|
||
|
|
bool IsEditorOnlyObject(const UObject* InObject, bool bCheckRecursive, bool bCheckMarks)
|
||
|
|
{
|
||
|
|
DECLARE_SCOPE_CYCLE_COUNTER(TEXT("IsEditorOnlyObject"), STAT_IsEditorOnlyObject, STATGROUP_LoadTime);
|
||
|
|
|
||
|
|
// Configurable via ini setting
|
||
|
|
static struct FCanStripEditorOnlyExportsAndImports
|
||
|
|
{
|
||
|
|
bool bCanStripEditorOnlyObjects;
|
||
|
|
FCanStripEditorOnlyExportsAndImports()
|
||
|
|
: bCanStripEditorOnlyObjects(true)
|
||
|
|
{
|
||
|
|
GConfig->GetBool(TEXT("Core.System"), TEXT("CanStripEditorOnlyExportsAndImports"), bCanStripEditorOnlyObjects, GEngineIni);
|
||
|
|
}
|
||
|
|
FORCEINLINE operator bool() const { return bCanStripEditorOnlyObjects; }
|
||
|
|
} CanStripEditorOnlyExportsAndImports;
|
||
|
|
if (!CanStripEditorOnlyExportsAndImports)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
check(InObject);
|
||
|
|
|
||
|
|
if ((bCheckMarks && InObject->HasAnyMarks(OBJECTMARK_EditorOnly)) || InObject->IsEditorOnly())
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
// If this is a package that is editor only or the object is in editor-only package,
|
||
|
|
// the object is editor-only too.
|
||
|
|
const bool bIsAPackage = InObject->IsA<UPackage>();
|
||
|
|
const UPackage* Package;
|
||
|
|
if (bIsAPackage)
|
||
|
|
{
|
||
|
|
if (InObject->HasAnyFlags(RF_ClassDefaultObject))
|
||
|
|
{
|
||
|
|
// The default package is not editor-only, and it is part of a cycle that would cause infinite recursion: DefaultPackage -> GetOuter() -> Package:/Script/CoreUObject -> GetArchetype() -> DefaultPackage
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
Package = static_cast<const UPackage*>(InObject);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Package = InObject->GetOutermost();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Package && Package->HasAnyPackageFlags(PKG_EditorOnly))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bCheckRecursive && !InObject->IsNative())
|
||
|
|
{
|
||
|
|
UObject* Outer = InObject->GetOuter();
|
||
|
|
if (Outer && Outer != Package)
|
||
|
|
{
|
||
|
|
if (IsEditorOnlyObject(Outer, true, bCheckMarks))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
const UStruct* InStruct = Cast<UStruct>(InObject);
|
||
|
|
if (InStruct)
|
||
|
|
{
|
||
|
|
const UStruct* SuperStruct = InStruct->GetSuperStruct();
|
||
|
|
if (SuperStruct && IsEditorOnlyObject(SuperStruct, true, bCheckMarks))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (IsEditorOnlyObject(InObject->GetClass(), true, bCheckMarks))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
|
||
|
|
UObject* Archetype = InObject->GetArchetype();
|
||
|
|
if (Archetype && IsEditorOnlyObject(Archetype, true, bCheckMarks))
|
||
|
|
{
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FObjectExportSortHelper::operator()(const FObjectExport& A, const FObjectExport& B) const
|
||
|
|
{
|
||
|
|
int32 Result = 0;
|
||
|
|
if (A.Object == nullptr)
|
||
|
|
{
|
||
|
|
Result = 1;
|
||
|
|
}
|
||
|
|
else if (B.Object == nullptr)
|
||
|
|
{
|
||
|
|
Result = -1;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
if (bUseFObjectFullName)
|
||
|
|
{
|
||
|
|
const FObjectFullName* FullNameA = ObjectToObjectFullNameMap.Find(A.Object);
|
||
|
|
const FObjectFullName* FullNameB = ObjectToObjectFullNameMap.Find(B.Object);
|
||
|
|
checkSlow(FullNameA);
|
||
|
|
checkSlow(FullNameB);
|
||
|
|
|
||
|
|
if (FullNameA->ClassName != FullNameB->ClassName)
|
||
|
|
{
|
||
|
|
Result = FCString::Stricmp(*FullNameA->ClassName.ToString(), *FullNameB->ClassName.ToString());
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
int Num = FMath::Min(FullNameA->Path.Num(), FullNameB->Path.Num());
|
||
|
|
for (int I = 0; I < Num; ++I)
|
||
|
|
{
|
||
|
|
if (FullNameA->Path[I] != FullNameB->Path[I])
|
||
|
|
{
|
||
|
|
Result = FCString::Stricmp(*FullNameA->Path[I].ToString(), *FullNameB->Path[I].ToString());
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (Result == 0)
|
||
|
|
{
|
||
|
|
Result = FullNameA->Path.Num() - FullNameB->Path.Num();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
const FString* FullNameA = ObjectToFullNameMap.Find(A.Object);
|
||
|
|
const FString* FullNameB = ObjectToFullNameMap.Find(B.Object);
|
||
|
|
checkSlow(FullNameA);
|
||
|
|
checkSlow(FullNameB);
|
||
|
|
|
||
|
|
Result = FCString::Stricmp(**FullNameA, **FullNameB);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return Result < 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
FObjectExportSortHelper::FObjectFullName::FObjectFullName(const UObject* Object, const UObject* Root)
|
||
|
|
{
|
||
|
|
ClassName = Object->GetClass()->GetFName();
|
||
|
|
const UObject* Current = Object;
|
||
|
|
while (Current != nullptr && Current != Root)
|
||
|
|
{
|
||
|
|
Path.Insert(Current->GetFName(), 0);
|
||
|
|
Current = Current->GetOuter();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FObjectExportSortHelper::FObjectFullName::FObjectFullName(FObjectFullName&& InFullName)
|
||
|
|
{
|
||
|
|
ClassName = InFullName.ClassName;
|
||
|
|
Swap(Path, InFullName.Path);
|
||
|
|
}
|
||
|
|
|
||
|
|
void FObjectExportSortHelper::SortExports(FLinkerSave* Linker, FLinkerLoad* LinkerToConformTo, bool InbUseFObjectFullName)
|
||
|
|
{
|
||
|
|
bUseFObjectFullName = InbUseFObjectFullName;
|
||
|
|
|
||
|
|
if (bUseFObjectFullName)
|
||
|
|
{
|
||
|
|
ObjectToObjectFullNameMap.Reserve(Linker->ExportMap.Num());
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ObjectToFullNameMap.Reserve(Linker->ExportMap.Num());
|
||
|
|
}
|
||
|
|
|
||
|
|
int32 SortStartPosition = 0;
|
||
|
|
if (LinkerToConformTo)
|
||
|
|
{
|
||
|
|
// build a map of object full names to the index into the new linker's export map prior to sorting.
|
||
|
|
// we need to do a little trickery here to generate an object path name that will match what we'll get back
|
||
|
|
// when we call GetExportFullName on the LinkerToConformTo's exports, due to localized packages and forced exports.
|
||
|
|
const FString LinkerName = Linker->LinkerRoot->GetName();
|
||
|
|
const FString PathNamePrefix = LinkerName + TEXT(".");
|
||
|
|
|
||
|
|
// Populate object to current index map.
|
||
|
|
TMap<FString, int32> OriginalExportIndexes;
|
||
|
|
OriginalExportIndexes.Reserve(Linker->ExportMap.Num());
|
||
|
|
for (int32 ExportIndex = 0; ExportIndex < Linker->ExportMap.Num(); ExportIndex++)
|
||
|
|
{
|
||
|
|
const FObjectExport& Export = Linker->ExportMap[ExportIndex];
|
||
|
|
if (Export.Object)
|
||
|
|
{
|
||
|
|
// get the path name for this object; if the object is contained within the package we're saving,
|
||
|
|
// we don't want the returned path name to contain the package name since we'll be adding that on
|
||
|
|
// to ensure that forced exports have the same outermost name as the non-forced exports
|
||
|
|
FString ObjectPathName = Export.Object != Linker->LinkerRoot ? Export.Object->GetPathName(Linker->LinkerRoot) : LinkerName;
|
||
|
|
|
||
|
|
FString ExportFullName = Export.Object->GetClass()->GetName() + TEXT(" ") + PathNamePrefix + ObjectPathName;
|
||
|
|
|
||
|
|
// Set the index (key) in the map to the index of this object into the export map.
|
||
|
|
OriginalExportIndexes.Add(*ExportFullName, ExportIndex);
|
||
|
|
if (bUseFObjectFullName)
|
||
|
|
{
|
||
|
|
FObjectFullName ObjectFullName(Export.Object, Linker->LinkerRoot);
|
||
|
|
ObjectToObjectFullNameMap.Add(Export.Object, MoveTemp(ObjectFullName));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ObjectToFullNameMap.Add(Export.Object, *ExportFullName);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// backup the existing export list so we can empty the linker's actual list
|
||
|
|
TArray<FObjectExport> OldExportMap = Linker->ExportMap;
|
||
|
|
Linker->ExportMap.Empty(Linker->ExportMap.Num());
|
||
|
|
|
||
|
|
// this array tracks which exports from the new package exist in the old package
|
||
|
|
TArray<uint8> Used;
|
||
|
|
Used.AddZeroed(OldExportMap.Num());
|
||
|
|
|
||
|
|
for (int32 i = 0; i < LinkerToConformTo->ExportMap.Num(); i++)
|
||
|
|
{
|
||
|
|
// determine whether the new version of the package contains this export from the old package
|
||
|
|
FString ExportFullName = LinkerToConformTo->GetExportFullName(i, *LinkerName);
|
||
|
|
int32* OriginalExportPosition = OriginalExportIndexes.Find(*ExportFullName);
|
||
|
|
if (OriginalExportPosition)
|
||
|
|
{
|
||
|
|
// this export exists in the new package as well,
|
||
|
|
// create a copy of the FObjectExport located at the original index and place it
|
||
|
|
// into the matching position in the new package's export map
|
||
|
|
FObjectExport* NewExport = new (Linker->ExportMap) FObjectExport(OldExportMap[*OriginalExportPosition]);
|
||
|
|
check(NewExport->Object == OldExportMap[*OriginalExportPosition].Object);
|
||
|
|
Used[*OriginalExportPosition] = 1;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
|
||
|
|
// this export no longer exists in the new package; to ensure that the _LinkerIndex matches, add an empty entry to pad the list
|
||
|
|
new (Linker->ExportMap) FObjectExport(nullptr);
|
||
|
|
UE_LOG(LogSavePackage, Log, TEXT("No matching export found in new package for original export %i: %s"), i, *ExportFullName);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
SortStartPosition = LinkerToConformTo->ExportMap.Num();
|
||
|
|
for (int32 i = 0; i < Used.Num(); i++)
|
||
|
|
{
|
||
|
|
if (!Used[i])
|
||
|
|
{
|
||
|
|
// the FObjectExport located at pos "i" in the original export table did not
|
||
|
|
// exist in the old package - add it to the end of the export table
|
||
|
|
new (Linker->ExportMap) FObjectExport(OldExportMap[i]);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
#if DO_GUARD_SLOW
|
||
|
|
|
||
|
|
// sanity-check: make sure that all exports which existed in the linker before we sorted exist in the linker's export map now
|
||
|
|
{
|
||
|
|
TSet<UObject*> ExportObjectList;
|
||
|
|
for (int32 ExportIndex = 0; ExportIndex < Linker->ExportMap.Num(); ExportIndex++)
|
||
|
|
{
|
||
|
|
ExportObjectList.Add(Linker->ExportMap[ExportIndex].Object);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (int32 OldExportIndex = 0; OldExportIndex < OldExportMap.Num(); OldExportIndex++)
|
||
|
|
{
|
||
|
|
check(ExportObjectList.Contains(OldExportMap[OldExportIndex].Object));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
for (int32 ExportIndex = 0; ExportIndex < Linker->ExportMap.Num(); ExportIndex++)
|
||
|
|
{
|
||
|
|
const FObjectExport& Export = Linker->ExportMap[ExportIndex];
|
||
|
|
if (Export.Object)
|
||
|
|
{
|
||
|
|
if (bUseFObjectFullName)
|
||
|
|
{
|
||
|
|
FObjectFullName ObjectFullName(Export.Object, nullptr);
|
||
|
|
ObjectToObjectFullNameMap.Add(Export.Object, MoveTemp(ObjectFullName));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
ObjectToFullNameMap.Add(Export.Object, Export.Object->GetFullName());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (SortStartPosition < Linker->ExportMap.Num())
|
||
|
|
{
|
||
|
|
Sort(&Linker->ExportMap[SortStartPosition], Linker->ExportMap.Num() - SortStartPosition, *this);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeHash::FEDLNodeHash()
|
||
|
|
: Object(nullptr), PathName(NAME_None), NodeID(NodeIDInvalid), ObjectEvent(EObjectEvent::Create), HashMode(EHashMode::Object) // Use Object as default union member
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeHash::FEDLNodeHash(const TArray<FEDLNodeData>* InNodes, FEDLNodeID InNodeID, EObjectEvent InObjectEvent)
|
||
|
|
: Nodes(InNodes), PathName(NAME_None), NodeID(InNodeID), ObjectEvent(InObjectEvent), HashMode(EHashMode::Node)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeHash::FEDLNodeHash(const UObject* InObject, EObjectEvent InObjectEvent)
|
||
|
|
: Object(InObject), PathName(NAME_None), NodeID(NodeIDInvalid), ObjectEvent(InObjectEvent), HashMode(EHashMode::Object)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeHash::FEDLNodeHash(FName InPath, EObjectEvent InObjectEvent)
|
||
|
|
: Object(nullptr), PathName(InPath), NodeID(NodeIDInvalid), ObjectEvent(InObjectEvent), HashMode(EHashMode::Path)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FEDLCookChecker::FEDLNodeHash::operator==(const FEDLNodeHash& Other) const
|
||
|
|
{
|
||
|
|
if (ObjectEvent != Other.ObjectEvent)
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Path-based comparison is the unified source of truth
|
||
|
|
return GetPathAsName() == Other.GetPathAsName();
|
||
|
|
}
|
||
|
|
|
||
|
|
uint32 GetTypeHash(const FEDLCookChecker::FEDLNodeHash& A)
|
||
|
|
{
|
||
|
|
// Path-based hash is the unified source of truth
|
||
|
|
return HashCombine(GetTypeHash(A.GetPathAsName()), GetTypeHash(A.GetObjectEvent()));
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::EObjectEvent FEDLCookChecker::FEDLNodeHash::GetObjectEvent() const
|
||
|
|
{
|
||
|
|
return ObjectEvent;
|
||
|
|
}
|
||
|
|
|
||
|
|
FName FEDLCookChecker::FEDLNodeHash::GetPathAsName() const
|
||
|
|
{
|
||
|
|
switch (HashMode)
|
||
|
|
{
|
||
|
|
case EHashMode::Object:
|
||
|
|
if (Object)
|
||
|
|
{
|
||
|
|
FString Path = Object->GetPathName();
|
||
|
|
Path.ReplaceInline(TEXT(":"), TEXT("."));
|
||
|
|
return FName(*Path);
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case EHashMode::Path: {
|
||
|
|
FString Path = PathName.ToString();
|
||
|
|
if (Path.Contains(TEXT(":")))
|
||
|
|
{
|
||
|
|
Path.ReplaceInline(TEXT(":"), TEXT("."));
|
||
|
|
return FName(*Path);
|
||
|
|
}
|
||
|
|
return PathName;
|
||
|
|
}
|
||
|
|
case EHashMode::Node: {
|
||
|
|
TStringBuilder<1024> PathBuilder;
|
||
|
|
AppendPath(PathBuilder);
|
||
|
|
return FName(PathBuilder.ToString());
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return NAME_None;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::FEDLNodeHash::AppendPath(TStringBuilder<1024>& Builder) const
|
||
|
|
{
|
||
|
|
check(HashMode == EHashMode::Node);
|
||
|
|
const FEDLNodeData& Node = (*Nodes)[NodeID];
|
||
|
|
if (Node.ParentID != NodeIDInvalid)
|
||
|
|
{
|
||
|
|
FEDLNodeHash ParentHash(Nodes, Node.ParentID, EObjectEvent::Create);
|
||
|
|
ParentHash.AppendPath(Builder);
|
||
|
|
Builder << TEXT('.');
|
||
|
|
}
|
||
|
|
Node.Name.AppendString(Builder);
|
||
|
|
}
|
||
|
|
|
||
|
|
FName FEDLCookChecker::FEDLNodeHash::GetName() const
|
||
|
|
{
|
||
|
|
switch (HashMode)
|
||
|
|
{
|
||
|
|
case EHashMode::Object:
|
||
|
|
if (Object)
|
||
|
|
{
|
||
|
|
if (!Object->GetOuter())
|
||
|
|
{
|
||
|
|
FString Path = Object->GetPathName();
|
||
|
|
Path.ReplaceInline(TEXT(":"), TEXT("."));
|
||
|
|
return FName(*Path);
|
||
|
|
}
|
||
|
|
return Object->GetFName();
|
||
|
|
}
|
||
|
|
return NAME_None;
|
||
|
|
case EHashMode::Path: {
|
||
|
|
FString PathStr = PathName.ToString();
|
||
|
|
int32 DotIndex = INDEX_NONE;
|
||
|
|
int32 ColonIndex = INDEX_NONE;
|
||
|
|
PathStr.FindLastChar(TCHAR('.'), DotIndex);
|
||
|
|
PathStr.FindLastChar(TCHAR(':'), ColonIndex);
|
||
|
|
int32 DelimiterPos = FMath::Max(DotIndex, ColonIndex);
|
||
|
|
|
||
|
|
if (DelimiterPos != INDEX_NONE)
|
||
|
|
{
|
||
|
|
return FName(*PathStr.Mid(DelimiterPos + 1));
|
||
|
|
}
|
||
|
|
return PathName;
|
||
|
|
}
|
||
|
|
case EHashMode::Node:
|
||
|
|
return (*Nodes)[NodeID].Name;
|
||
|
|
default:
|
||
|
|
check(false);
|
||
|
|
return NAME_None;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FEDLCookChecker::FEDLNodeHash::TryGetParent(FEDLNodeHash& OutParent) const
|
||
|
|
{
|
||
|
|
const EObjectEvent ParentObjectEvent = EObjectEvent::Create;
|
||
|
|
|
||
|
|
switch (HashMode)
|
||
|
|
{
|
||
|
|
case EHashMode::Object:
|
||
|
|
if (Object)
|
||
|
|
{
|
||
|
|
if (UObject* ParentObject = Object->GetOuter())
|
||
|
|
{
|
||
|
|
OutParent = FEDLNodeHash(ParentObject, ParentObjectEvent);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
case EHashMode::Path: {
|
||
|
|
FString PathStr = PathName.ToString();
|
||
|
|
int32 DotIndex = INDEX_NONE;
|
||
|
|
int32 ColonIndex = INDEX_NONE;
|
||
|
|
PathStr.FindLastChar(TCHAR('.'), DotIndex);
|
||
|
|
PathStr.FindLastChar(TCHAR(':'), ColonIndex);
|
||
|
|
int32 DelimiterPos = FMath::Max(DotIndex, ColonIndex);
|
||
|
|
|
||
|
|
if (DelimiterPos != INDEX_NONE)
|
||
|
|
{
|
||
|
|
OutParent = FEDLNodeHash(FName(*PathStr.Left(DelimiterPos)), ParentObjectEvent);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
case EHashMode::Node: {
|
||
|
|
FEDLNodeID ParentID = (*Nodes)[NodeID].ParentID;
|
||
|
|
if (ParentID != NodeIDInvalid)
|
||
|
|
{
|
||
|
|
OutParent = FEDLNodeHash(Nodes, ParentID, ParentObjectEvent);
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeData::FEDLNodeData(FEDLNodeID InID, FEDLNodeID InParentID, FName InName, EObjectEvent InObjectEvent)
|
||
|
|
: Name(InName), ID(InID), ParentID(InParentID), ObjectEvent(InObjectEvent), bIsExport(false)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeData::FEDLNodeData(FEDLNodeID InID, FEDLNodeID InParentID, FName InName, FEDLNodeData&& Other)
|
||
|
|
: Name(InName), ID(InID), ImportingPackagesSorted(MoveTemp(Other.ImportingPackagesSorted)), ParentID(InParentID), ObjectEvent(Other.ObjectEvent), bIsExport(Other.bIsExport)
|
||
|
|
{
|
||
|
|
// Note that Other Name and ParentID must be unmodified, since they might still be needed for GetHashCode calls from children
|
||
|
|
Other.ImportingPackagesSorted.Empty();
|
||
|
|
}
|
||
|
|
|
||
|
|
FString FEDLCookChecker::FEDLNodeData::ToString(const FEDLCookChecker& Owner) const
|
||
|
|
{
|
||
|
|
TStringBuilder<NAME_SIZE> Result;
|
||
|
|
switch (ObjectEvent)
|
||
|
|
{
|
||
|
|
case EObjectEvent::Create:
|
||
|
|
Result << TEXT("Create:");
|
||
|
|
break;
|
||
|
|
case EObjectEvent::Serialize:
|
||
|
|
Result << TEXT("Serialize:");
|
||
|
|
break;
|
||
|
|
default:
|
||
|
|
check(false);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
AppendPathName(Owner, Result);
|
||
|
|
return FString(Result);
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::FEDLNodeData::AppendPathName(const FEDLCookChecker& Owner, FStringBuilderBase& Result) const
|
||
|
|
{
|
||
|
|
if (ParentID != NodeIDInvalid)
|
||
|
|
{
|
||
|
|
const FEDLNodeData& ParentNode = Owner.Nodes[ParentID];
|
||
|
|
ParentNode.AppendPathName(Owner, Result);
|
||
|
|
bool bParentIsOutermost = ParentNode.ParentID == NodeIDInvalid;
|
||
|
|
Result << (bParentIsOutermost ? TEXT(".") : SUBOBJECT_DELIMITER);
|
||
|
|
}
|
||
|
|
Name.AppendString(Result);
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::FEDLNodeData::Merge(FEDLCookChecker::FEDLNodeData&& Other)
|
||
|
|
{
|
||
|
|
check(ObjectEvent == Other.ObjectEvent);
|
||
|
|
bIsExport = bIsExport | Other.bIsExport;
|
||
|
|
|
||
|
|
ImportingPackagesSorted.Append(Other.ImportingPackagesSorted);
|
||
|
|
Algo::Sort(ImportingPackagesSorted, FNameFastLess());
|
||
|
|
ImportingPackagesSorted.SetNum(Algo::Unique(ImportingPackagesSorted), true /* bAllowShrinking */);
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLCookChecker(EInternalConstruct)
|
||
|
|
: bIsActive(false)
|
||
|
|
{
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLCookChecker()
|
||
|
|
: FEDLCookChecker(EInternalConstruct::Type)
|
||
|
|
{
|
||
|
|
SetActiveIfNeeded();
|
||
|
|
|
||
|
|
FScopeLock CookCheckerInstanceLock(&CookCheckerInstanceCritical);
|
||
|
|
CookCheckerInstances.Add(this);
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::SetActiveIfNeeded()
|
||
|
|
{
|
||
|
|
bIsActive = IsEventDrivenLoaderEnabledInCookedBuilds() && !FParse::Param(FCommandLine::Get(), TEXT("DisableEDLCookChecker"));
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::Reset()
|
||
|
|
{
|
||
|
|
check(!GIsSavingPackage);
|
||
|
|
|
||
|
|
Nodes.Empty();
|
||
|
|
NodeHashToNodeID.Empty();
|
||
|
|
PathToNodeID.Empty();
|
||
|
|
NodePrereqs.Empty();
|
||
|
|
bIsActive = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::AddImport_Path(FName ImportPath, FName ImportingPackageName)
|
||
|
|
{
|
||
|
|
if (bIsActive)
|
||
|
|
{
|
||
|
|
FEDLNodeID NodeId = FindOrAddNodeFromPath(ImportPath, EObjectEvent::Serialize);
|
||
|
|
FEDLNodeData& NodeData = Nodes[NodeId];
|
||
|
|
TArray<FName>& Sorted = NodeData.ImportingPackagesSorted;
|
||
|
|
int32 InsertionIndex = Algo::LowerBound(Sorted, ImportingPackageName, FNameFastLess());
|
||
|
|
if (InsertionIndex == Sorted.Num() || Sorted[InsertionIndex] != ImportingPackageName)
|
||
|
|
{
|
||
|
|
Sorted.Insert(ImportingPackageName, InsertionIndex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::AddExport_Path(FName ExportPath)
|
||
|
|
{
|
||
|
|
if (bIsActive)
|
||
|
|
{
|
||
|
|
FEDLNodeID SerializeID = FindOrAddNodeFromPath(ExportPath, EObjectEvent::Serialize);
|
||
|
|
Nodes[SerializeID].bIsExport = true;
|
||
|
|
FEDLNodeID CreateID = FindOrAddNodeFromPath(ExportPath, EObjectEvent::Create);
|
||
|
|
Nodes[CreateID].bIsExport = true;
|
||
|
|
AddDependency(SerializeID, CreateID);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::AddArc_Path(FName DepPath, bool bDepIsSerialize, FName ExpPath, bool bExpIsSerialize)
|
||
|
|
{
|
||
|
|
if (bIsActive)
|
||
|
|
{
|
||
|
|
FEDLNodeID ExportID = FindOrAddNodeFromPath(ExpPath, bExpIsSerialize ? EObjectEvent::Serialize : EObjectEvent::Create);
|
||
|
|
FEDLNodeID DepID = FindOrAddNodeFromPath(DepPath, bDepIsSerialize ? EObjectEvent::Serialize : EObjectEvent::Create);
|
||
|
|
AddDependency(ExportID, DepID);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::AddImport(UObject* Import, UPackage* ImportingPackage)
|
||
|
|
{
|
||
|
|
if (ICookSideEffectCollector* CoolectorInstance = ICookSideEffectCollector::GetInstance())
|
||
|
|
{
|
||
|
|
if (!Import->GetOutermost()->HasAnyPackageFlags(PKG_CompiledIn))
|
||
|
|
{
|
||
|
|
CoolectorInstance->AddImport(Import, ImportingPackage);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bIsActive)
|
||
|
|
{
|
||
|
|
if (!Import->GetOutermost()->HasAnyPackageFlags(PKG_CompiledIn))
|
||
|
|
{
|
||
|
|
FEDLNodeID NodeId = FindOrAddNode(FEDLNodeHash(Import, EObjectEvent::Serialize));
|
||
|
|
FEDLNodeData& NodeData = Nodes[NodeId];
|
||
|
|
FName ImportingPackageName = ImportingPackage->GetFName();
|
||
|
|
TArray<FName>& Sorted = NodeData.ImportingPackagesSorted;
|
||
|
|
int32 InsertionIndex = Algo::LowerBound(Sorted, ImportingPackageName, FNameFastLess());
|
||
|
|
if (InsertionIndex == Sorted.Num() || Sorted[InsertionIndex] != ImportingPackageName)
|
||
|
|
{
|
||
|
|
Sorted.Insert(ImportingPackageName, InsertionIndex);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
void FEDLCookChecker::AddExport(UObject* Export)
|
||
|
|
{
|
||
|
|
if (ICookSideEffectCollector* CoolectorInstance = ICookSideEffectCollector::GetInstance())
|
||
|
|
{
|
||
|
|
CoolectorInstance->AddExport(Export);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bIsActive)
|
||
|
|
{
|
||
|
|
FEDLNodeID SerializeID = FindOrAddNode(FEDLNodeHash(Export, EObjectEvent::Serialize));
|
||
|
|
Nodes[SerializeID].bIsExport = true;
|
||
|
|
FEDLNodeID CreateID = FindOrAddNode(FEDLNodeHash(Export, EObjectEvent::Create));
|
||
|
|
Nodes[CreateID].bIsExport = true;
|
||
|
|
AddDependency(SerializeID, CreateID); // every export must be created before it can be serialize...these arcs are implicit and not listed in any table.
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::AddArc(UObject* DepObject, bool bDepIsSerialize, UObject* Export, bool bExportIsSerialize)
|
||
|
|
{
|
||
|
|
if (ICookSideEffectCollector* CoolectorInstance = ICookSideEffectCollector::GetInstance())
|
||
|
|
{
|
||
|
|
CoolectorInstance->AddArc(DepObject, bDepIsSerialize, Export, bExportIsSerialize);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bIsActive)
|
||
|
|
{
|
||
|
|
FEDLNodeID ExportID = FindOrAddNode(FEDLNodeHash(Export, bExportIsSerialize ? EObjectEvent::Serialize : EObjectEvent::Create));
|
||
|
|
FEDLNodeID DepID = FindOrAddNode(FEDLNodeHash(DepObject, bDepIsSerialize ? EObjectEvent::Serialize : EObjectEvent::Create));
|
||
|
|
AddDependency(ExportID, DepID);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::AddDependency(FEDLNodeID SourceID, FEDLNodeID TargetID)
|
||
|
|
{
|
||
|
|
NodePrereqs.Add(SourceID, TargetID);
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::StartSavingEDLCookInfoForVerification()
|
||
|
|
{
|
||
|
|
FScopeLock CookCheckerInstanceLock(&CookCheckerInstanceCritical);
|
||
|
|
for (FEDLCookChecker* Checker: CookCheckerInstances)
|
||
|
|
{
|
||
|
|
Checker->Reset();
|
||
|
|
Checker->SetActiveIfNeeded();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool FEDLCookChecker::CheckForCyclesInner(TSet<FEDLNodeID>& Visited, TSet<FEDLNodeID>& Stack, const FEDLNodeID& Visit, FEDLNodeID& FailNode)
|
||
|
|
{
|
||
|
|
bool bResult = false;
|
||
|
|
if (Stack.Contains(Visit))
|
||
|
|
{
|
||
|
|
FailNode = Visit;
|
||
|
|
bResult = true;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
bool bWasAlreadyTested = false;
|
||
|
|
Visited.Add(Visit, &bWasAlreadyTested);
|
||
|
|
if (!bWasAlreadyTested)
|
||
|
|
{
|
||
|
|
Stack.Add(Visit);
|
||
|
|
for (auto It = NodePrereqs.CreateConstKeyIterator(Visit); !bResult && It; ++It)
|
||
|
|
{
|
||
|
|
bResult = CheckForCyclesInner(Visited, Stack, It.Value(), FailNode);
|
||
|
|
}
|
||
|
|
Stack.Remove(Visit);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
UE_CLOG(bResult && Stack.Contains(FailNode), LogSavePackage, Error, TEXT("Cycle Node %s"), *Nodes[Visit].ToString(*this));
|
||
|
|
return bResult;
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeID FEDLCookChecker::FindOrAddNode(const FEDLNodeHash& NodeHash)
|
||
|
|
{
|
||
|
|
uint32 TypeHash = GetTypeHash(NodeHash);
|
||
|
|
FEDLNodeID* NodeIDPtr = NodeHashToNodeID.FindByHash(TypeHash, NodeHash);
|
||
|
|
if (NodeIDPtr)
|
||
|
|
{
|
||
|
|
return *NodeIDPtr;
|
||
|
|
}
|
||
|
|
|
||
|
|
FName Name = NodeHash.GetName();
|
||
|
|
FEDLNodeHash ParentHash;
|
||
|
|
FEDLNodeID ParentID = NodeHash.TryGetParent(ParentHash) ? FindOrAddNode(ParentHash) : NodeIDInvalid;
|
||
|
|
FEDLNodeID NodeID = Nodes.Num();
|
||
|
|
FEDLNodeData& NewNodeData = Nodes.Emplace_GetRef(NodeID, ParentID, Name, NodeHash.GetObjectEvent());
|
||
|
|
NodeHashToNodeID.AddByHash(TypeHash, FEDLNodeHash(&Nodes, NodeID, NewNodeData.ObjectEvent), NodeID);
|
||
|
|
return NodeID;
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeID FEDLCookChecker::FindOrAddNodeFromPath(FName Path, EObjectEvent ObjectEvent)
|
||
|
|
{
|
||
|
|
FEDLNodeHash NodeHash(Path, ObjectEvent);
|
||
|
|
uint32 TypeHash = GetTypeHash(NodeHash);
|
||
|
|
if (FEDLNodeID* NodeIDPtr = NodeHashToNodeID.FindByHash(TypeHash, NodeHash))
|
||
|
|
{
|
||
|
|
return *NodeIDPtr;
|
||
|
|
}
|
||
|
|
|
||
|
|
// PathToNodeID only caches Create events.
|
||
|
|
if (ObjectEvent == EObjectEvent::Create)
|
||
|
|
{
|
||
|
|
if (FEDLNodeID* NodeIDPtr = PathToNodeID.Find(Path))
|
||
|
|
{
|
||
|
|
// The node is in the fast cache. We need to add it to the main hash table under the current hash
|
||
|
|
// since it was likely created as a parent of another node and not looked up directly.
|
||
|
|
NodeHashToNodeID.AddByHash(TypeHash, NodeHash, *NodeIDPtr);
|
||
|
|
return *NodeIDPtr;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// If not in any cache, create it using the main recursive function.
|
||
|
|
FEDLNodeID NewNodeID = FindOrAddNode(NodeHash);
|
||
|
|
|
||
|
|
// If we just created a 'Create' node, add it to the fast cache.
|
||
|
|
if (ObjectEvent == EObjectEvent::Create)
|
||
|
|
{
|
||
|
|
PathToNodeID.Add(Path, NewNodeID);
|
||
|
|
}
|
||
|
|
return NewNodeID;
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeID FEDLCookChecker::FindOrAddNode(FEDLNodeData&& NodeData, const FEDLCookChecker& OldOwnerOfNode, FEDLNodeID ParentIDInThis, bool& bNew)
|
||
|
|
{
|
||
|
|
// Note that NodeData's Name and ParentID must be unmodified, since they might still be needed for GetHashCode calls from children
|
||
|
|
|
||
|
|
FEDLNodeHash NodeHash(&OldOwnerOfNode.Nodes, NodeData.ID, NodeData.ObjectEvent);
|
||
|
|
uint32 TypeHash = GetTypeHash(NodeHash);
|
||
|
|
FEDLNodeID* NodeIDPtr = NodeHashToNodeID.FindByHash(TypeHash, NodeHash);
|
||
|
|
if (NodeIDPtr)
|
||
|
|
{
|
||
|
|
bNew = false;
|
||
|
|
return *NodeIDPtr;
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLNodeID NodeID = Nodes.Num();
|
||
|
|
FEDLNodeData& NewNodeData = Nodes.Emplace_GetRef(NodeID, ParentIDInThis, NodeData.Name, MoveTemp(NodeData));
|
||
|
|
NodeHashToNodeID.AddByHash(TypeHash, FEDLNodeHash(&Nodes, NodeID, NewNodeData.ObjectEvent), NodeID);
|
||
|
|
bNew = true;
|
||
|
|
return NodeID;
|
||
|
|
}
|
||
|
|
|
||
|
|
FEDLCookChecker::FEDLNodeID FEDLCookChecker::FindNode(const FEDLNodeHash& NodeHash)
|
||
|
|
{
|
||
|
|
const FEDLNodeID* NodeIDPtr = NodeHashToNodeID.Find(NodeHash);
|
||
|
|
return NodeIDPtr ? *NodeIDPtr : NodeIDInvalid;
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::Merge(FEDLCookChecker&& Other)
|
||
|
|
{
|
||
|
|
if (Nodes.Num() == 0)
|
||
|
|
{
|
||
|
|
Swap(Nodes, Other.Nodes);
|
||
|
|
Swap(NodeHashToNodeID, Other.NodeHashToNodeID);
|
||
|
|
Swap(NodePrereqs, Other.NodePrereqs);
|
||
|
|
|
||
|
|
// Switch the pointers in all of the swapped data to point at this instead of Other
|
||
|
|
for (TPair<FEDLNodeHash, FEDLNodeID>& KVPair: NodeHashToNodeID)
|
||
|
|
{
|
||
|
|
FEDLNodeHash& NodeHash = KVPair.Key;
|
||
|
|
if (NodeHash.HashMode == FEDLNodeHash::EHashMode::Node)
|
||
|
|
{
|
||
|
|
NodeHash.Nodes = &Nodes;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Other.NodeHashToNodeID.Empty(); // We will be invalidating the data these NodeHashes point to in the Other.Nodes loop, so empty the array now to avoid using it by accident
|
||
|
|
|
||
|
|
TArray<FEDLNodeID> RemapIDs;
|
||
|
|
RemapIDs.Reserve(Other.Nodes.Num());
|
||
|
|
for (FEDLNodeData& NodeData: Other.Nodes)
|
||
|
|
{
|
||
|
|
FEDLNodeID ParentID;
|
||
|
|
if (NodeData.ParentID == NodeIDInvalid)
|
||
|
|
{
|
||
|
|
ParentID = NodeIDInvalid;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
// Parents should be earlier in the nodes list than children, since we always FindOrAdd the parent (and hence add it to the nodelist) when creating the child.
|
||
|
|
// Since the parent is earlier in the nodes list, we have already transferred it, and its ID in this->Nodes is therefore RemapIDs[Other.ParentID]
|
||
|
|
check(NodeData.ParentID < NodeData.ID);
|
||
|
|
ParentID = RemapIDs[NodeData.ParentID];
|
||
|
|
}
|
||
|
|
|
||
|
|
bool bNew;
|
||
|
|
FEDLNodeID NodeID = FindOrAddNode(MoveTemp(NodeData), Other, ParentID, bNew);
|
||
|
|
if (!bNew)
|
||
|
|
{
|
||
|
|
Nodes[NodeID].Merge(MoveTemp(NodeData));
|
||
|
|
}
|
||
|
|
RemapIDs.Add(NodeID);
|
||
|
|
}
|
||
|
|
|
||
|
|
for (const TPair<FEDLNodeID, FEDLNodeID>& Prereq: Other.NodePrereqs)
|
||
|
|
{
|
||
|
|
FEDLNodeID SourceID = RemapIDs[Prereq.Key];
|
||
|
|
FEDLNodeID TargetID = RemapIDs[Prereq.Value];
|
||
|
|
AddDependency(SourceID, TargetID);
|
||
|
|
}
|
||
|
|
|
||
|
|
Other.NodePrereqs.Empty();
|
||
|
|
Other.Nodes.Empty();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
void FEDLCookChecker::Verify(bool bFullReferencesExpected)
|
||
|
|
{
|
||
|
|
check(!GIsSavingPackage);
|
||
|
|
|
||
|
|
FEDLCookChecker Accumulator(EInternalConstruct::Type);
|
||
|
|
|
||
|
|
{
|
||
|
|
FScopeLock CookCheckerInstanceLock(&CookCheckerInstanceCritical);
|
||
|
|
for (FEDLCookChecker* Checker: CookCheckerInstances)
|
||
|
|
{
|
||
|
|
if (Checker->bIsActive)
|
||
|
|
{
|
||
|
|
Accumulator.bIsActive = true;
|
||
|
|
Accumulator.Merge(MoveTemp(*Checker));
|
||
|
|
}
|
||
|
|
Checker->Reset();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (Accumulator.bIsActive)
|
||
|
|
{
|
||
|
|
double StartTime = FPlatformTime::Seconds();
|
||
|
|
|
||
|
|
if (bFullReferencesExpected)
|
||
|
|
{
|
||
|
|
// imports to things that are not exports...
|
||
|
|
for (const FEDLNodeData& NodeData: Accumulator.Nodes)
|
||
|
|
{
|
||
|
|
if (NodeData.bIsExport)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Any imports of this non-exported node are an error; log them all if they exist
|
||
|
|
for (FName PackageName: NodeData.ImportingPackagesSorted)
|
||
|
|
{
|
||
|
|
UE_LOG(LogSavePackage, Warning, TEXT("%s imported %s, but it was never saved as an export."), *PackageName.ToString(), *NodeData.ToString(Accumulator));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// cycles in the dep graph
|
||
|
|
TSet<FEDLNodeID> Visited;
|
||
|
|
TSet<FEDLNodeID> Stack;
|
||
|
|
bool bHadCycle = false;
|
||
|
|
for (const FEDLNodeData& NodeData: Accumulator.Nodes)
|
||
|
|
{
|
||
|
|
if (!NodeData.bIsExport)
|
||
|
|
{
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
FEDLNodeID FailNode;
|
||
|
|
if (Accumulator.CheckForCyclesInner(Visited, Stack, NodeData.ID, FailNode))
|
||
|
|
{
|
||
|
|
UE_LOG(LogSavePackage, Error, TEXT("----- %s contained a cycle (listed above)."), *Accumulator.Nodes[FailNode].ToString(Accumulator));
|
||
|
|
bHadCycle = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (bHadCycle)
|
||
|
|
{
|
||
|
|
UE_LOG(LogSavePackage, Fatal, TEXT("EDL dep graph contained a cycle (see errors, above). This is fatal at runtime so it is fatal at cook time."));
|
||
|
|
}
|
||
|
|
UE_LOG(LogSavePackage, Display, TEXT("Took %fs to verify the EDL loading graph."), float(FPlatformTime::Seconds() - StartTime));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FCriticalSection FEDLCookChecker::CookCheckerInstanceCritical;
|
||
|
|
TArray<FEDLCookChecker*> FEDLCookChecker::CookCheckerInstances;
|
||
|
|
|
||
|
|
void StartSavingEDLCookInfoForVerification()
|
||
|
|
{
|
||
|
|
FEDLCookChecker::StartSavingEDLCookInfoForVerification();
|
||
|
|
}
|
||
|
|
|
||
|
|
void VerifyEDLCookInfo(bool bFullReferencesExpected)
|
||
|
|
{
|
||
|
|
FEDLCookChecker::Verify(bFullReferencesExpected);
|
||
|
|
}
|
||
|
|
|
||
|
|
FScopedSavingFlag::FScopedSavingFlag(bool InSavingConcurrent)
|
||
|
|
: bSavingConcurrent(InSavingConcurrent)
|
||
|
|
{
|
||
|
|
check(!IsGarbageCollecting());
|
||
|
|
|
||
|
|
// We need the same lock as GC so that no StaticFindObject can happen in parallel to saving a package
|
||
|
|
if (IsInGameThread())
|
||
|
|
{
|
||
|
|
FGCCSyncObject::Get().GCLock();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
FGCCSyncObject::Get().LockAsync();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do not change GIsSavingPackage while saving concurrently. It should have been set before and after all packages are saved
|
||
|
|
if (!bSavingConcurrent)
|
||
|
|
{
|
||
|
|
GIsSavingPackage = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FScopedSavingFlag::~FScopedSavingFlag()
|
||
|
|
{
|
||
|
|
if (!bSavingConcurrent)
|
||
|
|
{
|
||
|
|
GIsSavingPackage = false;
|
||
|
|
}
|
||
|
|
if (IsInGameThread())
|
||
|
|
{
|
||
|
|
FGCCSyncObject::Get().GCUnlock();
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
FGCCSyncObject::Get().UnlockAsync();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FSavePackageDiffSettings::FSavePackageDiffSettings(bool bDiffing)
|
||
|
|
: MaxDiffsToLog(5), bIgnoreHeaderDiffs(false), bSaveForDiff(false)
|
||
|
|
{
|
||
|
|
if (bDiffing)
|
||
|
|
{
|
||
|
|
GConfig->GetInt(TEXT("CookSettings"), TEXT("MaxDiffsToLog"), MaxDiffsToLog, GEditorIni);
|
||
|
|
// Command line override for MaxDiffsToLog
|
||
|
|
FParse::Value(FCommandLine::Get(), TEXT("MaxDiffstoLog="), MaxDiffsToLog);
|
||
|
|
|
||
|
|
GConfig->GetBool(TEXT("CookSettings"), TEXT("IgnoreHeaderDiffs"), bIgnoreHeaderDiffs, GEditorIni);
|
||
|
|
// Command line override for IgnoreHeaderDiffs
|
||
|
|
if (bIgnoreHeaderDiffs)
|
||
|
|
{
|
||
|
|
bIgnoreHeaderDiffs = !FParse::Param(FCommandLine::Get(), TEXT("HeaderDiffs"));
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
bIgnoreHeaderDiffs = FParse::Param(FCommandLine::Get(), TEXT("IgnoreHeaderDiffs"));
|
||
|
|
}
|
||
|
|
bSaveForDiff = FParse::Param(FCommandLine::Get(), TEXT("SaveForDiff"));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
FCanSkipEditorReferencedPackagesWhenCooking::FCanSkipEditorReferencedPackagesWhenCooking()
|
||
|
|
: bCanSkipEditorReferencedPackagesWhenCooking(true)
|
||
|
|
{
|
||
|
|
GConfig->GetBool(TEXT("Core.System"), TEXT("CanSkipEditorReferencedPackagesWhenCooking"), bCanSkipEditorReferencedPackagesWhenCooking, GEngineIni);
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace SavePackageUtilities
|
||
|
|
{
|
||
|
|
/**
|
||
|
|
* Static: Saves thumbnail data for the specified package outer and linker
|
||
|
|
*
|
||
|
|
* @param InOuter the outer to use for the new package
|
||
|
|
* @param Linker linker we're currently saving with
|
||
|
|
* @param Slot structed archive slot we are saving too (temporary)
|
||
|
|
*/
|
||
|
|
void SaveThumbnails(UPackage* InOuter, FLinkerSave* Linker, FStructuredArchive::FSlot Slot)
|
||
|
|
{
|
||
|
|
FStructuredArchive::FRecord Record = Slot.EnterRecord();
|
||
|
|
|
||
|
|
Linker->Summary.ThumbnailTableOffset = 0;
|
||
|
|
|
||
|
|
#if WITH_EDITORONLY_DATA
|
||
|
|
// Do we have any thumbnails to save?
|
||
|
|
if (!(Linker->Summary.PackageFlags & PKG_FilterEditorOnly) && InOuter->HasThumbnailMap())
|
||
|
|
{
|
||
|
|
const FThumbnailMap& PackageThumbnailMap = InOuter->GetThumbnailMap();
|
||
|
|
|
||
|
|
// Figure out which objects have thumbnails. Note that we only want to save thumbnails
|
||
|
|
// for objects that are actually in the export map. This is so that we avoid saving out
|
||
|
|
// thumbnails that were cached for deleted objects and such.
|
||
|
|
TArray<FObjectFullNameAndThumbnail> ObjectsWithThumbnails;
|
||
|
|
for (int32 i = 0; i < Linker->ExportMap.Num(); i++)
|
||
|
|
{
|
||
|
|
FObjectExport& Export = Linker->ExportMap[i];
|
||
|
|
if (Export.Object)
|
||
|
|
{
|
||
|
|
const FName ObjectFullName(*Export.Object->GetFullName());
|
||
|
|
const FObjectThumbnail* ObjectThumbnail = PackageThumbnailMap.Find(ObjectFullName);
|
||
|
|
|
||
|
|
// if we didn't find the object via full name, try again with ??? as the class name, to support having
|
||
|
|
// loaded old packages without going through the editor (ie cooking old packages)
|
||
|
|
if (ObjectThumbnail == nullptr)
|
||
|
|
{
|
||
|
|
// can't overwrite ObjectFullName, so that we add it properly to the map
|
||
|
|
FName OldPackageStyleObjectFullName = FName(*FString::Printf(TEXT("??? %s"), *Export.Object->GetPathName()));
|
||
|
|
ObjectThumbnail = PackageThumbnailMap.Find(OldPackageStyleObjectFullName);
|
||
|
|
}
|
||
|
|
if (ObjectThumbnail != nullptr)
|
||
|
|
{
|
||
|
|
// IMPORTANT: We save all thumbnails here, even if they are a shared (empty) thumbnail!
|
||
|
|
// Empty thumbnails let us know that an asset is in a package without having to
|
||
|
|
// make a linker for it.
|
||
|
|
ObjectsWithThumbnails.Add(FObjectFullNameAndThumbnail(ObjectFullName, ObjectThumbnail));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// preserve thumbnail rendered for the level
|
||
|
|
const FObjectThumbnail* ObjectThumbnail = PackageThumbnailMap.Find(FName(*InOuter->GetFullName()));
|
||
|
|
if (ObjectThumbnail != nullptr)
|
||
|
|
{
|
||
|
|
ObjectsWithThumbnails.Add(FObjectFullNameAndThumbnail(FName(*InOuter->GetFullName()), ObjectThumbnail));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Do we have any thumbnails? If so, we'll save them out along with a table of contents
|
||
|
|
if (ObjectsWithThumbnails.Num() > 0)
|
||
|
|
{
|
||
|
|
// Save out the image data for the thumbnails
|
||
|
|
FStructuredArchive::FStream ThumbnailStream = Record.EnterStream(SA_FIELD_NAME(TEXT("Thumbnails")));
|
||
|
|
|
||
|
|
for (int32 CurObjectIndex = 0; CurObjectIndex < ObjectsWithThumbnails.Num(); ++CurObjectIndex)
|
||
|
|
{
|
||
|
|
FObjectFullNameAndThumbnail& CurObjectThumb = ObjectsWithThumbnails[CurObjectIndex];
|
||
|
|
|
||
|
|
// Store the file offset to this thumbnail
|
||
|
|
CurObjectThumb.FileOffset = Linker->Tell();
|
||
|
|
|
||
|
|
// Serialize the thumbnail!
|
||
|
|
FObjectThumbnail* SerializableThumbnail = const_cast<FObjectThumbnail*>(CurObjectThumb.ObjectThumbnail);
|
||
|
|
SerializableThumbnail->Serialize(ThumbnailStream.EnterElement());
|
||
|
|
}
|
||
|
|
|
||
|
|
// Store the thumbnail table of contents
|
||
|
|
{
|
||
|
|
Linker->Summary.ThumbnailTableOffset = Linker->Tell();
|
||
|
|
|
||
|
|
// Save number of thumbnails
|
||
|
|
int32 ThumbnailCount = ObjectsWithThumbnails.Num();
|
||
|
|
FStructuredArchive::FArray IndexArray = Record.EnterField(SA_FIELD_NAME(TEXT("Index"))).EnterArray(ThumbnailCount);
|
||
|
|
|
||
|
|
// Store a list of object names along with the offset in the file where the thumbnail is stored
|
||
|
|
for (int32 CurObjectIndex = 0; CurObjectIndex < ObjectsWithThumbnails.Num(); ++CurObjectIndex)
|
||
|
|
{
|
||
|
|
const FObjectFullNameAndThumbnail& CurObjectThumb = ObjectsWithThumbnails[CurObjectIndex];
|
||
|
|
|
||
|
|
// Object name
|
||
|
|
const FString ObjectFullName = CurObjectThumb.ObjectFullName.ToString();
|
||
|
|
|
||
|
|
// Break the full name into it's class and path name parts
|
||
|
|
const int32 FirstSpaceIndex = ObjectFullName.Find(TEXT(" "));
|
||
|
|
check(FirstSpaceIndex != INDEX_NONE && FirstSpaceIndex > 0);
|
||
|
|
FString ObjectClassName = ObjectFullName.Left(FirstSpaceIndex);
|
||
|
|
const FString ObjectPath = ObjectFullName.Mid(FirstSpaceIndex + 1);
|
||
|
|
|
||
|
|
// Remove the package name from the object path since that will be implicit based
|
||
|
|
// on the package file name
|
||
|
|
FString ObjectPathWithoutPackageName = ObjectPath.Mid(ObjectPath.Find(TEXT(".")) + 1);
|
||
|
|
|
||
|
|
// File offset for the thumbnail (already saved out.)
|
||
|
|
int32 FileOffset = CurObjectThumb.FileOffset;
|
||
|
|
|
||
|
|
IndexArray.EnterElement().EnterRecord()
|
||
|
|
<< SA_VALUE(TEXT("ObjectClassName"), ObjectClassName)
|
||
|
|
<< SA_VALUE(TEXT("ObjectPathWithoutPackageName"), ObjectPathWithoutPackageName)
|
||
|
|
<< SA_VALUE(TEXT("FileOffset"), FileOffset);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// if content browser isn't enabled, clear the thumbnail map so we're not using additional memory for nothing
|
||
|
|
if (!GIsEditor || IsRunningCommandlet())
|
||
|
|
{
|
||
|
|
InOuter->ThumbnailMap.Reset();
|
||
|
|
}
|
||
|
|
#endif
|
||
|
|
}
|
||
|
|
|
||
|
|
void SaveBulkData(FLinkerSave* Linker, const UPackage* InOuter, const TCHAR* Filename, const ITargetPlatform* TargetPlatform,
|
||
|
|
FSavePackageContext* SavePackageContext, const bool bTextFormat, const bool bDiffing, const bool bComputeHash, TAsyncWorkSequence<FMD5>& AsyncWriteAndHashSequence, int64& TotalPackageSizeUncompressed)
|
||
|
|
{
|
||
|
|
// Now we write all the bulkdata that is supposed to be at the end of the package
|
||
|
|
// and fix up the offset
|
||
|
|
const int64 StartOfBulkDataArea = Linker->Tell();
|
||
|
|
Linker->Summary.BulkDataStartOffset = StartOfBulkDataArea;
|
||
|
|
|
||
|
|
check(!bTextFormat || Linker->BulkDataToAppend.Num() == 0);
|
||
|
|
|
||
|
|
if (!bTextFormat && Linker->BulkDataToAppend.Num() > 0)
|
||
|
|
{
|
||
|
|
COOK_STAT(FScopedDurationTimer SaveTimer(FSavePackageStats::SerializeBulkDataTimeSec));
|
||
|
|
|
||
|
|
FScopedSlowTask BulkDataFeedback(Linker->BulkDataToAppend.Num());
|
||
|
|
|
||
|
|
class FLargeMemoryWriterWithRegions: public FLargeMemoryWriter
|
||
|
|
{
|
||
|
|
public:
|
||
|
|
FLargeMemoryWriterWithRegions()
|
||
|
|
: FLargeMemoryWriter(0, /* IsPersistent */ true)
|
||
|
|
{}
|
||
|
|
|
||
|
|
TArray<FFileRegion> FileRegions;
|
||
|
|
};
|
||
|
|
|
||
|
|
TUniquePtr<FLargeMemoryWriterWithRegions> BulkArchive;
|
||
|
|
TUniquePtr<FLargeMemoryWriterWithRegions> OptionalBulkArchive;
|
||
|
|
TUniquePtr<FLargeMemoryWriterWithRegions> MappedBulkArchive;
|
||
|
|
|
||
|
|
uint32 ExtraBulkDataFlags = 0;
|
||
|
|
|
||
|
|
static const struct FUseSeparateBulkDataFiles
|
||
|
|
{
|
||
|
|
bool bEnable = false;
|
||
|
|
|
||
|
|
FUseSeparateBulkDataFiles()
|
||
|
|
{
|
||
|
|
GConfig->GetBool(TEXT("Core.System"), TEXT("UseSeperateBulkDataFiles"), /* out */ bEnable, GEngineIni);
|
||
|
|
|
||
|
|
if (IsEventDrivenLoaderEnabledInCookedBuilds())
|
||
|
|
{
|
||
|
|
// Always split bulk data when splitting cooked files
|
||
|
|
bEnable = true;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} ShouldUseSeparateBulkDataFiles;
|
||
|
|
|
||
|
|
const bool bShouldUseSeparateBulkFile = ShouldUseSeparateBulkDataFiles.bEnable && Linker->IsCooking();
|
||
|
|
|
||
|
|
if (bShouldUseSeparateBulkFile)
|
||
|
|
{
|
||
|
|
ExtraBulkDataFlags = BULKDATA_PayloadInSeperateFile;
|
||
|
|
|
||
|
|
BulkArchive.Reset(new FLargeMemoryWriterWithRegions);
|
||
|
|
OptionalBulkArchive.Reset(new FLargeMemoryWriterWithRegions);
|
||
|
|
MappedBulkArchive.Reset(new FLargeMemoryWriterWithRegions);
|
||
|
|
}
|
||
|
|
|
||
|
|
// If we are not allowing BulkData to go to the IoStore and we will be saving the BulkData to a separate file then
|
||
|
|
// we cannot manipulate the offset as we cannot 'fix' it at runtime with the AsyncLoader2
|
||
|
|
//
|
||
|
|
// We should remove the manipulated offset entirely, at least for separate files but for now we need to leave it to
|
||
|
|
// prevent larger patching sizes.
|
||
|
|
if (SavePackageContext != nullptr && SavePackageContext->bForceLegacyOffsets == false && bShouldUseSeparateBulkFile)
|
||
|
|
{
|
||
|
|
ExtraBulkDataFlags |= BULKDATA_NoOffsetFixUp;
|
||
|
|
}
|
||
|
|
|
||
|
|
bool bAlignBulkData = false;
|
||
|
|
bool bUseFileRegions = false;
|
||
|
|
int64 BulkDataAlignment = 0;
|
||
|
|
|
||
|
|
if (TargetPlatform)
|
||
|
|
{
|
||
|
|
bAlignBulkData = TargetPlatform->SupportsFeature(ETargetPlatformFeatures::MemoryMappedFiles);
|
||
|
|
bUseFileRegions = TargetPlatform->SupportsFeature(ETargetPlatformFeatures::CookFileRegionMetadata);
|
||
|
|
BulkDataAlignment = TargetPlatform->GetMemoryMappingAlignment();
|
||
|
|
}
|
||
|
|
|
||
|
|
uint16 BulkDataIndex = 1;
|
||
|
|
for (FLinkerSave::FBulkDataStorageInfo& BulkDataStorageInfo: Linker->BulkDataToAppend)
|
||
|
|
{
|
||
|
|
BulkDataFeedback.EnterProgressFrame();
|
||
|
|
|
||
|
|
// Set bulk data flags to what they were during initial serialization (they might have changed after that)
|
||
|
|
const uint32 OldBulkDataFlags = BulkDataStorageInfo.BulkData->GetBulkDataFlags();
|
||
|
|
uint32 ModifiedBulkDataFlags = BulkDataStorageInfo.BulkDataFlags | ExtraBulkDataFlags;
|
||
|
|
const bool bBulkItemIsOptional = (ModifiedBulkDataFlags & BULKDATA_OptionalPayload) != 0;
|
||
|
|
bool bBulkItemIsMapped = bAlignBulkData && ((ModifiedBulkDataFlags & BULKDATA_MemoryMappedPayload) != 0);
|
||
|
|
|
||
|
|
if (bBulkItemIsMapped && bBulkItemIsOptional)
|
||
|
|
{
|
||
|
|
UE_LOG(LogSavePackage, Warning, TEXT("%s has bulk data that is both mapped and optional. This is not currently supported. Will not be mapped."), Filename);
|
||
|
|
ModifiedBulkDataFlags &= ~BULKDATA_MemoryMappedPayload;
|
||
|
|
bBulkItemIsMapped = false;
|
||
|
|
}
|
||
|
|
|
||
|
|
BulkDataStorageInfo.BulkData->ClearBulkDataFlags(0xFFFFFFFF);
|
||
|
|
BulkDataStorageInfo.BulkData->SetBulkDataFlags(ModifiedBulkDataFlags);
|
||
|
|
|
||
|
|
TArray<FFileRegion>* TargetRegions = &Linker->FileRegions;
|
||
|
|
FArchive* TargetArchive = Linker;
|
||
|
|
|
||
|
|
if (bShouldUseSeparateBulkFile)
|
||
|
|
{
|
||
|
|
if (bBulkItemIsOptional)
|
||
|
|
{
|
||
|
|
TargetArchive = OptionalBulkArchive.Get();
|
||
|
|
TargetRegions = &OptionalBulkArchive->FileRegions;
|
||
|
|
}
|
||
|
|
else if (bBulkItemIsMapped)
|
||
|
|
{
|
||
|
|
TargetArchive = MappedBulkArchive.Get();
|
||
|
|
TargetRegions = &MappedBulkArchive->FileRegions;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
TargetArchive = BulkArchive.Get();
|
||
|
|
TargetRegions = &BulkArchive->FileRegions;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
check(TargetArchive && TargetRegions);
|
||
|
|
|
||
|
|
// Pad archive for proper alignment for memory mapping
|
||
|
|
if (bBulkItemIsMapped && BulkDataAlignment > 0)
|
||
|
|
{
|
||
|
|
const int64 BulkStartOffset = TargetArchive->Tell();
|
||
|
|
|
||
|
|
if (!IsAligned(BulkStartOffset, BulkDataAlignment))
|
||
|
|
{
|
||
|
|
const int64 AlignedOffset = Align(BulkStartOffset, BulkDataAlignment);
|
||
|
|
|
||
|
|
int64 Padding = AlignedOffset - BulkStartOffset;
|
||
|
|
check(Padding > 0);
|
||
|
|
|
||
|
|
uint64 Zero64 = 0;
|
||
|
|
while (Padding >= 8)
|
||
|
|
{
|
||
|
|
*TargetArchive << Zero64;
|
||
|
|
Padding -= 8;
|
||
|
|
}
|
||
|
|
|
||
|
|
uint8 Zero8 = 0;
|
||
|
|
while (Padding > 0)
|
||
|
|
{
|
||
|
|
*TargetArchive << Zero8;
|
||
|
|
Padding--;
|
||
|
|
}
|
||
|
|
|
||
|
|
check(TargetArchive->Tell() == AlignedOffset);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const int64 BulkStartOffset = TargetArchive->Tell();
|
||
|
|
|
||
|
|
int64 StoredBulkStartOffset = (ModifiedBulkDataFlags & BULKDATA_NoOffsetFixUp) == 0 ? BulkStartOffset - StartOfBulkDataArea : BulkStartOffset;
|
||
|
|
|
||
|
|
BulkDataStorageInfo.BulkData->SerializeBulkData(*TargetArchive, BulkDataStorageInfo.BulkData->Lock(LOCK_READ_ONLY));
|
||
|
|
|
||
|
|
int64 BulkEndOffset = TargetArchive->Tell();
|
||
|
|
const int64 LinkerEndOffset = Linker->Tell();
|
||
|
|
|
||
|
|
int64 SizeOnDisk = BulkEndOffset - BulkStartOffset;
|
||
|
|
|
||
|
|
Linker->Seek(BulkDataStorageInfo.BulkDataFlagsPos);
|
||
|
|
*Linker << ModifiedBulkDataFlags;
|
||
|
|
|
||
|
|
Linker->Seek(BulkDataStorageInfo.BulkDataOffsetInFilePos);
|
||
|
|
*Linker << StoredBulkStartOffset;
|
||
|
|
|
||
|
|
Linker->Seek(BulkDataStorageInfo.BulkDataSizeOnDiskPos);
|
||
|
|
if (ModifiedBulkDataFlags & BULKDATA_Size64Bit)
|
||
|
|
{
|
||
|
|
*Linker << SizeOnDisk;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
check(SizeOnDisk < (1LL << 31));
|
||
|
|
int32 SizeOnDiskAsInt32 = SizeOnDisk;
|
||
|
|
*Linker << SizeOnDiskAsInt32;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (SavePackageContext != nullptr && SavePackageContext->BulkDataManifest != nullptr)
|
||
|
|
{
|
||
|
|
auto BulkDataTypeFromFlags = [](uint32 BulkDataFlags)
|
||
|
|
{
|
||
|
|
if (BulkDataFlags & BULKDATA_MemoryMappedPayload)
|
||
|
|
{
|
||
|
|
return FPackageStoreBulkDataManifest::EBulkdataType::MemoryMapped;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (BulkDataFlags & BULKDATA_OptionalPayload)
|
||
|
|
{
|
||
|
|
return FPackageStoreBulkDataManifest::EBulkdataType::Optional;
|
||
|
|
}
|
||
|
|
|
||
|
|
return FPackageStoreBulkDataManifest::EBulkdataType::Normal;
|
||
|
|
};
|
||
|
|
|
||
|
|
const FPackageStoreBulkDataManifest::EBulkdataType Type = BulkDataTypeFromFlags(BulkDataStorageInfo.BulkDataFlags);
|
||
|
|
|
||
|
|
SavePackageContext->BulkDataManifest->AddFileAccess(Filename, Type, StoredBulkStartOffset, BulkStartOffset, SizeOnDisk);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (bUseFileRegions && BulkDataStorageInfo.BulkDataFileRegionType != EFileRegionType::None && SizeOnDisk > 0)
|
||
|
|
{
|
||
|
|
TargetRegions->Add(FFileRegion(BulkStartOffset, SizeOnDisk, BulkDataStorageInfo.BulkDataFileRegionType));
|
||
|
|
}
|
||
|
|
|
||
|
|
Linker->Seek(LinkerEndOffset);
|
||
|
|
|
||
|
|
// Restore BulkData flags to before serialization started
|
||
|
|
BulkDataStorageInfo.BulkData->ClearBulkDataFlags(0xFFFFFFFF);
|
||
|
|
BulkDataStorageInfo.BulkData->SetBulkDataFlags(OldBulkDataFlags);
|
||
|
|
BulkDataStorageInfo.BulkData->Unlock();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (BulkArchive)
|
||
|
|
{
|
||
|
|
check(OptionalBulkArchive);
|
||
|
|
check(MappedBulkArchive);
|
||
|
|
|
||
|
|
const bool bWriteBulkToDisk = !bDiffing;
|
||
|
|
|
||
|
|
if (SavePackageContext != nullptr && SavePackageContext->PackageStoreWriter != nullptr && bWriteBulkToDisk)
|
||
|
|
{
|
||
|
|
auto AddSizeAndConvertToIoBuffer = [&TotalPackageSizeUncompressed](FLargeMemoryWriter* Writer)
|
||
|
|
{
|
||
|
|
const int64 TotalSize = Writer->TotalSize();
|
||
|
|
TotalPackageSizeUncompressed += TotalSize;
|
||
|
|
return FIoBuffer(FIoBuffer::AssumeOwnership, Writer->ReleaseOwnership(), TotalSize);
|
||
|
|
};
|
||
|
|
|
||
|
|
FPackageStoreWriter::FBulkDataInfo BulkInfo;
|
||
|
|
BulkInfo.PackageName = InOuter->GetFName();
|
||
|
|
BulkInfo.LooseFilePath = Filename;
|
||
|
|
BulkInfo.BulkdataType = FPackageStoreWriter::FBulkDataInfo::Standard;
|
||
|
|
|
||
|
|
SavePackageContext->PackageStoreWriter->WriteBulkdata(BulkInfo, AddSizeAndConvertToIoBuffer(BulkArchive.Get()), BulkArchive->FileRegions);
|
||
|
|
|
||
|
|
BulkInfo.BulkdataType = FPackageStoreWriter::FBulkDataInfo::Optional;
|
||
|
|
SavePackageContext->PackageStoreWriter->WriteBulkdata(BulkInfo, AddSizeAndConvertToIoBuffer(OptionalBulkArchive.Get()), OptionalBulkArchive->FileRegions);
|
||
|
|
|
||
|
|
BulkInfo.BulkdataType = FPackageStoreWriter::FBulkDataInfo::Mmap;
|
||
|
|
SavePackageContext->PackageStoreWriter->WriteBulkdata(BulkInfo, AddSizeAndConvertToIoBuffer(MappedBulkArchive.Get()), MappedBulkArchive->FileRegions);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
auto WriteBulkData = [&](FLargeMemoryWriterWithRegions* Archive, const TCHAR* BulkFileExtension)
|
||
|
|
{
|
||
|
|
if (const int64 DataSize = Archive->TotalSize())
|
||
|
|
{
|
||
|
|
TotalPackageSizeUncompressed += DataSize;
|
||
|
|
|
||
|
|
if (bComputeHash || bWriteBulkToDisk)
|
||
|
|
{
|
||
|
|
FLargeMemoryPtr DataPtr(Archive->ReleaseOwnership());
|
||
|
|
|
||
|
|
const FString ArchiveFilename = FPaths::ChangeExtension(Filename, BulkFileExtension);
|
||
|
|
|
||
|
|
EAsyncWriteOptions WriteOptions(EAsyncWriteOptions::None);
|
||
|
|
if (bComputeHash)
|
||
|
|
{
|
||
|
|
WriteOptions |= EAsyncWriteOptions::ComputeHash;
|
||
|
|
}
|
||
|
|
if (bWriteBulkToDisk)
|
||
|
|
{
|
||
|
|
WriteOptions |= EAsyncWriteOptions::WriteFileToDisk;
|
||
|
|
}
|
||
|
|
SavePackageUtilities::AsyncWriteFile(AsyncWriteAndHashSequence, MoveTemp(DataPtr), DataSize, *ArchiveFilename, WriteOptions, Archive->FileRegions);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
WriteBulkData(BulkArchive.Get(), TEXT(".ubulk")); // Regular separate bulk data file
|
||
|
|
WriteBulkData(OptionalBulkArchive.Get(), TEXT(".uptnl")); // Optional bulk data
|
||
|
|
WriteBulkData(MappedBulkArchive.Get(), TEXT(".m.ubulk")); // Memory-mapped bulk data
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Linker->BulkDataToAppend.Empty();
|
||
|
|
}
|
||
|
|
|
||
|
|
void SaveWorldLevelInfo(UPackage* InOuter, FLinkerSave* Linker, FStructuredArchive::FRecord Record)
|
||
|
|
{
|
||
|
|
Linker->Summary.WorldTileInfoDataOffset = 0;
|
||
|
|
|
||
|
|
if (InOuter->WorldTileInfo.IsValid())
|
||
|
|
{
|
||
|
|
Linker->Summary.WorldTileInfoDataOffset = Linker->Tell();
|
||
|
|
Record << SA_VALUE(TEXT("WorldLevelInfo"), *(InOuter->WorldTileInfo));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
} // end namespace SavePackageUtilities
|
||
|
|
|
||
|
|
void UPackage::WaitForAsyncFileWrites()
|
||
|
|
{
|
||
|
|
while (OutstandingAsyncWrites.GetValue())
|
||
|
|
{
|
||
|
|
FPlatformProcess::Sleep(0.0f);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
bool UPackage::IsEmptyPackage(UPackage* Package, const UObject* LastReferencer)
|
||
|
|
{
|
||
|
|
// Don't count null or volatile packages as empty, just let them be NULL or get GCed
|
||
|
|
if (Package != nullptr)
|
||
|
|
{
|
||
|
|
// Make sure the package is fully loaded before determining if it is empty
|
||
|
|
if (!Package->IsFullyLoaded())
|
||
|
|
{
|
||
|
|
Package->FullyLoad();
|
||
|
|
}
|
||
|
|
|
||
|
|
bool bIsEmpty = true;
|
||
|
|
ForEachObjectWithPackage(Package, [LastReferencer, &bIsEmpty](UObject* InObject)
|
||
|
|
{
|
||
|
|
// if the package contains at least one object that has asset registry data and isn't the `LastReferencer` consider it not empty
|
||
|
|
if (InObject->IsAsset() && InObject != LastReferencer)
|
||
|
|
{
|
||
|
|
bIsEmpty = false;
|
||
|
|
// we can break out of the iteration as soon as we find one valid object
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
// Don't consider transient, class default or pending kill objects
|
||
|
|
},
|
||
|
|
false, RF_Transient | RF_ClassDefaultObject, EInternalObjectFlags::PendingKill);
|
||
|
|
return bIsEmpty;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Invalid package
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
namespace UE
|
||
|
|
{
|
||
|
|
namespace AssetRegistry
|
||
|
|
{
|
||
|
|
// See the corresponding ReadPackageDataMain and ReadPackageDataDependencies defined in PackageReader.cpp in AssetRegistry module
|
||
|
|
void WritePackageData(FStructuredArchiveRecord& ParentRecord, bool bIsCooking, const UPackage* Package, FLinkerSave* Linker, const TSet<UObject*>& ImportsUsedInGame, const TSet<FName>& SoftPackagesUsedInGame)
|
||
|
|
{
|
||
|
|
// To avoid large patch sizes, we have frozen cooked package format at the format before VER_UE4_ASSETREGISTRY_DEPENDENCYFLAGS
|
||
|
|
bool bPreDependencyFormat = bIsCooking;
|
||
|
|
|
||
|
|
// WritePackageData is currently only called if not bTextFormat; we rely on that to save offsets
|
||
|
|
FArchive& BinaryArchive = ParentRecord.GetUnderlyingArchive();
|
||
|
|
check(!BinaryArchive.IsTextFormat());
|
||
|
|
|
||
|
|
// Store the asset registry offset in the file and enter a record for the asset registry data
|
||
|
|
Linker->Summary.AssetRegistryDataOffset = BinaryArchive.Tell();
|
||
|
|
FStructuredArchiveRecord AssetRegistryRecord = ParentRecord.EnterField(SA_FIELD_NAME(TEXT("AssetRegistry"))).EnterRecord();
|
||
|
|
|
||
|
|
// Offset to Dependencies
|
||
|
|
int64 OffsetToAssetRegistryDependencyDataOffset = INDEX_NONE;
|
||
|
|
if (!bPreDependencyFormat)
|
||
|
|
{
|
||
|
|
// Write placeholder data for the offset to the separately-serialized AssetRegistryDependencyData
|
||
|
|
OffsetToAssetRegistryDependencyDataOffset = BinaryArchive.Tell();
|
||
|
|
int64 AssetRegistryDependencyDataOffset = 0;
|
||
|
|
AssetRegistryRecord << SA_VALUE(TEXT("AssetRegistryDependencyDataOffset"), AssetRegistryDependencyDataOffset);
|
||
|
|
check(BinaryArchive.Tell() == OffsetToAssetRegistryDependencyDataOffset + sizeof(AssetRegistryDependencyDataOffset));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Collect the tag map
|
||
|
|
TArray<UObject*> AssetObjects;
|
||
|
|
if (!(Linker->Summary.PackageFlags & PKG_FilterEditorOnly))
|
||
|
|
{
|
||
|
|
// Find any exports which are not in the tag map
|
||
|
|
for (int32 i = 0; i < Linker->ExportMap.Num(); i++)
|
||
|
|
{
|
||
|
|
FObjectExport& Export = Linker->ExportMap[i];
|
||
|
|
if (Export.Object && Export.Object->IsAsset())
|
||
|
|
{
|
||
|
|
AssetObjects.Add(Export.Object);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
int32 ObjectCount = AssetObjects.Num();
|
||
|
|
FStructuredArchive::FArray AssetArray = AssetRegistryRecord.EnterArray(SA_FIELD_NAME(TEXT("TagMap")), ObjectCount);
|
||
|
|
for (int32 ObjectIdx = 0; ObjectIdx < AssetObjects.Num(); ++ObjectIdx)
|
||
|
|
{
|
||
|
|
const UObject* Object = AssetObjects[ObjectIdx];
|
||
|
|
|
||
|
|
// Exclude the package name in the object path, we just need to know the path relative to the package we are saving
|
||
|
|
FString ObjectPath = Object->GetPathName(Package);
|
||
|
|
FString ObjectClassName = Object->GetClass()->GetName();
|
||
|
|
|
||
|
|
TArray<UObject::FAssetRegistryTag> SourceTags;
|
||
|
|
Object->GetAssetRegistryTags(SourceTags);
|
||
|
|
|
||
|
|
TArray<UObject::FAssetRegistryTag> Tags;
|
||
|
|
for (UObject::FAssetRegistryTag& SourceTag: SourceTags)
|
||
|
|
{
|
||
|
|
UObject::FAssetRegistryTag* Existing = Tags.FindByPredicate([SourceTag](const UObject::FAssetRegistryTag& InTag)
|
||
|
|
{
|
||
|
|
return InTag.Name == SourceTag.Name;
|
||
|
|
});
|
||
|
|
if (Existing)
|
||
|
|
{
|
||
|
|
Existing->Value = SourceTag.Value;
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
Tags.Add(SourceTag);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
int32 TagCount = Tags.Num();
|
||
|
|
|
||
|
|
FStructuredArchive::FRecord AssetRecord = AssetArray.EnterElement().EnterRecord();
|
||
|
|
AssetRecord << SA_VALUE(TEXT("Path"), ObjectPath) << SA_VALUE(TEXT("Class"), ObjectClassName);
|
||
|
|
|
||
|
|
FStructuredArchive::FMap TagMap = AssetRecord.EnterField(SA_FIELD_NAME(TEXT("Tags"))).EnterMap(TagCount);
|
||
|
|
|
||
|
|
for (TArray<UObject::FAssetRegistryTag>::TConstIterator TagIter(Tags); TagIter; ++TagIter)
|
||
|
|
{
|
||
|
|
FString Key = TagIter->Name.ToString();
|
||
|
|
FString Value = TagIter->Value;
|
||
|
|
TagMap.EnterElement(Key) << Value;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (bPreDependencyFormat)
|
||
|
|
{
|
||
|
|
// The legacy format did not write the other sections, or the offsets to those other sections
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Overwrite the placeholder offset for the AssetRegistryDependencyData and enter a record for the asset registry dependency data
|
||
|
|
{
|
||
|
|
int64 AssetRegistryDependencyDataOffset = Linker->Tell();
|
||
|
|
BinaryArchive.Seek(OffsetToAssetRegistryDependencyDataOffset);
|
||
|
|
BinaryArchive << AssetRegistryDependencyDataOffset;
|
||
|
|
BinaryArchive.Seek(AssetRegistryDependencyDataOffset);
|
||
|
|
}
|
||
|
|
FStructuredArchiveRecord DependencyDataRecord = ParentRecord.EnterField(SA_FIELD_NAME(TEXT("AssetRegistryDependencyData"))).EnterRecord();
|
||
|
|
|
||
|
|
// Convert the IsUsedInGame sets into a bitarray with a value per import/softpackagereference
|
||
|
|
TBitArray<> ImportUsedInGameBits;
|
||
|
|
TBitArray<> SoftPackageUsedInGameBits;
|
||
|
|
ImportUsedInGameBits.Reserve(Linker->ImportMap.Num());
|
||
|
|
for (int32 ImportIndex = 0; ImportIndex < Linker->ImportMap.Num(); ++ImportIndex)
|
||
|
|
{
|
||
|
|
ImportUsedInGameBits.Add(ImportsUsedInGame.Contains(Linker->ImportMap[ImportIndex].XObject));
|
||
|
|
}
|
||
|
|
SoftPackageUsedInGameBits.Reserve(Linker->SoftPackageReferenceList.Num());
|
||
|
|
for (int32 SoftPackageIndex = 0; SoftPackageIndex < Linker->SoftPackageReferenceList.Num(); ++SoftPackageIndex)
|
||
|
|
{
|
||
|
|
SoftPackageUsedInGameBits.Add(SoftPackagesUsedInGame.Contains(Linker->SoftPackageReferenceList[SoftPackageIndex]));
|
||
|
|
}
|
||
|
|
|
||
|
|
// Serialize the Dependency section
|
||
|
|
DependencyDataRecord << SA_VALUE(TEXT("ImportUsedInGame"), ImportUsedInGameBits);
|
||
|
|
DependencyDataRecord << SA_VALUE(TEXT("SoftPackageUsedInGame"), SoftPackageUsedInGameBits);
|
||
|
|
}
|
||
|
|
} // namespace AssetRegistry
|
||
|
|
} // namespace UE
|