2286 lines
100 KiB
C++
2286 lines
100 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "UObject/SavePackage.h"
|
|
|
|
#if UE_WITH_SAVEPACKAGE
|
|
#include "Async/ParallelFor.h"
|
|
#include "Blueprint/BlueprintSupport.h"
|
|
#include "HAL/FileManager.h"
|
|
#include "Internationalization/TextPackageNamespaceUtil.h"
|
|
#include "IO/IoDispatcher.h"
|
|
#include "Misc/AssetRegistryInterface.h"
|
|
#include "Misc/ConfigCacheIni.h"
|
|
#include "Misc/FeedbackContext.h"
|
|
#include "Misc/MessageDialog.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "Misc/ScopedSlowTask.h"
|
|
#include "Misc/SecureHash.h"
|
|
#include "ProfilingDebugging/CookStats.h"
|
|
#include "Serialization/ArchiveUObjectFromStructuredArchive.h"
|
|
#include "Serialization/ArchiveStackTrace.h"
|
|
#include "Serialization/Formatters/JsonArchiveOutputFormatter.h"
|
|
#include "Serialization/LargeMemoryWriter.h"
|
|
#include "Serialization/PropertyLocalizationDataGathering.h"
|
|
#include "Serialization/UnversionedPropertySerialization.h"
|
|
#include "UObject/AsyncWorkSequence.h"
|
|
#include "UObject/DebugSerializationFlags.h"
|
|
#include "UObject/EditorObjectVersion.h"
|
|
#include "UObject/LinkerSave.h"
|
|
#include "UObject/Object.h"
|
|
#include "UObject/ObjectMacros.h"
|
|
#include "UObject/ObjectRedirector.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/SavePackage/PackageHarvester.h"
|
|
#include "UObject/SavePackage/SaveContext.h"
|
|
#include "UObject/SavePackage/SavePackageUtilities.h"
|
|
#include "UObject/UObjectGlobals.h"
|
|
#include "UObject/UObjectHash.h"
|
|
|
|
#if ENABLE_COOK_STATS
|
|
#include "ProfilingDebugging/ScopedTimers.h"
|
|
#endif
|
|
|
|
// defined in UObjectGlobals.cpp
|
|
COREUOBJECT_API extern bool GOutputCookingWarnings;
|
|
|
|
// bring the UObectGlobal declaration visible to non editor
|
|
bool IsEditorOnlyObject(const UObject* InObject, bool bCheckRecursive, bool bCheckMarks);
|
|
|
|
ESavePackageResult ReturnSuccessOrCancel()
|
|
{
|
|
return !GWarn->ReceivedUserCancel() ? ESavePackageResult::Success : ESavePackageResult::Canceled;
|
|
}
|
|
|
|
ESavePackageResult ValidateBlueprintNativeCodeGenReplacement(FSaveContext& SaveContext)
|
|
{
|
|
#if WITH_EDITOR
|
|
if (const IBlueprintNativeCodeGenCore* Coordinator = IBlueprintNativeCodeGenCore::Get())
|
|
{
|
|
EReplacementResult ReplacementResult = Coordinator->IsTargetedForReplacement(SaveContext.GetPackage(), Coordinator->GetNativizationOptionsForPlatform(SaveContext.GetTargetPlatform()));
|
|
if (ReplacementResult == EReplacementResult::ReplaceCompletely)
|
|
{
|
|
UE_LOG(LogSavePackage, Verbose, TEXT("Package %s contains assets that are being converted to native code."), *SaveContext.GetPackage()->GetName());
|
|
return ESavePackageResult::ReplaceCompletely;
|
|
}
|
|
else if (ReplacementResult == EReplacementResult::GenerateStub)
|
|
{
|
|
SaveContext.RequestStubFile();
|
|
}
|
|
}
|
|
#endif
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult ValidatePackage(FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_ValidatePackage);
|
|
|
|
// Platform can't save the package
|
|
if (!FPlatformProperties::HasEditorOnlyData())
|
|
{
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
// Check recursive save package call
|
|
if (GIsSavingPackage && !SaveContext.IsConcurrent())
|
|
{
|
|
ensureMsgf(false, TEXT("Recursive SavePackage() is not supported"));
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
FString FilenameStr(SaveContext.GetFilename());
|
|
|
|
// If an asset is provided, validate it is in the package
|
|
UObject* Asset = SaveContext.GetAsset();
|
|
if (Asset && !Asset->IsInPackage(SaveContext.GetPackage()))
|
|
{
|
|
if (SaveContext.IsGenerateSaveError() && SaveContext.GetError())
|
|
{
|
|
FFormatNamedArguments Arguments;
|
|
Arguments.Add(TEXT("Name"), FText::FromString(FilenameStr));
|
|
FText ErrorText = FText::Format(NSLOCTEXT("SavePackage2", "AssetSaveNotInPackage", "The Asset '{Name}' being saved is not in the provided is not in the provided package."), Arguments);
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *ErrorText.ToString());
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
// Make sure package is allowed to be saved.
|
|
if (!SaveContext.IsCooking() && FCoreUObjectDelegates::IsPackageOKToSaveDelegate.IsBound())
|
|
{
|
|
bool bIsOKToSave = FCoreUObjectDelegates::IsPackageOKToSaveDelegate.Execute(SaveContext.GetPackage(), SaveContext.GetFilename(), SaveContext.GetError());
|
|
if (!bIsOKToSave)
|
|
{
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
FFormatNamedArguments Arguments;
|
|
Arguments.Add(TEXT("Name"), FText::FromString(FilenameStr));
|
|
FText FormatText = SaveContext.GetPackage()->ContainsMap() ? NSLOCTEXT("SavePackage2", "MapSaveNotAllowed", "Map '{Name}' is not allowed to save (see log for reason)") : NSLOCTEXT("SavePackage2", "AssetSaveNotAllowed", "Asset '{Name}' is not allowed to save (see log for reason");
|
|
FText ErrorText = FText::Format(FormatText, Arguments);
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *ErrorText.ToString());
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
}
|
|
|
|
// Check if the package is fully loaded
|
|
if (!SaveContext.GetPackage()->IsFullyLoaded())
|
|
{
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
// We cannot save packages that aren't fully loaded as it would clobber existing not loaded content.
|
|
FFormatNamedArguments Arguments;
|
|
Arguments.Add(TEXT("Name"), FText::FromString(FilenameStr));
|
|
FText FormatText = SaveContext.GetPackage()->ContainsMap() ? NSLOCTEXT("SavePackage2", "CannotSaveMapPartiallyLoaded", "Map '{Name}' cannot be saved as it has only been partially loaded") : NSLOCTEXT("SavePackage2", "CannotSaveAssetPartiallyLoaded", "Asset '{Name}' cannot be saved as it has only been partially loaded");
|
|
FText ErrorText = FText::Format(FormatText, Arguments);
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *ErrorText.ToString());
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
/// Cooking checks
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
#if WITH_EDITORONLY_DATA
|
|
// if we strip editor only data, validate the package isn't referenced only by editor data
|
|
if (SaveContext.IsStripEditorOnly())
|
|
{
|
|
// Don't save packages marked as editor-only.
|
|
if (SaveContext.CanSkipEditorReferencedPackagesWhenCooking() && SaveContext.GetPackage()->IsLoadedByEditorPropertiesOnly())
|
|
{
|
|
UE_CLOG(SaveContext.IsGenerateSaveError(), LogSavePackage, Display, TEXT("Package loaded by editor-only properties: %s. Package will not be saved."), *SaveContext.GetPackage()->GetName());
|
|
return ESavePackageResult::ReferencedOnlyByEditorOnlyData;
|
|
}
|
|
else if (SaveContext.GetPackage()->HasAnyPackageFlags(PKG_EditorOnly))
|
|
{
|
|
UE_CLOG(SaveContext.IsGenerateSaveError(), LogSavePackage, Display, TEXT("Package marked as editor-only: %s. Package will not be saved."), *SaveContext.GetPackage()->GetName());
|
|
return ESavePackageResult::ReferencedOnlyByEditorOnlyData;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Warn about long package names, which may be bad for consoles with limited filename lengths.
|
|
if (SaveContext.IsWarningLongFilename())
|
|
{
|
|
int32 MaxFilenameLength = FPlatformMisc::GetMaxPathLength();
|
|
|
|
// If the name is of the form "_LOC_xxx.ext", remove the loc data before the length check
|
|
FString BaseFilename = FPaths::GetBaseFilename(FilenameStr);
|
|
FString CleanBaseFilename = BaseFilename;
|
|
if (CleanBaseFilename.Find("_LOC_") == BaseFilename.Len() - 8)
|
|
{
|
|
CleanBaseFilename = BaseFilename.LeftChop(8);
|
|
}
|
|
if (CleanBaseFilename.Len() > MaxFilenameLength)
|
|
{
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
FFormatNamedArguments Arguments;
|
|
Arguments.Add(TEXT("FileName"), FText::FromString(BaseFilename));
|
|
Arguments.Add(TEXT("MaxLength"), FText::AsNumber(MaxFilenameLength));
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *FText::Format(NSLOCTEXT("Core", "Error_FilenameIsTooLongForCooking", "Filename '{FileName}' is too long; this may interfere with cooking for consoles. Unreal filenames should be no longer than {MaxLength} characters."), Arguments).ToString());
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("%s"), *FString::Printf(TEXT("Filename is too long (%d characters); this may interfere with cooking for consoles. Unreal filenames should be no longer than %s characters. Filename value: %s"), BaseFilename.Len(), MaxFilenameLength, *BaseFilename));
|
|
}
|
|
}
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
FORCEINLINE void EnsurePackageLocalization(UPackage* InPackage)
|
|
{
|
|
#if USE_STABLE_LOCALIZATION_KEYS
|
|
if (GIsEditor)
|
|
{
|
|
// We need to ensure that we have a package localization namespace as the package loading will need it
|
|
// We need to do this before entering the GIsSavingPackage block as it may change the package meta-data
|
|
TextNamespaceUtil::EnsurePackageNamespace(InPackage);
|
|
}
|
|
#endif // USE_STABLE_LOCALIZATION_KEYS
|
|
}
|
|
|
|
ESavePackageResult RoutePresave(FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_RoutePresave);
|
|
|
|
// Just route presave on all objects in the package while skipping unsaveable objects.
|
|
// This should be more efficient then trying to restrict to just the actual export,
|
|
// objects likely to not be export will probably not care about PreSave and should be mainly noop
|
|
TArray<UObject*> ObjectsInPackage;
|
|
GetObjectsWithPackage(SaveContext.GetPackage(), ObjectsInPackage);
|
|
for (UObject* Object: ObjectsInPackage)
|
|
{
|
|
if (!SaveContext.IsUnsaveable(Object))
|
|
{
|
|
if (SaveContext.IsCooking() && Object->HasAnyFlags(RF_ClassDefaultObject | RF_ArchetypeObject))
|
|
{
|
|
FArchiveObjectCrc32NonEditorProperties CrcArchive;
|
|
int32 Before = CrcArchive.Crc32(Object);
|
|
Object->PreSave(SaveContext.GetTargetPlatform());
|
|
int32 After = CrcArchive.Crc32(Object);
|
|
|
|
if (Before != After)
|
|
{
|
|
UE_ASSET_LOG(
|
|
LogSavePackage,
|
|
Warning,
|
|
Object,
|
|
TEXT("Non-deterministic cook warning - PreSave() has modified %s '%s' - a resave may be required"),
|
|
Object->HasAnyFlags(RF_ClassDefaultObject) ? TEXT("CDO") : TEXT("archetype"),
|
|
*Object->GetName());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Object->PreSave(SaveContext.GetTargetPlatform());
|
|
}
|
|
}
|
|
}
|
|
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult HarvestPackage(FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_HarvestPackage);
|
|
|
|
FPackageHarvester Harvester(SaveContext);
|
|
EObjectFlags TopLevelFlags = SaveContext.GetTopLevelFlags();
|
|
UObject* Asset = SaveContext.GetAsset();
|
|
|
|
// if no top level flags are passed, process just the provided package asset
|
|
if (TopLevelFlags == RF_NoFlags)
|
|
{
|
|
Harvester.TryHarvestExport(Asset);
|
|
while (UObject* Export = Harvester.PopExportToProcess())
|
|
{
|
|
Harvester.ProcessExport(Export);
|
|
}
|
|
}
|
|
// Otherwise process all objects which have the relevant flags
|
|
else
|
|
{
|
|
// Validate that if an asset is provided it has the appropriate top level flags
|
|
ensure(!Asset || Asset->HasAnyFlags(TopLevelFlags));
|
|
|
|
ForEachObjectWithPackage(SaveContext.GetPackage(), [&Harvester, TopLevelFlags](UObject* InObject)
|
|
{
|
|
if (InObject->HasAnyFlags(TopLevelFlags))
|
|
{
|
|
Harvester.TryHarvestExport(InObject);
|
|
}
|
|
return true;
|
|
},
|
|
true /*bIncludeNestedObjects */, RF_Transient);
|
|
while (UObject* Export = Harvester.PopExportToProcess())
|
|
{
|
|
Harvester.ProcessExport(Export);
|
|
}
|
|
}
|
|
|
|
// Harvest Prestream package class name if needed
|
|
if (SaveContext.GetPrestreamPackages().Num() > 0)
|
|
{
|
|
Harvester.HarvestName(SavePackageUtilities::NAME_PrestreamPackage);
|
|
|
|
//@todo FH: is this really needed?
|
|
// TSet<UPackage*> KeptPrestreamPackages;
|
|
// for (UPackage* Pkg : PrestreamPackages)
|
|
//{
|
|
// if (!Pkg->HasAnyMarks(OBJECTMARK_TagImp))
|
|
// {
|
|
// Pkg->Mark(OBJECTMARK_TagImp);
|
|
// KeptPrestreamPackages.Add(Pkg);
|
|
// }
|
|
//}
|
|
// Exchange(PrestreamPackages, KeptPrestreamPackages);
|
|
}
|
|
|
|
// if we have a WorldTileInfo, we need to harvest its dependencies as well, i.e. Custom Version
|
|
if (SaveContext.GetPackage()->WorldTileInfo.IsValid())
|
|
{
|
|
Harvester << *(SaveContext.GetPackage()->WorldTileInfo);
|
|
}
|
|
|
|
// The Editor version is used as part of the check to see if a package is too old to use the gather cache, so we always have to add it if we have gathered loc for this asset
|
|
// We need to set the editor custom version before we copy the version container to the summary, otherwise we may end up with corrupt assets
|
|
// because we later do it on the Linker when actually gathering loc data
|
|
if (!SaveContext.IsFilterEditorOnly())
|
|
{
|
|
Harvester.UsingCustomVersion(FEditorObjectVersion::GUID);
|
|
}
|
|
SaveContext.SetCustomVersions(Harvester.GetCustomVersions());
|
|
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
static FNameEntryId NAME_UniqueObjectNameForCookingComparisonIndex = FName("UniqueObjectNameForCooking").GetComparisonIndex();
|
|
|
|
ESavePackageResult ValidateExports(FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_ValidateExports);
|
|
|
|
// Check if we gathered any exports
|
|
if (SaveContext.GetExports().Num() == 0)
|
|
{
|
|
UE_CLOG(SaveContext.IsGenerateSaveError(), LogSavePackage, Verbose, TEXT("No exports found (or all exports are editor-only) for %s. Package will not be saved."), SaveContext.GetFilename());
|
|
return SaveContext.IsCooking() ? ESavePackageResult::ContainsEditorOnlyData : ESavePackageResult::Error;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (GOutputCookingWarnings)
|
|
{
|
|
// check the name list for UniqueObjectNameForCooking cooking
|
|
if (SaveContext.NameExists(NAME_UniqueObjectNameForCookingComparisonIndex))
|
|
{
|
|
for (const FTaggedExport& Export: SaveContext.GetExports())
|
|
{
|
|
FName NameInUse = Export.Obj->GetFName();
|
|
if (NameInUse.GetComparisonIndex() == NAME_UniqueObjectNameForCookingComparisonIndex)
|
|
{
|
|
UObject* Outer = Export.Obj->GetOuter();
|
|
UE_LOG(LogSavePackage, Warning, TEXT("Saving object into cooked package %s which was created at cook time, Object Name %s, Full Path %s, Class %s, Outer %s, Outer class %s"), SaveContext.GetFilename(), *NameInUse.ToString(), *Export.Obj->GetFullName(), *Export.Obj->GetClass()->GetName(), Outer ? *Outer->GetName() : TEXT("None"), Outer ? *Outer->GetClass()->GetName() : TEXT("None"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// If this is a map package, make sure there is a world or level in the export map.
|
|
if (SaveContext.GetPackage()->ContainsMap())
|
|
{
|
|
bool bContainsMap = false;
|
|
for (const FTaggedExport& Export: SaveContext.GetExports())
|
|
{
|
|
UObject* Object = Export.Obj;
|
|
// Consider redirectors to world/levels as map packages too.
|
|
if (UObjectRedirector* Redirector = Cast<UObjectRedirector>(Object))
|
|
{
|
|
Object = Redirector->DestinationObject;
|
|
}
|
|
if (Object)
|
|
{
|
|
FName ClassName = Object->GetClass()->GetFName();
|
|
bContainsMap |= (ClassName == SavePackageUtilities::NAME_World || ClassName == SavePackageUtilities::NAME_Level);
|
|
}
|
|
}
|
|
if (!bContainsMap)
|
|
{
|
|
ensureMsgf(false, TEXT("Attempting to save a map package '%s' that does not contain a map object."), *SaveContext.GetPackage()->GetName());
|
|
UE_LOG(LogSavePackage, Error, TEXT("Attempting to save a map package '%s' that does not contain a map object."), *SaveContext.GetPackage()->GetName());
|
|
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *FText::Format(NSLOCTEXT("Core", "SavePackageNoMap", "Attempting to save a map asset '{0}' that does not contain a map object"), FText::FromString(SaveContext.GetFilename())).ToString());
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
}
|
|
|
|
// Cooking checks
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
// Add the exports for the cook checker
|
|
// This needs to be done before validating NativeCodeGenReplacement which can exit early and will exist anyway but in compiled form
|
|
if (FEDLCookChecker* EDLCookChecker = SaveContext.GetEDLCookChecker())
|
|
{
|
|
// the package isn't actually in the export map, but that is ok, we add it as export anyway for error checking
|
|
EDLCookChecker->AddExport(SaveContext.GetPackage());
|
|
for (const FTaggedExport& Export: SaveContext.GetExports())
|
|
{
|
|
EDLCookChecker->AddExport(Export.Obj);
|
|
}
|
|
}
|
|
|
|
return ValidateBlueprintNativeCodeGenReplacement(SaveContext);
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult ValidateIllegalReferences(FSaveContext& SaveContext, TArray<UObject*>& PrivateObjects, TArray<UObject*>& ObjectsInOtherMaps)
|
|
{
|
|
FFormatNamedArguments Args;
|
|
|
|
// Illegal objects in other map warning
|
|
if (ObjectsInOtherMaps.Num() > 0)
|
|
{
|
|
UObject* MostLikelyCulprit = nullptr;
|
|
const FProperty* PropertyRef = nullptr;
|
|
|
|
// construct a string containing up to the first 5 objects problem objects
|
|
FString ObjectNames;
|
|
int32 MaxNamesToDisplay = 5;
|
|
bool DisplayIsLimited = true;
|
|
|
|
if (ObjectsInOtherMaps.Num() < MaxNamesToDisplay)
|
|
{
|
|
MaxNamesToDisplay = ObjectsInOtherMaps.Num();
|
|
DisplayIsLimited = false;
|
|
}
|
|
|
|
for (int32 Idx = 0; Idx < MaxNamesToDisplay; Idx++)
|
|
{
|
|
ObjectNames += ObjectsInOtherMaps[Idx]->GetName() + TEXT("\n");
|
|
}
|
|
|
|
// if there are more than 5 items we indicated this by adding "..." at the end of the list
|
|
if (DisplayIsLimited)
|
|
{
|
|
ObjectNames += TEXT("...\n");
|
|
}
|
|
|
|
Args.Empty();
|
|
Args.Add(TEXT("FileName"), FText::FromString(SaveContext.GetFilename()));
|
|
Args.Add(TEXT("ObjectNames"), FText::FromString(ObjectNames));
|
|
const FText Message = FText::Format(NSLOCTEXT("Core", "LinkedToObjectsInOtherMap_FindCulpritQ", "Can't save {FileName}: Graph is linked to object(s) in external map.\nExternal Object(s):\n{ObjectNames} \nTry to find the chain of references to that object (may take some time)?"), Args);
|
|
|
|
FString CulpritString = TEXT("Unknown");
|
|
bool bFindCulprit = IsRunningCommandlet() || (FMessageDialog::Open(EAppMsgType::YesNo, Message) == EAppReturnType::Yes);
|
|
if (bFindCulprit)
|
|
{
|
|
SavePackageUtilities::FindMostLikelyCulprit(ObjectsInOtherMaps, MostLikelyCulprit, PropertyRef);
|
|
if (MostLikelyCulprit != nullptr && PropertyRef != nullptr)
|
|
{
|
|
CulpritString = FString::Printf(TEXT("%s (%s)"), *MostLikelyCulprit->GetFullName(), *PropertyRef->GetName());
|
|
}
|
|
else if (MostLikelyCulprit != nullptr)
|
|
{
|
|
CulpritString = FString::Printf(TEXT("%s (Unknown property)"), *MostLikelyCulprit->GetFullName());
|
|
}
|
|
}
|
|
|
|
FString ErrorMessage = FString::Printf(TEXT("Can't save %s: Graph is linked to object %s in external map"), SaveContext.GetFilename(), *CulpritString);
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *ErrorMessage);
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSavePackage, Error, TEXT("%s"), *ErrorMessage);
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
if (PrivateObjects.Num() > 0)
|
|
{
|
|
UObject* MostLikelyCulprit = nullptr;
|
|
const FProperty* PropertyRef = nullptr;
|
|
|
|
// construct a string containing up to the first 5 objects problem objects
|
|
FString ObjectNames;
|
|
int32 MaxNamesToDisplay = 5;
|
|
bool DisplayIsLimited = true;
|
|
|
|
if (PrivateObjects.Num() < MaxNamesToDisplay)
|
|
{
|
|
MaxNamesToDisplay = PrivateObjects.Num();
|
|
DisplayIsLimited = false;
|
|
}
|
|
|
|
for (int32 Idx = 0; Idx < MaxNamesToDisplay; Idx++)
|
|
{
|
|
ObjectNames += PrivateObjects[Idx]->GetName() + TEXT("\n");
|
|
}
|
|
|
|
// if there are more than 5 items we indicated this by adding "..." at the end of the list
|
|
if (DisplayIsLimited)
|
|
{
|
|
ObjectNames += TEXT("...\n");
|
|
}
|
|
|
|
Args.Empty();
|
|
Args.Add(TEXT("FileName"), FText::FromString(SaveContext.GetFilename()));
|
|
Args.Add(TEXT("ObjectNames"), FText::FromString(ObjectNames));
|
|
const FText Message = FText::Format(NSLOCTEXT("Core", "LinkedToPrivateObjectsInOtherPackage_FindCulpritQ", "Can't save {FileName}: Graph is linked to private object(s) in an external package.\nExternal Object(s):\n{ObjectNames} \nTry to find the chain of references to that object (may take some time)?"), Args);
|
|
|
|
FString CulpritString = TEXT("Unknown");
|
|
if (FMessageDialog::Open(EAppMsgType::YesNo, Message) == EAppReturnType::Yes)
|
|
{
|
|
SavePackageUtilities::FindMostLikelyCulprit(PrivateObjects, MostLikelyCulprit, PropertyRef);
|
|
CulpritString = FString::Printf(TEXT("%s (%s)"),
|
|
(MostLikelyCulprit != nullptr) ? *MostLikelyCulprit->GetFullName() : TEXT("(unknown culprit)"),
|
|
(PropertyRef != nullptr) ? *PropertyRef->GetName() : TEXT("unknown property ref"));
|
|
}
|
|
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("Can't save %s: Graph is linked to external private object %s"), SaveContext.GetFilename(), *CulpritString);
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult ValidateImports(FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_ValidateImports);
|
|
|
|
TArray<UObject*> TopLevelObjects;
|
|
GetObjectsWithPackage(SaveContext.GetPackage(), TopLevelObjects, false);
|
|
auto IsInAnyTopLevelObject = [&TopLevelObjects](UObject* InObject) -> bool
|
|
{
|
|
for (UObject* TopObject: TopLevelObjects)
|
|
{
|
|
if (InObject->IsInOuter(TopObject))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
auto AnyTopLevelObjectIsIn = [&TopLevelObjects](UObject* InObject) -> bool
|
|
{
|
|
for (UObject* TopObject: TopLevelObjects)
|
|
{
|
|
if (TopObject->IsInOuter(InObject))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
auto AnyTopLevelObjectHasSameOutermostObject = [&TopLevelObjects](UObject* InObject) -> bool
|
|
{
|
|
UObject* Outermost = InObject->GetOutermostObject();
|
|
for (UObject* TopObject: TopLevelObjects)
|
|
{
|
|
if (TopObject->GetOutermostObject() == Outermost)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Warn for private objects & map object references
|
|
TArray<UObject*> PrivateObjects;
|
|
TArray<UObject*> ObjectsInOtherMaps;
|
|
for (UObject* Import: SaveContext.GetImports())
|
|
{
|
|
UPackage* ImportPackage = Import->GetPackage();
|
|
// All names should be properly harvested at this point
|
|
ensureAlways(SaveContext.NameExists(Import->GetFName().GetComparisonIndex()));
|
|
ensureAlways(SaveContext.NameExists(ImportPackage->GetFName().GetComparisonIndex()));
|
|
ensureAlways(SaveContext.NameExists(Import->GetClass()->GetFName().GetComparisonIndex()));
|
|
ensureAlways(SaveContext.NameExists(Import->GetClass()->GetOuter()->GetFName().GetComparisonIndex()));
|
|
|
|
// if an import outer is an export and that import doesn't have a specific package set then, there's an error
|
|
const bool bWrongImport = Import->GetOuter() && Import->GetOuter()->IsInPackage(SaveContext.GetPackage()) && Import->GetExternalPackage() == nullptr;
|
|
if (bWrongImport)
|
|
{
|
|
if (!Import->HasAllFlags(RF_Transient) || !Import->IsNative())
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("Bad Object=%s"), *Import->GetFullName());
|
|
}
|
|
else
|
|
{
|
|
// if an object is marked RF_Transient and native, it is either an intrinsic class or
|
|
// a property of an intrinsic class. Only properties of intrinsic classes will have
|
|
// an Outer that passes the check for "GetOuter()->IsInPackage(InOuter)" (thus ending up in this
|
|
// block of code). Just verify that the Outer for this property is also marked RF_Transient and Native
|
|
check(Import->GetOuter()->HasAllFlags(RF_Transient) && Import->GetOuter()->IsNative());
|
|
}
|
|
}
|
|
check(!bWrongImport || Import->HasAllFlags(RF_Transient) || Import->IsNative());
|
|
|
|
// @todo FH: validate prestream packages still needed..
|
|
if (SaveContext.GetPrestreamPackages().Contains(ImportPackage))
|
|
{
|
|
// These are not errors
|
|
UE_LOG(LogSavePackage, Display, TEXT("Prestreaming package %s "), *ImportPackage->GetPathName()); //-V595
|
|
continue;
|
|
}
|
|
|
|
// if this import shares a outer with top level object of this package then the reference is acceptable
|
|
if (!SaveContext.IsCooking() && (IsInAnyTopLevelObject(Import) || AnyTopLevelObjectIsIn(Import) || AnyTopLevelObjectHasSameOutermostObject(Import)))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// See whether the object we are referencing is in another map package.
|
|
if (ImportPackage->ContainsMap())
|
|
{
|
|
ObjectsInOtherMaps.Add(Import);
|
|
}
|
|
|
|
if (!Import->HasAnyFlags(RF_Public) && (!SaveContext.IsCooking() || !ImportPackage->HasAnyPackageFlags(PKG_CompiledIn)))
|
|
{
|
|
PrivateObjects.Add(Import);
|
|
}
|
|
}
|
|
if (PrivateObjects.Num() > 0 || ObjectsInOtherMaps.Num() > 0)
|
|
{
|
|
return ValidateIllegalReferences(SaveContext, PrivateObjects, ObjectsInOtherMaps);
|
|
}
|
|
|
|
// Cooking checks
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
// Now that imports are validated add them to the cook checker if available
|
|
if (FEDLCookChecker* EDLCookChecker = SaveContext.GetEDLCookChecker())
|
|
{
|
|
for (UObject* Import: SaveContext.GetImports())
|
|
{
|
|
check(Import);
|
|
EDLCookChecker->AddImport(Import, SaveContext.GetPackage());
|
|
}
|
|
}
|
|
}
|
|
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult CreateLinker(FSaveContext& SaveContext)
|
|
{
|
|
const FString BaseFilename = FPaths::GetBaseFilename(SaveContext.GetFilename());
|
|
// Make temp file. CreateTempFilename guarantees unique, non-existing filename.
|
|
// The temp file will be saved in the game save folder to not have to deal with potentially too long paths.
|
|
// Since the temp filename may include a 32 character GUID as well, limit the user prefix to 32 characters.
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_CreateLinkerSave);
|
|
|
|
if (SaveContext.IsDiffing())
|
|
{
|
|
// Diffing is supported for cooking only
|
|
if (!SaveContext.IsCooking())
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("Diffing Package %s is supported only while cooking."), *SaveContext.GetPackage()->GetName());
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
// The package asset should always be provided upstream
|
|
check(SaveContext.GetAsset());
|
|
|
|
// The entire package will be serialized to memory and then compared against package on disk.
|
|
// Each difference will be log with its Serialize call stack trace if IsDiffCallstack is true
|
|
FArchive* Saver = new FArchiveStackTrace(SaveContext.GetAsset(), *SaveContext.GetPackage()->FileName.ToString(), SaveContext.IsDiffCallstack(), SaveContext.GetDiffMapPtr());
|
|
SaveContext.Linker = MakeUnique<FLinkerSave>(SaveContext.GetPackage(), Saver, SaveContext.IsForceByteSwapping(), SaveContext.IsSaveUnversioned());
|
|
}
|
|
else
|
|
{
|
|
if (SaveContext.IsSaveAsync())
|
|
{
|
|
// Allocate the linker with a memory writer, forcing byte swapping if wanted.
|
|
SaveContext.Linker = MakeUnique<FLinkerSave>(SaveContext.GetPackage(), SaveContext.IsForceByteSwapping(), SaveContext.IsSaveUnversioned());
|
|
}
|
|
else
|
|
{
|
|
// Allocate the linker, forcing byte swapping if wanted.
|
|
SaveContext.TempFilename = FPaths::CreateTempFilename(*FPaths::ProjectSavedDir(), *BaseFilename.Left(32));
|
|
SaveContext.Linker = MakeUnique<FLinkerSave>(SaveContext.GetPackage(), *SaveContext.TempFilename.GetValue(), SaveContext.IsForceByteSwapping(), SaveContext.IsSaveUnversioned());
|
|
}
|
|
}
|
|
|
|
#if WITH_TEXT_ARCHIVE_SUPPORT
|
|
if (SaveContext.IsTextFormat())
|
|
{
|
|
if (SaveContext.TempFilename.IsSet())
|
|
{
|
|
SaveContext.TextFormatTempFilename = SaveContext.TempFilename.GetValue() + FPackageName::GetTextAssetPackageExtension();
|
|
}
|
|
else
|
|
{
|
|
SaveContext.TextFormatTempFilename = FPaths::CreateTempFilename(*FPaths::ProjectSavedDir(), *BaseFilename.Left(32)) + FPackageName::GetTextAssetPackageExtension();
|
|
}
|
|
SaveContext.TextFormatArchive = TUniquePtr<FArchive>(IFileManager::Get().CreateFileWriter(*SaveContext.TextFormatTempFilename.GetValue()));
|
|
TUniquePtr<FJsonArchiveOutputFormatter> OutputFormatter = MakeUnique<FJsonArchiveOutputFormatter>(*SaveContext.TextFormatArchive);
|
|
OutputFormatter->SetObjectIndicesMap(&SaveContext.Linker->ObjectIndicesMap);
|
|
SaveContext.Formatter = MoveTemp(OutputFormatter);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
SaveContext.Formatter = MakeUnique<FBinaryArchiveFormatter>(*(FArchive*)SaveContext.Linker.Get());
|
|
}
|
|
}
|
|
|
|
SaveContext.StructuredArchive = MakeUnique<FStructuredArchive>(*SaveContext.Formatter);
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
struct FNameEntryIdSortHelper
|
|
{
|
|
private:
|
|
/** the linker that we're sorting names for */
|
|
friend struct TDereferenceWrapper<FNameEntryId, FNameEntryIdSortHelper>;
|
|
|
|
/** Comparison function used by Sort */
|
|
FORCEINLINE bool operator()(const FName& A, const FName& B) const
|
|
{
|
|
return A.Compare(B) < 0;
|
|
}
|
|
|
|
/** Comparison function used by Sort */
|
|
FORCEINLINE bool operator()(FNameEntryId A, FNameEntryId B) const
|
|
{
|
|
//@todo Could be implemented without constructing FName but need a would new FNameEntry comparison API
|
|
return A != B && operator()(FName::CreateFromDisplayId(A, 0), FName::CreateFromDisplayId(B, 0));
|
|
}
|
|
};
|
|
|
|
struct FObjectResourceSortHelper
|
|
{
|
|
private:
|
|
friend struct TDereferenceWrapper<FObjectImport, FObjectResourceSortHelper>;
|
|
friend struct TDereferenceWrapper<FObjectExport, FObjectResourceSortHelper>;
|
|
|
|
/** Comparison function used by Sort */
|
|
FORCEINLINE bool operator()(const FObjectResource& A, const FObjectResource& B) const
|
|
{
|
|
return A.ObjectName.Compare(B.ObjectName) < 0;
|
|
}
|
|
};
|
|
|
|
ESavePackageResult BuildLinker(FSaveContext& SaveContext)
|
|
{
|
|
// Setup Linker
|
|
{
|
|
// Use the custom versions we harvested from the dependency harvesting pass
|
|
SaveContext.Linker->Summary.SetCustomVersionContainer(SaveContext.GetCustomVersions());
|
|
|
|
SaveContext.Linker->SetPortFlags(SaveContext.GetPortFlags());
|
|
SaveContext.Linker->SetFilterEditorOnly(SaveContext.IsFilterEditorOnly());
|
|
SaveContext.Linker->SetCookingTarget(SaveContext.GetTargetPlatform());
|
|
|
|
bool bUseUnversionedProperties = SaveContext.IsUsingUnversionedProperties();
|
|
SaveContext.Linker->SetUseUnversionedPropertySerialization(bUseUnversionedProperties);
|
|
SaveContext.Linker->Saver->SetUseUnversionedPropertySerialization(bUseUnversionedProperties);
|
|
|
|
#if WITH_EDITOR
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
SaveContext.Linker->SetDebugSerializationFlags(DSF_EnableCookerWarnings | SaveContext.Linker->GetDebugSerializationFlags());
|
|
}
|
|
#endif
|
|
// Make sure the package has the same version as the linker
|
|
SaveContext.GetPackage()->LinkerPackageVersion = SaveContext.Linker->UE4Ver();
|
|
SaveContext.GetPackage()->LinkerLicenseeVersion = SaveContext.Linker->LicenseeUE4Ver();
|
|
SaveContext.GetPackage()->LinkerCustomVersion = SaveContext.Linker->GetCustomVersions();
|
|
}
|
|
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
SaveContext.Linker->Summary.Guid = SaveContext.IsKeepGuid() ? SaveContext.GetPackage()->GetGuid() : SaveContext.GetPackage()->MakeNewGuid();
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
#if WITH_EDITORONLY_DATA
|
|
SaveContext.Linker->Summary.PersistentGuid = SaveContext.GetPackage()->GetPersistentGuid();
|
|
#endif
|
|
SaveContext.Linker->Summary.Generations = TArray<FGenerationInfo>{FGenerationInfo(0, 0)};
|
|
|
|
FLinkerSave* Linker = SaveContext.Linker.Get();
|
|
|
|
// Build Name Map
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_BuildNameMap);
|
|
Linker->Summary.NameOffset = 0;
|
|
Linker->Summary.NameCount = 0;
|
|
Linker->NameMap.Append(SaveContext.GetReferencedNames().Array());
|
|
Sort(&Linker->NameMap[0], Linker->NameMap.Num(), FNameEntryIdSortHelper());
|
|
|
|
if (!SaveContext.IsTextFormat())
|
|
{
|
|
for (int32 Index = 0; Index < Linker->NameMap.Num(); ++Index)
|
|
{
|
|
Linker->NameIndices.Add(Linker->NameMap[Index], Index);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build GatherableText
|
|
{
|
|
Linker->Summary.GatherableTextDataOffset = 0;
|
|
Linker->Summary.GatherableTextDataCount = 0;
|
|
if (!SaveContext.IsFilterEditorOnly())
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_BuildGatherableTextData);
|
|
|
|
// Gathers from the given package
|
|
SaveContext.GatherableTextResultFlags = EPropertyLocalizationGathererResultFlags::Empty;
|
|
FPropertyLocalizationDataGatherer(Linker->GatherableTextDataMap, SaveContext.GetPackage(), SaveContext.GatherableTextResultFlags);
|
|
}
|
|
}
|
|
|
|
// Build ImportMap
|
|
TMap<UObject*, UObject*> ReplacedImportOuters;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_BuildImportMap);
|
|
for (UObject* Import: SaveContext.GetImports())
|
|
{
|
|
UClass* ImportClass = Import->GetClass();
|
|
FName ReplacedName = NAME_None;
|
|
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
UObject* ReplacedOuter = nullptr;
|
|
SavePackageUtilities::GetBlueprintNativeCodeGenReplacement(Import, ImportClass, ReplacedOuter, ReplacedName, SaveContext.GetTargetPlatform());
|
|
if (ReplacedOuter)
|
|
{
|
|
ReplacedImportOuters.Add(Import, ReplacedOuter);
|
|
}
|
|
}
|
|
FObjectImport& ObjectImport = Linker->ImportMap.Add_GetRef(FObjectImport(Import, ImportClass));
|
|
|
|
//@todo FH: validate this
|
|
if (SaveContext.GetPrestreamPackages().Contains((UPackage*)Import))
|
|
{
|
|
ObjectImport.ClassName = SavePackageUtilities::NAME_PrestreamPackage;
|
|
}
|
|
|
|
if (ReplacedName != NAME_None)
|
|
{
|
|
ObjectImport.ObjectName = ReplacedName;
|
|
}
|
|
}
|
|
Sort(&Linker->ImportMap[0], Linker->ImportMap.Num(), FObjectResourceSortHelper());
|
|
Linker->Summary.ImportCount = Linker->ImportMap.Num();
|
|
}
|
|
|
|
// Build ExportMap & Package Netplay data
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_BuildExportMap);
|
|
for (const FTaggedExport& TaggedExport: SaveContext.GetExports())
|
|
{
|
|
FObjectExport& Export = Linker->ExportMap.Add_GetRef(FObjectExport(TaggedExport.Obj, TaggedExport.bNotAlwaysLoadedForEditorGame));
|
|
if (UPackage* Package = Cast<UPackage>(Export.Object))
|
|
{
|
|
Export.PackageFlags = Package->GetPackageFlags();
|
|
if (!Package->HasAnyPackageFlags(PKG_ServerSideOnly))
|
|
{
|
|
PRAGMA_DISABLE_DEPRECATION_WARNINGS
|
|
Export.PackageGuid = Package->GetGuid();
|
|
PRAGMA_ENABLE_DEPRECATION_WARNINGS
|
|
}
|
|
}
|
|
}
|
|
// Sort(&Linker->ExportMap[0], Linker->ExportMap.Num(), FObjectResourceSortHelper());
|
|
|
|
// @todo: To remove but for now object sort freaking matter in an incidental manner where it should be properly tracked with dependencies
|
|
// for example where FAnimInstanceProxy PostLoad actually depends on UAnimBlueprintGeneratedClass PostLoad to be properly initialized.
|
|
FObjectExportSortHelper ExportSortHelper;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SortExports);
|
|
ExportSortHelper.SortExports(Linker, nullptr);
|
|
}
|
|
Linker->Summary.ExportCount = Linker->ExportMap.Num();
|
|
}
|
|
|
|
// Build Linker Reverse Mapping
|
|
{
|
|
for (int32 ExportIndex = 0; ExportIndex < Linker->ExportMap.Num(); ++ExportIndex)
|
|
{
|
|
UObject* Object = Linker->ExportMap[ExportIndex].Object;
|
|
check(Object);
|
|
Linker->ObjectIndicesMap.Add(Object, FPackageIndex::FromExport(ExportIndex));
|
|
}
|
|
for (int32 ImportIndex = 0; ImportIndex < Linker->ImportMap.Num(); ++ImportIndex)
|
|
{
|
|
UObject* Object = Linker->ImportMap[ImportIndex].XObject;
|
|
check(Object);
|
|
Linker->ObjectIndicesMap.Add(Object, FPackageIndex::FromImport(ImportIndex));
|
|
}
|
|
}
|
|
|
|
// Build DependsMap
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_BuildExportDependsMap);
|
|
|
|
Linker->DependsMap.AddZeroed(Linker->ExportMap.Num());
|
|
for (int32 ExpIndex = 0; ExpIndex < Linker->ExportMap.Num(); ++ExpIndex)
|
|
{
|
|
UObject* Object = Linker->ExportMap[ExpIndex].Object;
|
|
TArray<FPackageIndex>& DependIndices = Linker->DependsMap[ExpIndex];
|
|
const TSet<UObject*>* SrcDepends = SaveContext.GetObjectDependencies().Find(Object);
|
|
checkf(SrcDepends, TEXT("Couldn't find dependency map for %s"), *Object->GetFullName());
|
|
DependIndices.Reserve(SrcDepends->Num());
|
|
|
|
for (UObject* DependentObject: *SrcDepends)
|
|
{
|
|
FPackageIndex DependencyIndex = Linker->ObjectIndicesMap.FindRef(DependentObject);
|
|
|
|
//@todo FH: UnmarkExportTagFromDuplicates redirect??
|
|
|
|
// if we didn't find it (FindRef returns 0 on failure, which is good in this case), then we are in trouble, something went wrong somewhere
|
|
checkf(!DependencyIndex.IsNull(), TEXT("Failed to find dependency index for %s (%s)"), *DependentObject->GetFullName(), *Object->GetFullName());
|
|
|
|
// add the import as an import for this export
|
|
DependIndices.Add(DependencyIndex);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Build Searchable Name Map
|
|
{
|
|
Linker->SoftPackageReferenceList = SaveContext.GetSoftPackageReferenceList();
|
|
|
|
// Convert the searchable names map from UObject to packageindex
|
|
for (TPair<UObject*, TArray<FName>>& SearchableNamePair: SaveContext.GetSearchableNamesObjectMap())
|
|
{
|
|
const FPackageIndex PackageIndex = Linker->MapObject(SearchableNamePair.Key);
|
|
// This should always be in the imports already
|
|
if (ensure(!PackageIndex.IsNull()))
|
|
{
|
|
Linker->SearchableNamesMap.FindOrAdd(PackageIndex) = MoveTemp(SearchableNamePair.Value);
|
|
}
|
|
}
|
|
SaveContext.GetSearchableNamesObjectMap().Empty();
|
|
}
|
|
|
|
// Map Export Indices
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_MapExportIndices);
|
|
for (FObjectExport& Export: Linker->ExportMap)
|
|
{
|
|
// Set class index.
|
|
// If this is *exactly* a UClass, store null instead; for anything else, including UClass-derived classes, map it
|
|
UClass* ObjClass = Export.Object->GetClass();
|
|
if (ObjClass != UClass::StaticClass())
|
|
{
|
|
Export.ClassIndex = Linker->MapObject(ObjClass);
|
|
checkf(!Export.ClassIndex.IsNull(), TEXT("Export %s class is not mapped when saving %s"), *Export.Object->GetFullName(), *Linker->LinkerRoot->GetName());
|
|
}
|
|
else
|
|
{
|
|
Export.ClassIndex = FPackageIndex();
|
|
}
|
|
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
UObject* Archetype = Export.Object->GetArchetype();
|
|
check(Archetype);
|
|
check(Archetype->IsA(Export.Object->HasAnyFlags(RF_ClassDefaultObject) ? ObjClass->GetSuperClass() : ObjClass));
|
|
Export.TemplateIndex = Linker->MapObject(Archetype);
|
|
UE_CLOG(Export.TemplateIndex.IsNull(), LogSavePackage, Fatal, TEXT("%s was an archetype of %s but returned a null index mapping the object."), *Archetype->GetFullName(), *Export.Object->GetFullName());
|
|
check(!Export.TemplateIndex.IsNull());
|
|
}
|
|
|
|
// Set the parent index, if this export represents a UStruct-derived object
|
|
UStruct* Struct = Cast<UStruct>(Export.Object);
|
|
if (Struct && Struct->GetSuperStruct() != nullptr)
|
|
{
|
|
Export.SuperIndex = Linker->MapObject(Struct->GetSuperStruct());
|
|
checkf(!Export.SuperIndex.IsNull(),
|
|
TEXT("Export Struct (%s) of type (%s) inheriting from (%s) of type (%s) has not mapped super struct."),
|
|
*GetPathNameSafe(Struct),
|
|
*(Struct->GetClass()->GetName()),
|
|
*GetPathNameSafe(Struct->GetSuperStruct()),
|
|
*(Struct->GetSuperStruct()->GetClass()->GetName()));
|
|
}
|
|
else
|
|
{
|
|
Export.SuperIndex = FPackageIndex();
|
|
}
|
|
|
|
// Set FPackageIndex for this export's Outer. If the export's Outer
|
|
// is the UPackage corresponding to this package's LinkerRoot, leave it null
|
|
Export.OuterIndex = Export.Object->GetOuter() != SaveContext.GetPackage() ? Linker->MapObject(Export.Object->GetOuter()) : FPackageIndex();
|
|
|
|
// Only packages or object having the currently saved package as outer are allowed to have no outer
|
|
ensureMsgf(Export.OuterIndex != FPackageIndex() || Export.Object->IsA(UPackage::StaticClass()) || Export.Object->GetOuter() == SaveContext.GetPackage(), TEXT("Export %s has no valid outer!"), *Export.Object->GetPathName());
|
|
}
|
|
|
|
for (FObjectImport& Import: Linker->ImportMap)
|
|
{
|
|
if (Import.XObject)
|
|
{
|
|
// Set the package index.
|
|
if (Import.XObject->GetOuter())
|
|
{
|
|
UObject** ReplacedOuter = ReplacedImportOuters.Find(Import.XObject);
|
|
if (ReplacedOuter && *ReplacedOuter)
|
|
{
|
|
Import.OuterIndex = Linker->MapObject(*ReplacedOuter);
|
|
ensure(Import.OuterIndex != FPackageIndex());
|
|
}
|
|
else
|
|
{
|
|
Import.OuterIndex = Linker->MapObject(Import.XObject->GetOuter());
|
|
}
|
|
|
|
// if the import has a package set, set it up
|
|
if (UPackage* ImportPackage = Import.XObject->GetExternalPackage())
|
|
{
|
|
Import.SetPackageName(ImportPackage->GetFName());
|
|
}
|
|
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
// Only package imports are allowed to have no outer
|
|
ensureMsgf(Import.OuterIndex != FPackageIndex() || Import.ClassName == NAME_Package, TEXT("Import %s has no valid outer when cooking!"), *Import.XObject->GetPathName());
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
checkf(false, TEXT("NULL XObject for import - Object: %s Class: %s"), *Import.ObjectName.ToString(), *Import.ClassName.ToString());
|
|
}
|
|
}
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
void SavePreloadDependencies(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
FLinkerSave* Linker = SaveContext.Linker.Get();
|
|
auto IncludeObjectAsDependency = [Linker, &SaveContext](int32 CallSite, TSet<FPackageIndex>& AddTo, UObject* ToTest, UObject* ForObj, bool bMandatory, bool bOnlyIfInLinkerTable)
|
|
{
|
|
// Skip transient, editor only, and excluded client/server objects
|
|
if (ToTest)
|
|
{
|
|
UPackage* Outermost = ToTest->GetOutermost();
|
|
check(Outermost);
|
|
if (Outermost->GetFName() == GLongCoreUObjectPackageName)
|
|
{
|
|
return; // We assume nothing in coreuobject ever loads assets in a constructor
|
|
}
|
|
FPackageIndex Index = Linker->MapObject(ToTest);
|
|
if (Index.IsNull() && bOnlyIfInLinkerTable)
|
|
{
|
|
return;
|
|
}
|
|
if (!Index.IsNull() && (ToTest->HasAllFlags(RF_Transient) && !ToTest->IsNative()))
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("A dependency '%s' of '%s' is in the linker table, but is transient. We will keep the dependency anyway (%d)."), *ToTest->GetFullName(), *ForObj->GetFullName(), CallSite);
|
|
}
|
|
if (!Index.IsNull() && ToTest->IsPendingKill())
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("A dependency '%s' of '%s' is in the linker table, but is pending kill. We will keep the dependency anyway (%d)."), *ToTest->GetFullName(), *ForObj->GetFullName(), CallSite);
|
|
}
|
|
bool bNotFiltered = !SaveContext.IsExcluded(ToTest);
|
|
if (bMandatory && !bNotFiltered)
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("A dependency '%s' of '%s' was filtered, but is mandatory. This indicates a problem with editor only stripping. We will keep the dependency anyway (%d)."), *ToTest->GetFullName(), *ForObj->GetFullName(), CallSite);
|
|
bNotFiltered = true;
|
|
}
|
|
if (bNotFiltered)
|
|
{
|
|
if (!Index.IsNull())
|
|
{
|
|
AddTo.Add(Index);
|
|
return;
|
|
}
|
|
else if (!ToTest->HasAnyFlags(RF_Transient))
|
|
{
|
|
UE_CLOG(Outermost->HasAnyPackageFlags(PKG_CompiledIn), LogSavePackage, Verbose, TEXT("A compiled in dependency '%s' of '%s' was not actually in the linker tables and so will be ignored (%d)."), *ToTest->GetFullName(), *ForObj->GetFullName(), CallSite);
|
|
UE_CLOG(!Outermost->HasAnyPackageFlags(PKG_CompiledIn), LogSavePackage, Fatal, TEXT("A dependency '%s' of '%s' was not actually in the linker tables and so will be ignored (%d)."), *ToTest->GetFullName(), *ForObj->GetFullName(), CallSite);
|
|
}
|
|
}
|
|
check(!bMandatory);
|
|
}
|
|
};
|
|
|
|
auto IncludeIndexAsDependency = [Linker](TSet<FPackageIndex>& AddTo, FPackageIndex Dep)
|
|
{
|
|
if (!Dep.IsNull())
|
|
{
|
|
UObject* ToTest = Dep.IsExport() ? Linker->Exp(Dep).Object : Linker->Imp(Dep).XObject;
|
|
if (ToTest)
|
|
{
|
|
UPackage* Outermost = ToTest->GetOutermost();
|
|
if (Outermost && Outermost->GetFName() != GLongCoreUObjectPackageName) // We assume nothing in coreuobject ever loads assets in a constructor
|
|
{
|
|
AddTo.Add(Dep);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
Linker->Summary.PreloadDependencyOffset = Linker->Tell();
|
|
Linker->Summary.PreloadDependencyCount = -1;
|
|
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
Linker->Summary.PreloadDependencyCount = 0;
|
|
|
|
FStructuredArchive::FStream DepedenciesStream = StructuredArchiveRoot.EnterStream(SA_FIELD_NAME(TEXT("PreloadDependencies")));
|
|
|
|
TArray<UObject*> Subobjects;
|
|
TArray<UObject*> Deps;
|
|
TSet<FPackageIndex> SerializationBeforeCreateDependencies;
|
|
TSet<FPackageIndex> SerializationBeforeSerializationDependencies;
|
|
TSet<FPackageIndex> CreateBeforeSerializationDependencies;
|
|
TSet<FPackageIndex> CreateBeforeCreateDependencies;
|
|
|
|
for (int32 ExportIndex = 0; ExportIndex < Linker->ExportMap.Num(); ++ExportIndex)
|
|
{
|
|
FObjectExport& Export = Linker->ExportMap[ExportIndex];
|
|
check(Export.Object);
|
|
{
|
|
SerializationBeforeCreateDependencies.Reset();
|
|
IncludeIndexAsDependency(SerializationBeforeCreateDependencies, Export.ClassIndex);
|
|
UObject* CDO = Export.Object->GetArchetype();
|
|
IncludeObjectAsDependency(1, SerializationBeforeCreateDependencies, CDO, Export.Object, true, false);
|
|
Subobjects.Reset();
|
|
GetObjectsWithOuter(CDO, Subobjects);
|
|
for (UObject* SubObj: Subobjects)
|
|
{
|
|
// Only include subobject archetypes
|
|
if (SubObj->HasAnyFlags(RF_DefaultSubObject | RF_ArchetypeObject))
|
|
{
|
|
while (SubObj->HasAnyFlags(RF_Transient)) // transient components are stripped by the ICH, so find the one it will really use at runtime
|
|
{
|
|
UObject* SubObjArch = SubObj->GetArchetype();
|
|
if (SubObjArch->GetClass()->HasAnyClassFlags(CLASS_Native | CLASS_Intrinsic))
|
|
{
|
|
break;
|
|
}
|
|
SubObj = SubObjArch;
|
|
}
|
|
if (!SubObj->IsPendingKill())
|
|
{
|
|
IncludeObjectAsDependency(2, SerializationBeforeCreateDependencies, SubObj, Export.Object, false, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
{
|
|
SerializationBeforeSerializationDependencies.Reset();
|
|
Deps.Reset();
|
|
Export.Object->GetPreloadDependencies(Deps);
|
|
|
|
for (UObject* Obj: Deps)
|
|
{
|
|
IncludeObjectAsDependency(3, SerializationBeforeSerializationDependencies, Obj, Export.Object, false, true);
|
|
}
|
|
if (Export.Object->HasAnyFlags(RF_ArchetypeObject | RF_ClassDefaultObject))
|
|
{
|
|
UObject* Outer = Export.Object->GetOuter();
|
|
if (!Outer->IsA(UPackage::StaticClass()))
|
|
{
|
|
IncludeObjectAsDependency(4, SerializationBeforeSerializationDependencies, Outer, Export.Object, true, false);
|
|
}
|
|
}
|
|
if (Export.Object->IsA(UClass::StaticClass()))
|
|
{
|
|
// we need to load archetypes of our subobjects before we load the class
|
|
UObject* CDO = CastChecked<UClass>(Export.Object)->GetDefaultObject();
|
|
Subobjects.Reset();
|
|
GetObjectsWithOuter(CDO, Subobjects);
|
|
for (UObject* SubObj: Subobjects)
|
|
{
|
|
// Only include subobject archetypes
|
|
if (SubObj->HasAnyFlags(RF_DefaultSubObject | RF_ArchetypeObject))
|
|
{
|
|
SubObj = SubObj->GetArchetype();
|
|
while (SubObj->HasAnyFlags(RF_Transient)) // transient components are stripped by the ICH, so find the one it will really use at runtime
|
|
{
|
|
UObject* SubObjArch = SubObj->GetArchetype();
|
|
if (SubObjArch->GetClass()->HasAnyClassFlags(CLASS_Native | CLASS_Intrinsic))
|
|
{
|
|
break;
|
|
}
|
|
SubObj = SubObjArch;
|
|
}
|
|
if (!SubObj->IsPendingKill())
|
|
{
|
|
IncludeObjectAsDependency(5, SerializationBeforeSerializationDependencies, SubObj, Export.Object, false, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
{
|
|
CreateBeforeSerializationDependencies.Reset();
|
|
UClass* Class = Cast<UClass>(Export.Object);
|
|
UObject* ClassCDO = Class ? Class->GetDefaultObject() : nullptr;
|
|
{
|
|
TArray<FPackageIndex>& Depends = Linker->DependsMap[ExportIndex];
|
|
for (FPackageIndex Dep: Depends)
|
|
{
|
|
UObject* ToTest = Dep.IsExport() ? Linker->Exp(Dep).Object : Linker->Imp(Dep).XObject;
|
|
if (ToTest != ClassCDO)
|
|
{
|
|
IncludeIndexAsDependency(CreateBeforeSerializationDependencies, Dep);
|
|
}
|
|
}
|
|
}
|
|
{
|
|
const TSet<UObject*>& NativeDeps = SaveContext.GetNativeObjectDependencies()[Export.Object];
|
|
for (UObject* ToTest: NativeDeps)
|
|
{
|
|
if (ToTest != ClassCDO)
|
|
{
|
|
IncludeObjectAsDependency(6, CreateBeforeSerializationDependencies, ToTest, Export.Object, false, true);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
{
|
|
CreateBeforeCreateDependencies.Reset();
|
|
IncludeIndexAsDependency(CreateBeforeCreateDependencies, Export.OuterIndex);
|
|
IncludeIndexAsDependency(CreateBeforeCreateDependencies, Export.SuperIndex);
|
|
}
|
|
FEDLCookChecker* EDLCookChecker = SaveContext.GetEDLCookChecker();
|
|
check(EDLCookChecker);
|
|
auto AddArcForDepChecking = [&Linker, &Export, EDLCookChecker](bool bExportIsSerialize, FPackageIndex Dep, bool bDepIsSerialize)
|
|
{
|
|
check(Export.Object);
|
|
check(!Dep.IsNull());
|
|
UObject* DepObject = Dep.IsExport() ? Linker->Exp(Dep).Object : Linker->Imp(Dep).XObject;
|
|
check(DepObject);
|
|
|
|
Linker->DepListForErrorChecking.Add(Dep);
|
|
EDLCookChecker->AddArc(DepObject, bDepIsSerialize, Export.Object, bExportIsSerialize);
|
|
};
|
|
|
|
for (FPackageIndex Index: SerializationBeforeSerializationDependencies)
|
|
{
|
|
if (SerializationBeforeCreateDependencies.Contains(Index))
|
|
{
|
|
continue; // if the other thing must be serialized before we create, then this is a redundant dep
|
|
}
|
|
if (Export.FirstExportDependency == -1)
|
|
{
|
|
Export.FirstExportDependency = Linker->Summary.PreloadDependencyCount;
|
|
check(Export.SerializationBeforeSerializationDependencies == 0 && Export.CreateBeforeSerializationDependencies == 0 && Export.SerializationBeforeCreateDependencies == 0 && Export.CreateBeforeCreateDependencies == 0);
|
|
}
|
|
Linker->Summary.PreloadDependencyCount++;
|
|
Export.SerializationBeforeSerializationDependencies++;
|
|
DepedenciesStream.EnterElement() << Index;
|
|
AddArcForDepChecking(true, Index, true);
|
|
}
|
|
for (FPackageIndex Index: CreateBeforeSerializationDependencies)
|
|
{
|
|
if (SerializationBeforeCreateDependencies.Contains(Index))
|
|
{
|
|
continue; // if the other thing must be serialized before we create, then this is a redundant dep
|
|
}
|
|
if (SerializationBeforeSerializationDependencies.Contains(Index))
|
|
{
|
|
continue; // if the other thing must be serialized before we serialize, then this is a redundant dep
|
|
}
|
|
if (CreateBeforeCreateDependencies.Contains(Index))
|
|
{
|
|
continue; // if the other thing must be created before we are created, then this is a redundant dep
|
|
}
|
|
if (Export.FirstExportDependency == -1)
|
|
{
|
|
Export.FirstExportDependency = Linker->Summary.PreloadDependencyCount;
|
|
check(Export.SerializationBeforeSerializationDependencies == 0 && Export.CreateBeforeSerializationDependencies == 0 && Export.SerializationBeforeCreateDependencies == 0 && Export.CreateBeforeCreateDependencies == 0);
|
|
}
|
|
Linker->Summary.PreloadDependencyCount++;
|
|
Export.CreateBeforeSerializationDependencies++;
|
|
DepedenciesStream.EnterElement() << Index;
|
|
AddArcForDepChecking(true, Index, false);
|
|
}
|
|
for (FPackageIndex Index: SerializationBeforeCreateDependencies)
|
|
{
|
|
if (Export.FirstExportDependency == -1)
|
|
{
|
|
Export.FirstExportDependency = Linker->Summary.PreloadDependencyCount;
|
|
check(Export.SerializationBeforeSerializationDependencies == 0 && Export.CreateBeforeSerializationDependencies == 0 && Export.SerializationBeforeCreateDependencies == 0 && Export.CreateBeforeCreateDependencies == 0);
|
|
}
|
|
Linker->Summary.PreloadDependencyCount++;
|
|
Export.SerializationBeforeCreateDependencies++;
|
|
DepedenciesStream.EnterElement() << Index;
|
|
AddArcForDepChecking(false, Index, true);
|
|
}
|
|
for (FPackageIndex Index: CreateBeforeCreateDependencies)
|
|
{
|
|
if (Export.FirstExportDependency == -1)
|
|
{
|
|
Export.FirstExportDependency = Linker->Summary.PreloadDependencyCount;
|
|
check(Export.SerializationBeforeSerializationDependencies == 0 && Export.CreateBeforeSerializationDependencies == 0 && Export.SerializationBeforeCreateDependencies == 0 && Export.CreateBeforeCreateDependencies == 0);
|
|
}
|
|
Linker->Summary.PreloadDependencyCount++;
|
|
Export.CreateBeforeCreateDependencies++;
|
|
DepedenciesStream.EnterElement() << Index;
|
|
AddArcForDepChecking(false, Index, false);
|
|
}
|
|
}
|
|
UE_LOG(LogSavePackage, Verbose, TEXT("Saved %d dependencies for %d exports."), Linker->Summary.PreloadDependencyCount, Linker->ExportMap.Num());
|
|
}
|
|
}
|
|
|
|
void WriteGatherableText(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
FStructuredArchive::FStream Stream = StructuredArchiveRoot.EnterStream(SA_FIELD_NAME(TEXT("GatherableTextData")));
|
|
if (!SaveContext.IsFilterEditorOnly()
|
|
// We can only cache packages that:
|
|
// 1) Don't contain script data, as script data is very volatile and can only be safely gathered after it's been compiled (which happens automatically on asset load).
|
|
// 2) Don't contain text keyed with an incorrect package localization ID, as these keys will be changed later during save.
|
|
&& !EnumHasAnyFlags(SaveContext.GatherableTextResultFlags, EPropertyLocalizationGathererResultFlags::HasScript | EPropertyLocalizationGathererResultFlags::HasTextWithInvalidPackageLocalizationID))
|
|
{
|
|
FLinkerSave* Linker = SaveContext.GetLinker();
|
|
|
|
// The Editor version is used as part of the check to see if a package is too old to use the gather cache, so we always have to add it if we have gathered loc for this asset
|
|
// Note that using custom version here only works because we already added it to the export tagger before the package summary was serialized
|
|
Linker->UsingCustomVersion(FEditorObjectVersion::GUID);
|
|
|
|
Linker->Summary.GatherableTextDataOffset = Linker->Tell();
|
|
Linker->Summary.GatherableTextDataCount = Linker->GatherableTextDataMap.Num();
|
|
for (FGatherableTextData& GatherableTextData: Linker->GatherableTextDataMap)
|
|
{
|
|
Stream.EnterElement() << GatherableTextData;
|
|
}
|
|
}
|
|
}
|
|
|
|
ESavePackageResult WritePackageHeader(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
FLinkerSave* Linker = SaveContext.Linker.Get();
|
|
#if WITH_EDITOR
|
|
FArchiveStackTraceIgnoreScope IgnoreDiffScope(SaveContext.IsIgnoringHeaderDiff());
|
|
#endif
|
|
|
|
// Write Dummy Summary
|
|
{
|
|
StructuredArchiveRoot.GetUnderlyingArchive() << Linker->Summary;
|
|
}
|
|
SaveContext.OffsetAfterPackageFileSummary = (int32)Linker->Tell();
|
|
|
|
// Write Name Map
|
|
Linker->Summary.NameOffset = SaveContext.OffsetAfterPackageFileSummary;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_BuildNameMap);
|
|
Linker->Summary.NameCount = Linker->NameMap.Num();
|
|
for (const FNameEntryId NameEntryId: Linker->NameMap)
|
|
{
|
|
FName::GetEntry(NameEntryId)->Write(*Linker);
|
|
}
|
|
}
|
|
|
|
// Write GatherableText
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WriteGatherableTextData);
|
|
WriteGatherableText(StructuredArchiveRoot, SaveContext);
|
|
}
|
|
|
|
// Save Dummy Import Map, overwritten later.
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WriteDummyImportMap);
|
|
Linker->Summary.ImportOffset = Linker->Tell();
|
|
for (FObjectImport& Import: Linker->ImportMap)
|
|
{
|
|
StructuredArchiveRoot.GetUnderlyingArchive() << Import;
|
|
}
|
|
}
|
|
SaveContext.OffsetAfterImportMap = Linker->Tell();
|
|
|
|
// Save Dummy Export Map, overwritten later.
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WriteDummyExportMap);
|
|
Linker->Summary.ExportOffset = Linker->Tell();
|
|
for (FObjectExport& Export: Linker->ExportMap)
|
|
{
|
|
*Linker << Export;
|
|
}
|
|
}
|
|
SaveContext.OffsetAfterExportMap = Linker->Tell();
|
|
|
|
// Save Depend Map
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WriteDependsMap);
|
|
|
|
FStructuredArchive::FStream DependsStream = StructuredArchiveRoot.EnterStream(SA_FIELD_NAME(TEXT("DependsMap")));
|
|
Linker->Summary.DependsOffset = Linker->Tell();
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
//@todo optimization, this should just be stripped entirely from cooked packages
|
|
TArray<FPackageIndex> Depends; // empty array
|
|
for (int32 ExportIndex = 0; ExportIndex < Linker->ExportMap.Num(); ++ExportIndex)
|
|
{
|
|
DependsStream.EnterElement() << Depends;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// save depends map (no need for later patching)
|
|
check(Linker->DependsMap.Num() == Linker->ExportMap.Num());
|
|
for (TArray<FPackageIndex>& Depends: Linker->DependsMap)
|
|
{
|
|
DependsStream.EnterElement() << Depends;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Write Soft Package references & Searchable Names
|
|
if (!SaveContext.IsFilterEditorOnly())
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SaveSoftPackagesAndSearchableNames);
|
|
|
|
// Save soft package references
|
|
Linker->Summary.SoftPackageReferencesOffset = Linker->Tell();
|
|
Linker->Summary.SoftPackageReferencesCount = Linker->SoftPackageReferenceList.Num();
|
|
{
|
|
FStructuredArchive::FStream SoftReferenceStream = StructuredArchiveRoot.EnterStream(SA_FIELD_NAME(TEXT("SoftReferences")));
|
|
for (FName& SoftPackageName: Linker->SoftPackageReferenceList)
|
|
{
|
|
SoftReferenceStream.EnterElement() << SoftPackageName;
|
|
}
|
|
|
|
// Save searchable names map
|
|
Linker->Summary.SearchableNamesOffset = Linker->Tell();
|
|
Linker->SerializeSearchableNamesMap(StructuredArchiveRoot.EnterField(SA_FIELD_NAME(TEXT("SearchableNames"))));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Linker->Summary.SoftPackageReferencesCount = 0;
|
|
Linker->Summary.SoftPackageReferencesOffset = 0;
|
|
Linker->Summary.SearchableNamesOffset = 0;
|
|
}
|
|
|
|
// Save thumbnails
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SaveThumbnails);
|
|
SavePackageUtilities::SaveThumbnails(SaveContext.GetPackage(), Linker, StructuredArchiveRoot.EnterField(SA_FIELD_NAME(TEXT("Thumbnails"))));
|
|
}
|
|
{
|
|
// Save asset registry data so the editor can search for information about assets in this package
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SaveAssetRegistryData);
|
|
UE::AssetRegistry::WritePackageData(StructuredArchiveRoot, SaveContext.IsCooking(), SaveContext.GetPackage(), Linker, SaveContext.GetImportsUsedInGame(), SaveContext.GetSoftPackagesUsedInGame());
|
|
}
|
|
// Save level information used by World browser
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WorldLevelData);
|
|
SavePackageUtilities::SaveWorldLevelInfo(SaveContext.GetPackage(), Linker, StructuredArchiveRoot);
|
|
}
|
|
|
|
// Write Preload Dependencies
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_PreloadDependencies);
|
|
SavePreloadDependencies(StructuredArchiveRoot, SaveContext);
|
|
}
|
|
Linker->Summary.TotalHeaderSize = Linker->Tell();
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult WritePackageTextHeader(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
FLinkerSave* Linker = SaveContext.GetLinker();
|
|
|
|
// Write GatherableText
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WriteGatherableTextData);
|
|
WriteGatherableText(StructuredArchiveRoot, SaveContext);
|
|
}
|
|
|
|
// Save thumbnails
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SaveThumbnails);
|
|
SavePackageUtilities::SaveThumbnails(SaveContext.GetPackage(), Linker, StructuredArchiveRoot.EnterField(SA_FIELD_NAME(TEXT("Thumbnails"))));
|
|
}
|
|
// Save level information used by World browser
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_WorldLevelData);
|
|
SavePackageUtilities::SaveWorldLevelInfo(SaveContext.GetPackage(), Linker, StructuredArchiveRoot);
|
|
}
|
|
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult WriteExports(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
// COOK_STAT(FScopedDurationTimer SaveTimer(FSavePackageStats::SerializeExportsTimeSec));
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SaveExports);
|
|
FLinkerSave* Linker = SaveContext.Linker.Get();
|
|
FScopedSlowTask SlowTask(Linker->ExportMap.Num(), FText(), SaveContext.IsUsingSlowTask());
|
|
|
|
FStructuredArchive::FRecord ExportsRecord = StructuredArchiveRoot.EnterRecord(SA_FIELD_NAME(TEXT("Exports")));
|
|
|
|
// Save exports.
|
|
int32 LastExportSaveStep = 0;
|
|
for (int32 i = 0; i < Linker->ExportMap.Num(); i++)
|
|
{
|
|
if (GWarn->ReceivedUserCancel())
|
|
{
|
|
return ESavePackageResult::Canceled;
|
|
}
|
|
SlowTask.EnterProgressFrame();
|
|
|
|
FObjectExport& Export = Linker->ExportMap[i];
|
|
if (Export.Object)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_SaveExport);
|
|
|
|
// Save the object data.
|
|
Export.SerialOffset = Linker->Tell();
|
|
Linker->CurrentlySavingExport = FPackageIndex::FromExport(i);
|
|
|
|
FString ObjectName = Export.Object->GetPathName(SaveContext.GetPackage());
|
|
FStructuredArchive::FSlot ExportSlot = ExportsRecord.EnterField(SA_FIELD_NAME(*ObjectName));
|
|
|
|
if (SaveContext.IsTextFormat())
|
|
{
|
|
FObjectTextExport ObjectTextExport(Export, SaveContext.GetPackage());
|
|
ExportSlot << ObjectTextExport;
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
bool bSupportsText = UClass::IsSafeToSerializeToStructuredArchives(Export.Object->GetClass());
|
|
#else
|
|
bool bSupportsText = false;
|
|
#endif
|
|
|
|
if (Export.Object->HasAnyFlags(RF_ClassDefaultObject))
|
|
{
|
|
if (bSupportsText)
|
|
{
|
|
Export.Object->GetClass()->SerializeDefaultObject(Export.Object, ExportSlot);
|
|
}
|
|
else
|
|
{
|
|
FArchiveUObjectFromStructuredArchive Adapter(ExportSlot);
|
|
Export.Object->GetClass()->SerializeDefaultObject(Export.Object, Adapter.GetArchive());
|
|
Adapter.Close();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
TGuardValue<UObject*> GuardSerializedObject(SaveContext.GetSerializeContext()->SerializedObject, Export.Object);
|
|
|
|
if (bSupportsText)
|
|
{
|
|
FStructuredArchive::FRecord ExportRecord = ExportSlot.EnterRecord();
|
|
Export.Object->Serialize(ExportRecord);
|
|
}
|
|
else
|
|
{
|
|
FArchiveUObjectFromStructuredArchive Adapter(ExportSlot);
|
|
Export.Object->Serialize(Adapter.GetArchive());
|
|
Adapter.Close();
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
if (Linker->IsCooking())
|
|
{
|
|
Export.Object->CookAdditionalFiles(SaveContext.GetFilename(), SaveContext.GetTargetPlatform(),
|
|
[&SaveContext](const TCHAR* Filename, void* Data, int64 Size)
|
|
{
|
|
FLargeMemoryWriter& Writer = SaveContext.AdditionalFilesFromExports.Emplace_GetRef(0, true, Filename);
|
|
Writer.Serialize(Data, Size);
|
|
});
|
|
}
|
|
#endif
|
|
}
|
|
Linker->CurrentlySavingExport = FPackageIndex();
|
|
Export.SerialSize = Linker->Tell() - Export.SerialOffset;
|
|
}
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult WriteAdditionalExportFiles(FSaveContext& SaveContext)
|
|
{
|
|
if (SaveContext.IsCooking() && SaveContext.AdditionalFilesFromExports.Num() > 0)
|
|
{
|
|
const bool bWriteFileToDisk = !SaveContext.IsDiffing();
|
|
const bool bComputeHash = SaveContext.IsComputeHash();
|
|
for (FLargeMemoryWriter& Writer: SaveContext.AdditionalFilesFromExports)
|
|
{
|
|
const int64 Size = Writer.TotalSize();
|
|
SaveContext.TotalPackageSizeUncompressed += Size;
|
|
|
|
if (bComputeHash || bWriteFileToDisk)
|
|
{
|
|
FLargeMemoryPtr DataPtr(Writer.ReleaseOwnership());
|
|
|
|
EAsyncWriteOptions WriteOptions(EAsyncWriteOptions::None);
|
|
if (bComputeHash)
|
|
{
|
|
WriteOptions |= EAsyncWriteOptions::ComputeHash;
|
|
}
|
|
if (bWriteFileToDisk)
|
|
{
|
|
WriteOptions |= EAsyncWriteOptions::WriteFileToDisk;
|
|
}
|
|
SavePackageUtilities::AsyncWriteFile(SaveContext.AsyncWriteAndHashSequence, MoveTemp(DataPtr), Size, *Writer.GetArchiveName(), WriteOptions, {});
|
|
}
|
|
}
|
|
SaveContext.AdditionalFilesFromExports.Empty();
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult UpdatePackageHeader(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_UpdatePackageHeader);
|
|
|
|
FLinkerSave* Linker = SaveContext.Linker.Get();
|
|
#if WITH_EDITOR
|
|
FArchiveStackTraceIgnoreScope IgnoreDiffScope(SaveContext.IsIgnoringHeaderDiff());
|
|
#endif
|
|
|
|
// Write Real Import Map
|
|
if (!SaveContext.IsTextFormat())
|
|
{
|
|
Linker->Seek(Linker->Summary.ImportOffset);
|
|
FStructuredArchive::FStream ImportTableStream = StructuredArchiveRoot.EnterStream(SA_FIELD_NAME(TEXT("ImportTable")));
|
|
for (FObjectImport& Import: Linker->ImportMap)
|
|
{
|
|
ImportTableStream.EnterElement() << Import;
|
|
}
|
|
}
|
|
// Write Real Export Map
|
|
if (!SaveContext.IsTextFormat())
|
|
{
|
|
check(Linker->Tell() == SaveContext.OffsetAfterImportMap);
|
|
Linker->Seek(Linker->Summary.ExportOffset);
|
|
FStructuredArchive::FStream ExportTableStream = StructuredArchiveRoot.EnterStream(SA_FIELD_NAME(TEXT("ExportTable")));
|
|
|
|
for (FObjectExport& Export: Linker->ExportMap)
|
|
{
|
|
ExportTableStream.EnterElement() << Export;
|
|
}
|
|
check(Linker->Tell() == SaveContext.OffsetAfterExportMap);
|
|
}
|
|
|
|
// Update Summary
|
|
// Write Real Summary
|
|
{
|
|
//@todo: remove ExportCount and NameCount - no longer used
|
|
Linker->Summary.Generations.Last().ExportCount = Linker->Summary.ExportCount;
|
|
Linker->Summary.Generations.Last().NameCount = Linker->Summary.NameCount;
|
|
|
|
// create the package source (based on developer or user created)
|
|
#if (UE_BUILD_SHIPPING && WITH_EDITOR)
|
|
Linker->Summary.PackageSource = FMath::Rand() * FMath::Rand();
|
|
#else
|
|
Linker->Summary.PackageSource = FCrc::StrCrc_DEPRECATED(*FPaths::GetBaseFilename(SaveContext.GetFilename()).ToUpper());
|
|
#endif
|
|
|
|
// Flag package as requiring localization gather if the archive requires localization gathering.
|
|
Linker->LinkerRoot->ThisRequiresLocalizationGather(Linker->RequiresLocalizationGather());
|
|
|
|
// Update package flags from package, in case serialization has modified package flags.
|
|
Linker->Summary.PackageFlags = Linker->LinkerRoot->GetPackageFlags() & ~PKG_NewlyCreated;
|
|
|
|
// @todo: custom versions: when can this be checked?
|
|
{
|
|
// Verify that the final serialization pass hasn't added any new custom versions. Otherwise this will result in crashes when loading the package.
|
|
bool bNewCustomVersionsUsed = false;
|
|
for (const FCustomVersion& LinkerCustomVer: Linker->GetCustomVersions().GetAllVersions())
|
|
{
|
|
if (Linker->Summary.GetCustomVersionContainer().GetVersion(LinkerCustomVer.Key) == nullptr)
|
|
{
|
|
UE_LOG(LogSavePackage, Error,
|
|
TEXT("Unexpected custom version \"%s\" found when saving %s. This usually happens when export tagging and final serialization paths differ. Package will not be saved."),
|
|
*LinkerCustomVer.GetFriendlyName().ToString(), *Linker->LinkerRoot->GetName());
|
|
bNewCustomVersionsUsed = true;
|
|
}
|
|
}
|
|
if (bNewCustomVersionsUsed)
|
|
{
|
|
return ESavePackageResult::Error;
|
|
}
|
|
}
|
|
|
|
if (!SaveContext.IsTextFormat())
|
|
{
|
|
Linker->Seek(0);
|
|
}
|
|
{
|
|
StructuredArchiveRoot.EnterField(SA_FIELD_NAME(TEXT("Summary"))) << Linker->Summary;
|
|
}
|
|
|
|
if (!SaveContext.IsTextFormat())
|
|
{
|
|
check(Linker->Tell() == SaveContext.OffsetAfterPackageFileSummary);
|
|
}
|
|
}
|
|
return ReturnSuccessOrCancel();
|
|
}
|
|
|
|
ESavePackageResult FinalizeFile(FStructuredArchive::FRecord& StructuredArchiveRoot, FSaveContext& SaveContext)
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_FinalizeFile);
|
|
|
|
// In the concurrent case, it is called right after routing presave so it can be done in batch before going concurrent
|
|
if (!SaveContext.IsConcurrent())
|
|
{
|
|
// If we're writing to the existing file call ResetLoaders on the Package so that we drop the handle to the file on disk and can write to it
|
|
// COOK_STAT(FScopedDurationTimer SaveTimer(FSavePackageStats::ResetLoadersTimeSec));
|
|
ResetLoadersForSave(SaveContext.GetPackage(), SaveContext.GetFilename());
|
|
}
|
|
|
|
if (SaveContext.IsSaveAsync())
|
|
{
|
|
FString PathToSave = SaveContext.GetFilename();
|
|
FLinkerSave* Linker = SaveContext.GetLinker();
|
|
|
|
if (SaveContext.IsDiffCallstack())
|
|
{
|
|
const TCHAR* CutoffString = TEXT("UEditorEngine::Save()");
|
|
FArchiveStackTrace* Writer = static_cast<FArchiveStackTrace*>(Linker->Saver);
|
|
TMap<FName, FArchiveDiffStats> PackageDiffStats;
|
|
Writer->CompareWith(*PathToSave, SaveContext.IsCooking() ? Linker->Summary.TotalHeaderSize : 0, CutoffString, SaveContext.GetMaxDiffsToLog(), PackageDiffStats);
|
|
SaveContext.TotalPackageSizeUncompressed += Writer->TotalSize();
|
|
|
|
// COOK_STAT(FSavePackageStats::NumberOfDifferentPackages++);
|
|
// COOK_STAT(FSavePackageStats::MergeStats(PackageDiffStats));
|
|
|
|
if (SaveContext.IsSavingForDiff())
|
|
{
|
|
PathToSave = FPaths::Combine(FPaths::GetPath(PathToSave), FPaths::GetBaseFilename(PathToSave) + TEXT("_ForDiff") + FPaths::GetExtension(PathToSave, true));
|
|
}
|
|
}
|
|
else if (SaveContext.IsDiffOnly())
|
|
{
|
|
FArchiveStackTrace* Writer = (FArchiveStackTrace*)(Linker->Saver);
|
|
FArchiveDiffMap OutDiffMap;
|
|
SaveContext.bDiffOnlyIdentical = Writer->GenerateDiffMap(*PathToSave, IsEventDrivenLoaderEnabledInCookedBuilds() ? Linker->Summary.TotalHeaderSize : 0, SaveContext.GetMaxDiffsToLog(), OutDiffMap);
|
|
SaveContext.TotalPackageSizeUncompressed += Writer->TotalSize();
|
|
if (FArchiveDiffMap* OutDiffMapPtr = SaveContext.GetDiffMapPtr())
|
|
{
|
|
*OutDiffMapPtr = MoveTemp(OutDiffMap);
|
|
}
|
|
}
|
|
|
|
if (!SaveContext.IsDiffing() || SaveContext.IsSavingForDiff())
|
|
{
|
|
UE_LOG(LogSavePackage, Verbose, TEXT("Async saving from memory to '%s'"), *PathToSave);
|
|
FLargeMemoryWriter* Writer = static_cast<FLargeMemoryWriter*>(Linker->Saver);
|
|
const int64 DataSize = Writer->TotalSize();
|
|
|
|
if (SaveContext.IsDiffCallstack()) // Avoid double counting the package size if SAVE_DiffCallstack flag is set and DiffSettings.bSaveForDiff == true.
|
|
{
|
|
SaveContext.TotalPackageSizeUncompressed += DataSize;
|
|
}
|
|
|
|
FSavePackageContext* SavePackageContext = SaveContext.GetSavePackageContext();
|
|
if (SavePackageContext != nullptr && SavePackageContext->PackageStoreWriter != nullptr && SaveContext.IsCooking())
|
|
{
|
|
FIoBuffer IoBuffer(FIoBuffer::AssumeOwnership, Writer->ReleaseOwnership(), DataSize);
|
|
|
|
if (SaveContext.IsComputeHash())
|
|
{
|
|
FIoBuffer InnerBuffer(IoBuffer.Data(), IoBuffer.DataSize(), IoBuffer);
|
|
SavePackageUtilities::IncrementOutstandingAsyncWrites();
|
|
SaveContext.AsyncWriteAndHashSequence.AddWork([InnerBuffer = MoveTemp(InnerBuffer)](FMD5& State)
|
|
{
|
|
State.Update(InnerBuffer.Data(), InnerBuffer.DataSize());
|
|
SavePackageUtilities::DecrementOutstandingAsyncWrites();
|
|
});
|
|
}
|
|
|
|
FPackageStoreWriter::HeaderInfo HeaderInfo;
|
|
FPackageStoreWriter::ExportsInfo ExportsInfo;
|
|
|
|
ExportsInfo.PackageName = HeaderInfo.PackageName = SaveContext.GetPackage()->GetFName();
|
|
ExportsInfo.LooseFilePath = HeaderInfo.LooseFilePath = SaveContext.GetFilename();
|
|
|
|
const int32 HeaderSize = Linker->Summary.TotalHeaderSize;
|
|
SavePackageContext->PackageStoreWriter->WriteHeader(HeaderInfo, FIoBuffer(IoBuffer.Data(), HeaderSize, IoBuffer));
|
|
|
|
const uint8* ExportsData = IoBuffer.Data() + HeaderSize;
|
|
const int32 ExportCount = Linker->ExportMap.Num();
|
|
|
|
ExportsInfo.Exports.Reserve(ExportCount);
|
|
ExportsInfo.RegionsOffset = HeaderSize;
|
|
|
|
for (const FObjectExport& Export: Linker->ExportMap)
|
|
{
|
|
ExportsInfo.Exports.Add(FIoBuffer(IoBuffer.Data() + Export.SerialOffset, Export.SerialSize, IoBuffer));
|
|
}
|
|
SavePackageContext->PackageStoreWriter->WriteExports(ExportsInfo, FIoBuffer(ExportsData, DataSize - HeaderSize, IoBuffer), Linker->FileRegions);
|
|
}
|
|
else
|
|
{
|
|
EAsyncWriteOptions WriteOptions(EAsyncWriteOptions::WriteFileToDisk);
|
|
if (SaveContext.IsComputeHash())
|
|
{
|
|
WriteOptions |= EAsyncWriteOptions::ComputeHash;
|
|
}
|
|
if (SaveContext.IsCooking())
|
|
{
|
|
SavePackageUtilities::AsyncWriteFileWithSplitExports(SaveContext.AsyncWriteAndHashSequence, FLargeMemoryPtr(Writer->ReleaseOwnership()), DataSize, Linker->Summary.TotalHeaderSize, *PathToSave, WriteOptions, Linker->FileRegions);
|
|
}
|
|
else
|
|
{
|
|
SavePackageUtilities::AsyncWriteFile(SaveContext.AsyncWriteAndHashSequence, FLargeMemoryPtr(Writer->ReleaseOwnership()), DataSize, *PathToSave, WriteOptions, Linker->FileRegions);
|
|
}
|
|
}
|
|
SaveContext.CloseLinkerArchives();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Destroy archives used for saving, closing file handle.
|
|
bool bSuccess = SaveContext.CloseLinkerArchives();
|
|
|
|
if (!bSuccess)
|
|
{
|
|
UE_LOG(LogSavePackage, Error, TEXT("Error writing temp file '%s' for '%s'"),
|
|
SaveContext.TempFilename.IsSet() ? *SaveContext.TempFilename.GetValue() : TEXT("UNKNOWN"), SaveContext.GetFilename());
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
// Move file to its real destination
|
|
check(SaveContext.TempFilename.IsSet());
|
|
if (SaveContext.IsTextFormat())
|
|
{
|
|
check(SaveContext.TextFormatTempFilename.IsSet());
|
|
IFileManager::Get().Delete(*SaveContext.TempFilename.GetValue());
|
|
SaveContext.TempFilename = SaveContext.TextFormatTempFilename;
|
|
SaveContext.TextFormatTempFilename.Reset();
|
|
}
|
|
|
|
UE_LOG(LogSavePackage, Log, TEXT("Moving '%s' to '%s'"), SaveContext.TempFilename.IsSet() ? *SaveContext.TempFilename.GetValue() : TEXT("UNKNOWN"), SaveContext.GetFilename());
|
|
bSuccess = IFileManager::Get().Move(SaveContext.GetFilename(), *SaveContext.TempFilename.GetValue());
|
|
SaveContext.TempFilename.Reset();
|
|
|
|
if (!bSuccess)
|
|
{
|
|
if (SaveContext.IsGenerateSaveError())
|
|
{
|
|
UE_LOG(LogSavePackage, Warning, TEXT("%s"), *FString::Printf(TEXT("Error saving '%s'"), SaveContext.GetFilename()));
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogSavePackage, Error, TEXT("%s"), *FString::Printf(TEXT("Error saving '%s'"), SaveContext.GetFilename()));
|
|
SaveContext.GetError()->Logf(ELogVerbosity::Warning, TEXT("%s"), *FText::Format(NSLOCTEXT("Core", "SaveWarning", "Error saving '{0}'"), FText::FromString(FString(SaveContext.GetFilename()))).ToString());
|
|
}
|
|
return ESavePackageResult::Error;
|
|
}
|
|
|
|
if (SaveContext.GetFinalTimestamp() != FDateTime::MinValue())
|
|
{
|
|
IFileManager::Get().SetTimeStamp(SaveContext.GetFilename(), SaveContext.GetFinalTimestamp());
|
|
}
|
|
|
|
if (SaveContext.IsComputeHash())
|
|
{
|
|
SavePackageUtilities::IncrementOutstandingAsyncWrites();
|
|
SaveContext.AsyncWriteAndHashSequence.AddWork([NewPath = FString(SaveContext.GetFilename())](FMD5& State)
|
|
{
|
|
SavePackageUtilities::AddFileToHash(NewPath, State);
|
|
SavePackageUtilities::DecrementOutstandingAsyncWrites();
|
|
});
|
|
}
|
|
}
|
|
|
|
return ESavePackageResult::Success;
|
|
}
|
|
|
|
void BeginCachePlatformCookedData(FSaveContext& SaveContext)
|
|
{
|
|
#if WITH_EDITOR
|
|
// Cache platform cooked data
|
|
if (SaveContext.IsCooking() && !SaveContext.IsConcurrent())
|
|
{
|
|
for (FTaggedExport& Export: SaveContext.GetExports())
|
|
{
|
|
Export.Obj->BeginCacheForCookedPlatformData(SaveContext.GetTargetPlatform());
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
void ClearCachedPlatformCookedData(FSaveContext& SaveContext)
|
|
{
|
|
#if WITH_EDITOR
|
|
if (SaveContext.IsCooking() && !SaveContext.IsConcurrent())
|
|
{
|
|
for (FTaggedExport& Export: SaveContext.GetExports())
|
|
{
|
|
Export.Obj->ClearCachedCookedPlatformData(SaveContext.GetTargetPlatform());
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
/**
|
|
* InnerSave is the portion of Save that can be safely run concurrently
|
|
*/
|
|
ESavePackageResult InnerSave(FSaveContext& SaveContext)
|
|
{
|
|
TRefCountPtr<FUObjectSerializeContext> SerializeContext(FUObjectThreadContext::Get().GetSerializeContext());
|
|
SaveContext.SetSerializeContext(SerializeContext);
|
|
SaveContext.SetEDLCookChecker(&FEDLCookChecker::Get());
|
|
|
|
// Create slow task dialog if needed
|
|
const int32 TotalSaveSteps = 12;
|
|
FScopedSlowTask SlowTask(TotalSaveSteps, FText(), SaveContext.IsUsingSlowTask());
|
|
SlowTask.MakeDialog(SaveContext.IsFromAutoSave());
|
|
|
|
// Harvest Package
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = HarvestPackage(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Validate Exports
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = ValidateExports(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Validate Imports
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = ValidateImports(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Trigger platform cooked data caching
|
|
BeginCachePlatformCookedData(SaveContext);
|
|
|
|
// Create Linker
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = CreateLinker(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Build Linker
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = BuildLinker(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
FStructuredArchive::FRecord StructuredArchiveRoot = SaveContext.StructuredArchive->Open().EnterRecord();
|
|
StructuredArchiveRoot.GetUnderlyingArchive().SetSerializeContext(SaveContext.GetSerializeContext());
|
|
|
|
// Write Header
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = !SaveContext.IsTextFormat() ? WritePackageHeader(StructuredArchiveRoot, SaveContext) : WritePackageTextHeader(StructuredArchiveRoot, SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// SHA Generation
|
|
TArray<uint8>* ScriptSHABytes = nullptr;
|
|
{
|
|
// look for this package in the list of packages to generate script SHA for
|
|
ScriptSHABytes = FLinkerSave::PackagesToScriptSHAMap.Find(*FPaths::GetBaseFilename(SaveContext.GetFilename()));
|
|
// if we want to generate the SHA key, start tracking script writes
|
|
if (ScriptSHABytes)
|
|
{
|
|
SaveContext.Linker->StartScriptSHAGeneration();
|
|
}
|
|
}
|
|
|
|
// Write Exports
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = WriteExports(StructuredArchiveRoot, SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
// Get SHA Key
|
|
{
|
|
// if we want to generate the SHA key, get it out now that the package has finished saving
|
|
if (ScriptSHABytes && SaveContext.Linker->ContainsCode())
|
|
{
|
|
// make space for the 20 byte key
|
|
ScriptSHABytes->Empty(20);
|
|
ScriptSHABytes->AddUninitialized(20);
|
|
|
|
// retrieve it
|
|
SaveContext.Linker->GetScriptSHAKey(ScriptSHABytes->GetData());
|
|
}
|
|
}
|
|
|
|
// Save Bulk Data
|
|
SlowTask.EnterProgressFrame();
|
|
SavePackageUtilities::SaveBulkData(SaveContext.Linker.Get(), SaveContext.GetPackage(), SaveContext.GetFilename(), SaveContext.GetTargetPlatform(), SaveContext.GetSavePackageContext(), SaveContext.IsTextFormat(), SaveContext.IsDiffing(),
|
|
SaveContext.IsComputeHash(), SaveContext.AsyncWriteAndHashSequence, SaveContext.TotalPackageSizeUncompressed);
|
|
|
|
// Write Additional files from export
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = WriteAdditionalExportFiles(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Write Package Post Tag
|
|
if (!SaveContext.IsTextFormat())
|
|
{
|
|
uint32 Tag = PACKAGE_FILE_TAG;
|
|
StructuredArchiveRoot.GetUnderlyingArchive() << Tag;
|
|
}
|
|
|
|
// Capture Package Size
|
|
int32 PackageSize = SaveContext.Linker->Tell();
|
|
SaveContext.TotalPackageSizeUncompressed += PackageSize;
|
|
|
|
// Update Package Header
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = UpdatePackageHeader(StructuredArchiveRoot, SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Finalize File Write
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = FinalizeFile(StructuredArchiveRoot, SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// COOK_STAT(FSavePackageStats::MBWritten += ((double)SaveContext.TotalPackageSizeUncompressed) / 1024.0 / 1024.0);
|
|
|
|
// Mark Exports & Package RF_Loaded
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save_MarkExportLoaded);
|
|
FLinkerSave* Linker = SaveContext.Linker.Get();
|
|
// Mark exports and the package as RF_Loaded after they've been serialized
|
|
// This is to ensue that newly created packages are properly marked as loaded (since they now exist on disk and
|
|
// in memory in the exact same state).
|
|
|
|
// Nobody should be touching those objects beside us while we are saving them here as this can potentially be executed from another thread
|
|
for (auto& Export: Linker->ExportMap)
|
|
{
|
|
if (Export.Object)
|
|
{
|
|
Export.Object->SetFlags(RF_WasLoaded | RF_LoadCompleted);
|
|
}
|
|
}
|
|
if (Linker->LinkerRoot)
|
|
{
|
|
// And finally set the flag on the package itself.
|
|
Linker->LinkerRoot->SetFlags(RF_WasLoaded | RF_LoadCompleted);
|
|
}
|
|
|
|
// Clear dirty flag if desired
|
|
if (!SaveContext.IsKeepDirty())
|
|
{
|
|
SaveContext.GetPackage()->SetDirtyFlag(false);
|
|
}
|
|
|
|
// Update package FileSize value
|
|
SaveContext.GetPackage()->FileSize = PackageSize;
|
|
}
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
FText GetSlowTaskStatusMessage(const FSaveContext& SaveContext)
|
|
{
|
|
FString CleanFilename = FPaths::GetCleanFilename(SaveContext.GetFilename());
|
|
FFormatNamedArguments Args;
|
|
Args.Add(TEXT("CleanFilename"), FText::FromString(CleanFilename));
|
|
return FText::Format(NSLOCTEXT("Core", "SavingFile", "Saving file: {CleanFilename}..."), Args);
|
|
}
|
|
|
|
FSavePackageResultStruct UPackage::Save2(UPackage* InPackage, UObject* InAsset, const TCHAR* InFilename, FSavePackageArgs& SaveArgs)
|
|
{
|
|
// COOK_STAT(FScopedDurationTimer FuncSaveTimer(FSavePackageStats::SavePackageTimeSec));
|
|
// COOK_STAT(FSavePackageStats::NumPackagesSaved++);
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_Save2);
|
|
FSaveContext SaveContext(InPackage, InAsset, InFilename, SaveArgs);
|
|
|
|
// Create the slow task dialog if needed
|
|
const int32 TotalSaveSteps = 7;
|
|
FScopedSlowTask SlowTask(TotalSaveSteps, GetSlowTaskStatusMessage(SaveContext), SaveContext.IsUsingSlowTask());
|
|
SlowTask.MakeDialog(SaveContext.IsFromAutoSave());
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
SaveContext.Result = ValidatePackage(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
|
|
// Ensures
|
|
SlowTask.EnterProgressFrame();
|
|
EnsurePackageLocalization(SaveContext.GetPackage());
|
|
{
|
|
// FullyLoad the package's Loader, so that anything we need to serialize (bulkdata, thumbnails) is available
|
|
// COOK_STAT(FScopedDurationTimer SaveTimer(FSavePackageStats::FullyLoadLoadersTimeSec));
|
|
EnsureLoadingComplete(SaveContext.GetPackage());
|
|
|
|
if (!SaveContext.IsConcurrent())
|
|
{
|
|
// We need to fulfill all pending streaming and async loading requests to then allow us to lock the global IO manager.
|
|
// The latter implies flushing all file handles which is a pre-requisite of saving a package. The code basically needs
|
|
// to be sure that we are not reading from a file that is about to be overwritten and that there is no way we might
|
|
// start reading from the file till we are done overwriting it.
|
|
FlushAsyncLoading();
|
|
}
|
|
(*GFlushStreamingFunc)();
|
|
}
|
|
|
|
// PreSave Asset
|
|
SlowTask.EnterProgressFrame();
|
|
if (InAsset)
|
|
{
|
|
SaveContext.SetPreSaveCleanup(InAsset->PreSaveRoot(InFilename));
|
|
}
|
|
|
|
// Route Presave only if not calling concurrently or diffing, in those case they should be handled separately
|
|
SlowTask.EnterProgressFrame();
|
|
if (!SaveContext.IsConcurrent() && !SaveContext.IsDiffing())
|
|
{
|
|
SaveContext.Result = RoutePresave(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
FScopedSavingFlag IsSavingFlag(SaveContext.IsConcurrent());
|
|
SaveContext.Result = InnerSave(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
return SaveContext.Result;
|
|
}
|
|
}
|
|
|
|
// PostSave Asset
|
|
SlowTask.EnterProgressFrame();
|
|
if (InAsset)
|
|
{
|
|
InAsset->PostSaveRoot(SaveContext.GetPreSaveCleanup());
|
|
SaveContext.SetPreSaveCleanup(false);
|
|
}
|
|
|
|
ClearCachedPlatformCookedData(SaveContext);
|
|
|
|
// Package Saved event
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
// Package has been save, so unmark NewlyCreated flag.
|
|
InPackage->ClearPackageFlags(PKG_NewlyCreated);
|
|
|
|
// send a message that the package was saved
|
|
UPackage::PackageSavedEvent.Broadcast(InFilename, InPackage);
|
|
}
|
|
return SaveContext.GetFinalResult();
|
|
}
|
|
|
|
ESavePackageResult UPackage::SaveConcurrent(TArrayView<FPackageSaveInfo> InPackages, FSavePackageArgs& SaveArgs, TArray<FSavePackageResultStruct>& OutResults)
|
|
{
|
|
auto GetPackageAsset = [](FPackageSaveInfo& PackageSaveInfo) -> UObject*
|
|
{
|
|
UObject* Asset = nullptr;
|
|
ForEachObjectWithPackage(PackageSaveInfo.Package, [&Asset](UObject* Object)
|
|
{
|
|
if (Object->IsAsset())
|
|
{
|
|
Asset = Object;
|
|
return false;
|
|
}
|
|
return true;
|
|
},
|
|
/*bIncludeNestedObjects*/ false);
|
|
return Asset;
|
|
};
|
|
|
|
const int32 TotalSaveSteps = 4;
|
|
FScopedSlowTask SlowTask(TotalSaveSteps, NSLOCTEXT("Core", "SavingFiles", "Saving files..."), SaveArgs.bSlowTask);
|
|
SlowTask.MakeDialog(!!(SaveArgs.SaveFlags & SAVE_FromAutosave));
|
|
|
|
// Create all the package save context and run pre save
|
|
SlowTask.EnterProgressFrame();
|
|
TArray<FSaveContext> PackageSaveContexts;
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_SaveConcurrent_PreSave);
|
|
for (FPackageSaveInfo& PackageSaveInfo: InPackages)
|
|
{
|
|
FSaveContext& SaveContext = PackageSaveContexts.Emplace_GetRef(PackageSaveInfo.Package, GetPackageAsset(PackageSaveInfo), *PackageSaveInfo.Filename, SaveArgs, nullptr);
|
|
|
|
// Validation
|
|
SaveContext.Result = ValidatePackage(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Ensures
|
|
EnsurePackageLocalization(SaveContext.GetPackage());
|
|
EnsureLoadingComplete(SaveContext.GetPackage()); //@todo: needed??
|
|
|
|
// PreSave Asset
|
|
if (SaveContext.GetAsset())
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_SaveConcurrent_PreSaveRoot);
|
|
SaveContext.SetPreSaveCleanup(SaveContext.GetAsset()->PreSaveRoot(SaveContext.GetFilename()));
|
|
}
|
|
|
|
// Route Presave
|
|
SaveContext.Result = RoutePresave(SaveContext);
|
|
if (SaveContext.Result != ESavePackageResult::Success)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
// Flush async loading and reset loaders
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_SaveConcurrent_ResetLoadersForSave);
|
|
ResetLoadersForSave(InPackages);
|
|
}
|
|
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_SaveConcurrent);
|
|
|
|
// Use concurrent new save only if new save is enabled, otherwise use old save
|
|
static const IConsoleVariable* EnableNewSave = IConsoleManager::Get().FindConsoleVariable(TEXT("SavePackage.EnableNewSave"));
|
|
if (EnableNewSave->GetInt())
|
|
{
|
|
// Passing in false here so that GIsSavingPackage is set to true on top of locking the GC
|
|
FScopedSavingFlag IsSavingFlag(false);
|
|
|
|
// Concurrent Part
|
|
ParallelFor(PackageSaveContexts.Num(), [&PackageSaveContexts](int32 PackageIdx)
|
|
{
|
|
InnerSave(PackageSaveContexts[PackageIdx]);
|
|
});
|
|
}
|
|
else
|
|
{
|
|
GIsSavingPackage = true;
|
|
ParallelFor(PackageSaveContexts.Num(), [&PackageSaveContexts](int32 PackageIdx)
|
|
{
|
|
FSaveContext& SaveContext = PackageSaveContexts[PackageIdx];
|
|
const FSavePackageArgs& SaveArgs = SaveContext.GetSaveArgs();
|
|
Save(SaveContext.GetPackage(), SaveContext.GetAsset(), SaveArgs.TopLevelFlags, SaveContext.GetFilename(),
|
|
SaveArgs.Error, nullptr, SaveArgs.bForceByteSwapping, SaveArgs.bWarnOfLongFilename, SaveArgs.SaveFlags | SAVE_Concurrent,
|
|
SaveArgs.TargetPlatform, SaveArgs.FinalTimeStamp, false, nullptr, nullptr);
|
|
});
|
|
GIsSavingPackage = false;
|
|
}
|
|
}
|
|
|
|
// Run Post Concurrent Save
|
|
SlowTask.EnterProgressFrame();
|
|
{
|
|
TRACE_CPUPROFILER_EVENT_SCOPE(UPackage_SaveConcurrent_PostSave);
|
|
for (FSaveContext& SaveContext: PackageSaveContexts)
|
|
{
|
|
// PostSave Asset
|
|
if (SaveContext.GetAsset())
|
|
{
|
|
SaveContext.GetAsset()->PostSaveRoot(SaveContext.GetPreSaveCleanup());
|
|
SaveContext.SetPreSaveCleanup(false);
|
|
}
|
|
|
|
ClearCachedPlatformCookedData(SaveContext);
|
|
|
|
// Package Saved event
|
|
if (SaveContext.Result == ESavePackageResult::Success)
|
|
{
|
|
// Package has been save, so unmark NewlyCreated flag.
|
|
SaveContext.GetPackage()->ClearPackageFlags(PKG_NewlyCreated);
|
|
|
|
// send a message that the package was saved
|
|
UPackage::PackageSavedEvent.Broadcast(SaveContext.GetFilename(), SaveContext.GetPackage());
|
|
}
|
|
OutResults.Add(SaveContext.GetFinalResult());
|
|
}
|
|
}
|
|
|
|
return ESavePackageResult::Success;
|
|
}
|
|
|
|
#endif // UE_WITH_SAVEPACKAGE
|