4645 lines
188 KiB
C++
4645 lines
188 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "AssetHeaderPatcher.h"
|
|
|
|
#include "Algo/Copy.h"
|
|
#include "Algo/Sort.h"
|
|
#include "AssetRegistry/IAssetRegistry.h"
|
|
#include "AssetRegistry/AssetData.h"
|
|
#include "AssetRegistry/PackageReader.h"
|
|
#include "Containers/ContainersFwd.h"
|
|
#include "Internationalization/GatherableTextData.h"
|
|
#include "Misc/Base64.h"
|
|
#include "Misc/EnumerateRange.h"
|
|
#include "Misc/FileHelper.h"
|
|
#include "Misc/PackageName.h"
|
|
#include "Misc/Paths.h"
|
|
#include "Misc/PathViews.h"
|
|
#include "Misc/RedirectCollector.h"
|
|
#include "Serialization/LargeMemoryReader.h"
|
|
#include "UObject/CoreRedirects.h"
|
|
#include "UObject/CoreRedirects/CoreRedirectsContext.h"
|
|
#include "UObject/Linker.h"
|
|
#include "UObject/NameTypes.h"
|
|
#include "UObject/ObjectResource.h"
|
|
#include "UObject/Package.h"
|
|
#include "UObject/PackageFileSummary.h"
|
|
#include "WorldPartition/WorldPartitionActorDesc.h"
|
|
#include "WorldPartition/WorldPartitionActorDescUtils.h"
|
|
|
|
DEFINE_LOG_CATEGORY_STATIC(LogAssetHeaderPatcher, Log, All);
|
|
|
|
#define DEBUG_ASSET_HEADER_PATCHING 0
|
|
|
|
namespace
|
|
{
|
|
// If working on header patching, this is very helpful for dumping what is patched and reviewing the files in a folder comparison of your favourite diff program.
|
|
FString DumpOutputDirectory;
|
|
static FAutoConsoleVariableRef CVarDumpOutputDirectory(
|
|
TEXT("AssetHeaderPatcher.DebugDumpDir"),
|
|
DumpOutputDirectory,
|
|
TEXT("'Before'/'After' text representations of each package processed during patching will be written out to the provided absolute filesystem path. Useful for comparing what was patched.")
|
|
);
|
|
|
|
// Tag 'Key' names that are generally large blobs of data that can't/shouldn't be patched
|
|
const TCHAR* TagsToIgnore[] =
|
|
{
|
|
TEXT("FiBData")
|
|
};
|
|
|
|
const FStringView InvalidObjectPathCharacters(INVALID_OBJECTPATH_CHARACTERS);
|
|
|
|
bool SplitLongPackageName(FStringView LongPackageName, FStringView& PackageRoot, FStringView& PackagePath, FStringView& PackageName)
|
|
{
|
|
if (LongPackageName.IsEmpty() || LongPackageName[0] != TEXT('/'))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
PackageRoot = FStringView(LongPackageName.GetData() + 1); // + 1 to skip the leading '/'
|
|
int32 SeparatorPos;
|
|
if (!PackageRoot.FindChar(TEXT('/'), SeparatorPos))
|
|
{
|
|
return false;
|
|
}
|
|
PackageRoot.LeftInline(SeparatorPos);
|
|
|
|
const int32 PackagePathOffset = PackageRoot.Len() + 2; // + 2 for the leading and trailing '/'
|
|
if (LongPackageName.Len() < PackagePathOffset || !LongPackageName.FindLastChar(TEXT('/'), SeparatorPos))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// May be empty. If the PackageName is off the root there is no PackagePath
|
|
const int32 PackagePathLen = SeparatorPos - (PackagePathOffset - 1);
|
|
check(PackagePathLen >= 0);
|
|
PackagePath = FStringView(LongPackageName.GetData() + PackagePathOffset, PackagePathLen - !!PackagePathLen);
|
|
|
|
const int32 PackageNameOffset = PackagePathOffset + PackagePath.Len() + !PackagePath.IsEmpty();
|
|
PackageName = FStringView(LongPackageName.GetData() + PackageNameOffset, LongPackageName.Len() - PackageNameOffset);
|
|
|
|
return true;
|
|
}
|
|
|
|
FStringView Find(const TMap<FString, FString>& Table, FStringView Needle)
|
|
{
|
|
uint32 NeedleHash = TMap<FString, FString>::KeyFuncsType::GetKeyHash<FStringView>(Needle);
|
|
const FString* MaybeNewItem = Table.FindByHash<FStringView>(NeedleHash, Needle);
|
|
if (MaybeNewItem)
|
|
{
|
|
return *MaybeNewItem;
|
|
}
|
|
|
|
return {};
|
|
}
|
|
}
|
|
|
|
FString LexToString(FAssetHeaderPatcher::EResult InResult)
|
|
{
|
|
switch (InResult)
|
|
{
|
|
case FAssetHeaderPatcher::EResult::NotStarted: return TEXT("Not Started");
|
|
case FAssetHeaderPatcher::EResult::Cancelled: return TEXT("Cancelled");
|
|
case FAssetHeaderPatcher::EResult::InProgress: return TEXT("In Progress");
|
|
case FAssetHeaderPatcher::EResult::Success: return TEXT("Success");
|
|
case FAssetHeaderPatcher::EResult::ErrorFailedToLoadSourceAsset: return TEXT("Failed to load source asset");
|
|
case FAssetHeaderPatcher::EResult::ErrorFailedToDeserializeSourceAsset: return TEXT("Failed to deserialize source asset");
|
|
case FAssetHeaderPatcher::EResult::ErrorUnexpectedSectionOrder: return TEXT("Unexpected section order");
|
|
case FAssetHeaderPatcher::EResult::ErrorBadOffset: return TEXT("Bad offset");
|
|
case FAssetHeaderPatcher::EResult::ErrorUnkownSection: return TEXT("Unknown section");
|
|
case FAssetHeaderPatcher::EResult::ErrorFailedToOpenDestinationFile: return TEXT("Failed to open destination file");
|
|
case FAssetHeaderPatcher::EResult::ErrorFailedToWriteToDestinationFile: return TEXT("Failed to write to destination file");
|
|
case FAssetHeaderPatcher::EResult::ErrorEmptyRequireSection: return TEXT("Empty required section");
|
|
default: return TEXT("Unknown");
|
|
}
|
|
}
|
|
|
|
FAssetHeaderPatcher::FContext::FContext(const TMap<FString, FString>& SourceAndDestPackages, const bool bInGatherDependentPackages)
|
|
: PackagePathRenameMap(SourceAndDestPackages)
|
|
{
|
|
AddVerseMounts();
|
|
|
|
if (bInGatherDependentPackages)
|
|
{
|
|
GatherDependentPackages();
|
|
}
|
|
|
|
GenerateFilePathsFromPackagePaths();
|
|
GenerateAdditionalRemappings();
|
|
}
|
|
|
|
FAssetHeaderPatcher::FContext::FContext(const FString& InSrcRoot, const FString& InDstRoot, const FString& InSrcBaseDir, const TMap<FString, FString>& InSrcAndDstFilePaths, const TMap<FString, FString>& InMountPointReplacements)
|
|
: FilePathRenameMap(InSrcAndDstFilePaths)
|
|
, StringMountReplacements(InMountPointReplacements)
|
|
{
|
|
AddVerseMounts();
|
|
GeneratePackagePathsFromFilePaths(InSrcRoot, InDstRoot, InSrcBaseDir);
|
|
GenerateAdditionalRemappings();
|
|
}
|
|
|
|
void FAssetHeaderPatcher::FContext::AddVerseMounts()
|
|
{
|
|
// Todo: Expose this so callers provide this data
|
|
VerseMountPoints.Add(TEXT("localhost"));
|
|
}
|
|
|
|
void FAssetHeaderPatcher::FContext::GenerateFilePathsFromPackagePaths()
|
|
{
|
|
FilePathRenameMap.Reserve(PackagePathRenameMap.Num());
|
|
|
|
// Construct all source and destination filenames from our package map
|
|
for (const TTuple<FString, FString>& Package : PackagePathRenameMap)
|
|
{
|
|
const FString& PackageName = Package.Key;
|
|
const FString& DestPackage = Package.Value;
|
|
FString SrcFilename;
|
|
|
|
// To consider: Allow the caller to provide their own file filter
|
|
if (FPackageName::IsVersePackage(PackageName))
|
|
{
|
|
// Verse packages are not header patchable.
|
|
// They are also not Packages as far as DoesPackageExist tells me.
|
|
// But they are real files that in template copying have already been done, so we dont want a warning message.
|
|
continue;
|
|
}
|
|
|
|
if (FPackageName::DoesPackageExist(PackageName, &SrcFilename))
|
|
{
|
|
FString DestFilename = FPackageName::LongPackageNameToFilename(DestPackage, FString(FPathViews::GetExtension(SrcFilename, true)));
|
|
FilePathRenameMap.Add({ MoveTemp(SrcFilename), MoveTemp(DestFilename) });
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Warning, TEXT("{%s} package does not exist, and will not be patched."), *PackageName);
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetHeaderPatcher::FContext::GeneratePackagePathsFromFilePaths(const FString& InSrcRoot, const FString& InDstRoot, const FString& InSrcBaseDir)
|
|
{
|
|
const FString SourceContentPath = FPaths::Combine(InSrcBaseDir, TEXT("Content"));
|
|
for (const TTuple<FString, FString>& SourceAndDest : FilePathRenameMap)
|
|
{
|
|
const FString& SrcFileName = SourceAndDest.Key;
|
|
|
|
if (FPaths::IsUnderDirectory(SrcFileName, SourceContentPath))
|
|
{
|
|
if (FStringView RelativePkgPath; FPathViews::TryMakeChildPathRelativeTo(SrcFileName, SourceContentPath, RelativePkgPath))
|
|
{
|
|
RelativePkgPath = FPathViews::GetBaseFilenameWithPath(RelativePkgPath); // chop the extension
|
|
if (RelativePkgPath.Len() > 0 && !RelativePkgPath.EndsWith(TEXT("/")))
|
|
{
|
|
PackagePathRenameMap.Add(FPaths::Combine(TEXT("/"), InSrcRoot, RelativePkgPath),
|
|
FPaths::Combine(TEXT("/"), InDstRoot, RelativePkgPath));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FAssetHeaderPatcher::FContext::GatherDependentPackages()
|
|
{
|
|
// Paths under the __External root drop the package root, so create mappings, per plugin,
|
|
// we can leverage when handling those cases where the package path may have been remapped
|
|
TMap<FString, TMap<FString, FString>> PluginExternalMappings;
|
|
for (const TPair<FString, FString>& SrcDstPair : PackagePathRenameMap)
|
|
{
|
|
const FString& Src = SrcDstPair.Key;
|
|
const FString& Dst = SrcDstPair.Value;
|
|
|
|
FStringView SrcPackageRoot;
|
|
FStringView SrcPackagePath;
|
|
FStringView SrcPackageName;
|
|
SplitLongPackageName(Src, SrcPackageRoot, SrcPackagePath, SrcPackageName);
|
|
|
|
FStringView DstPackageRoot;
|
|
FStringView DstPackagePath;
|
|
FStringView DstPackageName;
|
|
SplitLongPackageName(Dst, DstPackageRoot, DstPackagePath, DstPackageName);
|
|
|
|
TMap<FString, FString>& ExternalMappings = PluginExternalMappings.FindOrAddByHash(GetTypeHash(SrcPackageRoot), FString(SrcPackageRoot));
|
|
FStringView SrcPath = SrcPackagePath.IsEmpty() ? SrcPackageName : SrcPackagePath;
|
|
FStringView DstPath = DstPackagePath.IsEmpty() ? DstPackageName : DstPackagePath;
|
|
ExternalMappings.Add(FString(SrcPath), FString(DstPath));
|
|
|
|
// if there is a path
|
|
if (!SrcPackagePath.IsEmpty())
|
|
{
|
|
// add the local path/asset for the case of maps (which we cannot tell at this point)
|
|
ExternalMappings.Add(FString(SrcPath.GetData()), FString(DstPath.GetData()));
|
|
}
|
|
|
|
// While iterating mappings, add any mountpoint changes
|
|
if (SrcPackageRoot != DstPackageRoot)
|
|
{
|
|
const uint32 SrcPackageRootHash = GetTypeHash(SrcPackageRoot);
|
|
FString* RemappedRoot = StringMountReplacements.FindByHash(SrcPackageRootHash, SrcPackageRoot);
|
|
if (RemappedRoot)
|
|
{
|
|
UE_CLOG(DstPackageRoot != *RemappedRoot, LogAssetHeaderPatcher, Warning,
|
|
TEXT("Found conflicting mountpoint remapping: /%s/ -> /%s/ and /%s/ -> /%s/. The second mapping will be used to overwrite the first."),
|
|
*FString(SrcPackageRoot), **RemappedRoot,
|
|
*FString(SrcPackageRoot), *FString(DstPackageRoot));
|
|
}
|
|
StringMountReplacements.AddByHash(SrcPackageRootHash, FString(SrcPackageRoot), FString(DstPackageRoot));
|
|
}
|
|
}
|
|
|
|
TMap<FString, FString> Result;
|
|
IAssetRegistry& Registry = *IAssetRegistry::Get();
|
|
|
|
TArray< TTuple<FString, FString> > ToProcess;
|
|
Algo::Copy(PackagePathRenameMap, ToProcess);
|
|
|
|
TStringBuilder<NAME_SIZE> SrcDependencyBuilder;
|
|
while (ToProcess.Num())
|
|
{
|
|
TTuple<FString, FString> Package = ToProcess.Pop();
|
|
|
|
if (Result.Contains(Package.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// Become a patching name even if it doesn't have a file.
|
|
Result.Add({ Package.Key, Package.Value });
|
|
|
|
TArray<FName> Dependencies;
|
|
if (!Registry.GetDependencies(FName(*Package.Key), Dependencies))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FStringView SrcPackageRoot = FPackageName::SplitPackageNameRoot(Package.Key, nullptr);
|
|
FStringView DstPackageRoot = FPackageName::SplitPackageNameRoot(Package.Value, nullptr);
|
|
for (const FName Dependency : Dependencies)
|
|
{
|
|
Dependency.ToString(SrcDependencyBuilder);
|
|
FStringView SrcDependency = SrcDependencyBuilder.ToView();
|
|
|
|
if (PackagePathRenameMap.FindByHash(GetTypeHash(SrcDependency), SrcDependency))
|
|
{
|
|
// We already handled this mapping
|
|
continue;
|
|
}
|
|
|
|
FStringView SrcDependencyPackageRoot;
|
|
FStringView SrcDependencyPackagePath;
|
|
FStringView SrcDependencyPackageName;
|
|
SplitLongPackageName(SrcDependency, SrcDependencyPackageRoot, SrcDependencyPackagePath, SrcDependencyPackageName);
|
|
check(!SrcDependencyPackageRoot.IsEmpty());
|
|
|
|
// Only consider dependency paths that are for the same package as our src->dst mapping
|
|
// If the src mapping doesn't begin with a '/' the package name will be empty, since the path isn't a package path
|
|
if (SrcDependencyPackageRoot != SrcPackageRoot)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
TStringBuilder<NAME_SIZE> DstDependencyString;
|
|
|
|
// Special handling for external references. The __External[Actors__|Objects__] directory is always under the package root, may contain an
|
|
// arbitrary amount of subdirs but then ends with two hash subdirs. The path between the __External[Actors__|Objects__] and the two hash dirs
|
|
// may need remapping so we look at our external mappings to do so.
|
|
bool bHasExternalActorDir = SrcDependencyPackagePath.StartsWith(FPackagePath::GetExternalActorsFolderName());
|
|
bool bHasExternalObjectsDir = !bHasExternalActorDir && SrcDependencyPackagePath.StartsWith(FPackagePath::GetExternalObjectsFolderName());
|
|
if (bHasExternalActorDir || bHasExternalObjectsDir)
|
|
{
|
|
int32 RightPartStartPos;
|
|
if (!SrcDependencyPackagePath.FindChar(TEXT('/'), RightPartStartPos))
|
|
{
|
|
// This is a path to only the special directory, skip it no remapping is needed
|
|
continue;
|
|
}
|
|
RightPartStartPos++; // Skip past the '/'
|
|
|
|
// Find the start of the two hash dirs
|
|
// e.g. __ExternalActors__/path/of/interest/A/A9, we only want 'path/of/interest'
|
|
FStringView ExternalPackagePath(SrcDependencyPackagePath.GetData() + RightPartStartPos, SrcDependencyPackagePath.Len() - RightPartStartPos);
|
|
int32 HashDirStartPos = 0;
|
|
int32 NumHashDirsToStrip = 2;
|
|
while (NumHashDirsToStrip--)
|
|
{
|
|
if (ExternalPackagePath.FindLastChar(TEXT('/'), HashDirStartPos))
|
|
{
|
|
ExternalPackagePath.LeftChopInline(ExternalPackagePath.Len() - HashDirStartPos);
|
|
}
|
|
}
|
|
|
|
// Our __External[Actors|Objects]__ path is malformed
|
|
if (HashDirStartPos == INDEX_NONE)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
const int32 HashPathOffset = RightPartStartPos + HashDirStartPos;
|
|
FStringView HashPath(SrcDependencyPackagePath.GetData() + HashPathOffset, SrcDependencyPackagePath.Len() - HashPathOffset);
|
|
const TMap<FString, FString>* ExternalMappings = PluginExternalMappings.FindByHash(GetTypeHash(SrcPackageRoot), SrcPackageRoot);
|
|
if (!ExternalMappings)
|
|
{
|
|
// We have no mapping for this dependency's external actors/objects
|
|
continue;
|
|
}
|
|
const FString* DstExternalPackagePath = ExternalMappings->FindByHash(GetTypeHash(ExternalPackagePath), ExternalPackagePath);
|
|
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
DstDependencyString.Append(DstPackageRoot);
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
DstDependencyString.Append(bHasExternalActorDir ? FPackagePath::GetExternalActorsFolderName() : FPackagePath::GetExternalObjectsFolderName());
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
DstDependencyString.Append(DstExternalPackagePath ? *DstExternalPackagePath : ExternalPackagePath);
|
|
DstDependencyString.Append(HashPath); // HashPath already contains the leading '/'
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
DstDependencyString.Append(SrcDependencyPackageName);
|
|
}
|
|
else
|
|
{
|
|
// We aren't handling a special directory so replace the package root
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
DstDependencyString.Append(DstPackageRoot);
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
|
|
if (!SrcDependencyPackagePath.IsEmpty())
|
|
{
|
|
DstDependencyString.Append(SrcDependencyPackagePath);
|
|
DstDependencyString.AppendChar(TEXT('/'));
|
|
}
|
|
|
|
DstDependencyString.Append(SrcDependencyPackageName);
|
|
}
|
|
|
|
// If a dep start with the package name, then we are going to copy the asset.
|
|
// but we need to recurse on this asset as it may have sub dependencies we don't know of yet.
|
|
ToProcess.Add({ FString(SrcDependency), DstDependencyString.ToString() });
|
|
}
|
|
}
|
|
|
|
PackagePathRenameMap = MoveTemp(Result);
|
|
}
|
|
|
|
void FAssetHeaderPatcher::FContext::GenerateAdditionalRemappings()
|
|
{
|
|
TArray<FCoreRedirect> ExternalObjectRedirects;
|
|
TStringBuilder<24> ExternalActorsFolderBuilder;
|
|
ExternalActorsFolderBuilder << FPackagePath::GetExternalActorsFolderName() << TEXT("/");
|
|
const FStringView ExternalActorsFolder = ExternalActorsFolderBuilder.ToView();
|
|
|
|
TStringBuilder<24> ExternalObjectsFolderBuilder;
|
|
ExternalObjectsFolderBuilder << FPackagePath::GetExternalObjectsFolderName() << TEXT("/");
|
|
const FStringView ExternalObjectsFolder = ExternalObjectsFolderBuilder.ToView();
|
|
|
|
TStringBuilder<NAME_SIZE> SrcNameBuilder;
|
|
TStringBuilder<NAME_SIZE> DstNameBuilder;
|
|
for (const TTuple<FString, FString>& Package : PackagePathRenameMap)
|
|
{
|
|
const FString& SrcNameString = Package.Key;
|
|
const FString& DstNameString = Package.Value;
|
|
|
|
bool bIsExternalObjectOrActor = false;
|
|
FStringView SrcPackageName;
|
|
{
|
|
FStringView SrcPackageRoot;
|
|
FStringView SrcPackagePath;
|
|
if (!ensure(SplitLongPackageName(SrcNameString, SrcPackageRoot, SrcPackagePath, SrcPackageName))
|
|
|| SrcPackagePath.StartsWith(ExternalActorsFolder)
|
|
|| SrcPackagePath.StartsWith(ExternalObjectsFolder))
|
|
{
|
|
bIsExternalObjectOrActor = true;
|
|
}
|
|
}
|
|
|
|
// /Path/To/Package mapping
|
|
{
|
|
FCoreRedirect PackageRedirect(ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(SrcNameString),
|
|
FCoreRedirectObjectName(DstNameString));
|
|
|
|
if (bIsExternalObjectOrActor)
|
|
{
|
|
// The other mappings below don't apply to ExternalActors or ExternalObjects so we skip them
|
|
// now that we have a PackagePath mapping for them
|
|
ExternalObjectRedirects.Emplace(MoveTemp(PackageRedirect));
|
|
continue;
|
|
}
|
|
else
|
|
{
|
|
Redirects.Emplace(MoveTemp(PackageRedirect));
|
|
}
|
|
}
|
|
|
|
FStringView DstPackageName = FPathViews::GetBaseFilename(DstNameString);
|
|
|
|
// Path.ObjectName mapping
|
|
{
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.Append(SrcNameString);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
SrcNameBuilder.Append(SrcPackageName);
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.Append(DstNameString);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
DstNameBuilder.Append(DstPackageName);
|
|
|
|
FCoreRedirect PackageObjectRedirect(ECoreRedirectFlags::Type_Package | ECoreRedirectFlags::Type_Object,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(PackageObjectRedirect));
|
|
}
|
|
|
|
// Path.ObjectName.* mapping
|
|
{
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.Append(SrcNameString);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
SrcNameBuilder.Append(SrcPackageName);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.Append(DstNameString);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
DstNameBuilder.Append(DstPackageName);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
|
|
FCoreRedirect ObjectRedirect(ECoreRedirectFlags::Option_MatchPrefix | ECoreRedirectFlags::Type_Object,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(ObjectRedirect));
|
|
}
|
|
|
|
// Path.Object.PersistentLevel.* mapping
|
|
{
|
|
FName PersistentLevelFName(NAME_PersistentLevel);
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.Append(SrcNameString);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
SrcNameBuilder.Append(SrcPackageName);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
PersistentLevelFName.AppendString(SrcNameBuilder);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.Append(DstNameString);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
DstNameBuilder.Append(DstPackageName);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
PersistentLevelFName.AppendString(DstNameBuilder);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
|
|
FCoreRedirect ObjectRedirect(ECoreRedirectFlags::Option_MatchPrefix | ECoreRedirectFlags::Type_Object,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(ObjectRedirect));
|
|
}
|
|
|
|
// MaterialFunctionInterface "EditorOnlyData"
|
|
{
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.Append(SrcNameString);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
SrcNameBuilder.Append(SrcPackageName);
|
|
SrcNameBuilder.Append(TEXT("EditorOnlyData"));
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.Append(DstNameString);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
DstNameBuilder.Append(DstPackageName);
|
|
DstNameBuilder.Append(TEXT("EditorOnlyData"));
|
|
|
|
FCoreRedirect BlueprintClassRedirect(ECoreRedirectFlags::Type_Class | ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(BlueprintClassRedirect));
|
|
}
|
|
|
|
// Compiled Blueprint class names
|
|
{
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.Append(SrcNameString);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
SrcNameBuilder.Append(SrcPackageName);
|
|
SrcNameBuilder.Append(TEXT("_C"));
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.Append(DstNameString);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
DstNameBuilder.Append(DstPackageName);
|
|
DstNameBuilder.Append(TEXT("_C"));
|
|
|
|
FCoreRedirect BlueprintClassRedirect(ECoreRedirectFlags::Type_Class | ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(BlueprintClassRedirect));
|
|
}
|
|
|
|
// Blueprint generated class default object
|
|
{
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.Append(SrcNameString);
|
|
SrcNameBuilder.AppendChar(TEXT('.'));
|
|
SrcNameBuilder.Append(DEFAULT_OBJECT_PREFIX);
|
|
SrcNameBuilder.Append(SrcPackageName);
|
|
SrcNameBuilder.Append(TEXT("_C"));
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.Append(DstNameString);
|
|
DstNameBuilder.AppendChar(TEXT('.'));
|
|
DstNameBuilder.Append(DEFAULT_OBJECT_PREFIX);
|
|
DstNameBuilder.Append(DstPackageName);
|
|
DstNameBuilder.Append(TEXT("_C"));
|
|
|
|
FCoreRedirect DefaultBlueprintClassRedirect(ECoreRedirectFlags::Type_Class | ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(DefaultBlueprintClassRedirect));
|
|
}
|
|
}
|
|
|
|
// For best-effort string matches. Intentionally excluding external objects as AssetRegistry Tag data
|
|
// can't refer to these paths in a manner that we can't deduce from the redirects themselves
|
|
for (auto& Redirect : Redirects)
|
|
{
|
|
const FCoreRedirectObjectName& SrcName = Redirect.OldName;
|
|
const FCoreRedirectObjectName& DstName = Redirect.NewName;
|
|
|
|
// Do not include Src->Dst ObjectName mappings since it's too likely that we will incorrectly rename when dealing with string data
|
|
StringReplacements.Add(SrcName.PackageName.ToString(), DstName.PackageName.ToString());
|
|
StringReplacements.Add(SrcName.ToString(), DstName.ToString());
|
|
|
|
// Tag data can contain VersePaths which are like Top-Level Asset Paths
|
|
// but with a mountpoint prefix and only '/' delimiters
|
|
for (FString& VerseMount : VerseMountPoints)
|
|
{
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.AppendChar(TEXT('/'));
|
|
SrcNameBuilder.Append(VerseMount);
|
|
SrcName.PackageName.AppendString(SrcNameBuilder);
|
|
SrcNameBuilder.AppendChar(TEXT('/'));
|
|
SrcName.ObjectName.AppendString(SrcNameBuilder);
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.AppendChar(TEXT('/'));
|
|
DstNameBuilder.Append(VerseMount);
|
|
DstName.PackageName.AppendString(DstNameBuilder);
|
|
DstNameBuilder.AppendChar(TEXT('/'));
|
|
DstName.ObjectName.AppendString(DstNameBuilder);
|
|
StringReplacements.Add(SrcNameBuilder.ToString(), DstNameBuilder.ToString());
|
|
}
|
|
}
|
|
|
|
// Now that we have generated the string matches above, add the external redirects
|
|
Redirects.Append(ExternalObjectRedirects);
|
|
|
|
// Add prefix redirects for any mountpoint replacements
|
|
TMap<FString, FString> FormattedStringMountReplacements;
|
|
FormattedStringMountReplacements.Reserve(StringMountReplacements.Num());
|
|
for (const auto& MountPointPair : StringMountReplacements)
|
|
{
|
|
const FString& SrcMountPoint = MountPointPair.Key;
|
|
const FString& DstMountPoint = MountPointPair.Value;
|
|
|
|
SrcNameBuilder.Reset();
|
|
SrcNameBuilder.AppendChar(TEXT('/'));
|
|
SrcNameBuilder.Append(SrcMountPoint);
|
|
SrcNameBuilder.AppendChar(TEXT('/'));
|
|
|
|
DstNameBuilder.Reset();
|
|
DstNameBuilder.AppendChar(TEXT('/'));
|
|
DstNameBuilder.Append(DstMountPoint);
|
|
DstNameBuilder.AppendChar(TEXT('/'));
|
|
|
|
FCoreRedirect MountRedirect(ECoreRedirectFlags::Type_Package | ECoreRedirectFlags::Option_MatchPrefix,
|
|
FCoreRedirectObjectName(SrcNameBuilder.ToString()),
|
|
FCoreRedirectObjectName(DstNameBuilder.ToString()));
|
|
Redirects.Emplace(MoveTemp(MountRedirect));
|
|
|
|
// Store off the actual mount path prefix to make patching easier later
|
|
FormattedStringMountReplacements.Add(SrcNameBuilder.ToString(), DstNameBuilder.ToString());
|
|
}
|
|
StringMountReplacements = MoveTemp(FormattedStringMountReplacements);
|
|
}
|
|
|
|
// To override writing of FName's to ensure they have been patched
|
|
class FNamePatchingWriter final : public FArchiveProxy
|
|
{
|
|
public:
|
|
FNamePatchingWriter(FArchive& InAr, const TMap<FNameEntryId, int32>& InNameToIndexMap)
|
|
: FArchiveProxy(InAr)
|
|
, NameToIndexMap(InNameToIndexMap)
|
|
{
|
|
}
|
|
|
|
virtual ~FNamePatchingWriter() { }
|
|
|
|
virtual FArchive& operator<<(FName& Name) override
|
|
{
|
|
FNameEntryId EntryId = Name.GetDisplayIndex();
|
|
const int32* MaybeIndex = NameToIndexMap.Find(EntryId);
|
|
|
|
if (MaybeIndex == nullptr)
|
|
{
|
|
ErrorMessage += FString::Printf(TEXT("Cannot serialize FName '%s' because it is not in the name table for %s\n"), *Name.ToString(), *GetArchiveName());
|
|
SetCriticalError();
|
|
return *this;
|
|
}
|
|
|
|
int32 Index = *MaybeIndex;
|
|
int32 Number = Name.GetNumber();
|
|
|
|
FArchive& Ar = *this;
|
|
Ar << Index;
|
|
Ar << Number;
|
|
|
|
return *this;
|
|
}
|
|
|
|
const FString& GetErrorMessage() const
|
|
{
|
|
return ErrorMessage;
|
|
}
|
|
|
|
private:
|
|
const TMap<FNameEntryId, int32>& NameToIndexMap;
|
|
FString ErrorMessage;
|
|
};
|
|
|
|
enum class EPatchedSection
|
|
{
|
|
Summary,
|
|
NameTable,
|
|
SoftPathTable,
|
|
GatherableTextDataTable,
|
|
SearchableNamesMap,
|
|
ImportTable,
|
|
ExportTable,
|
|
SoftPackageReferencesTable,
|
|
ThumbnailTable,
|
|
AssetRegistryData,
|
|
AssetRegistryDependencyData,
|
|
};
|
|
|
|
struct FSectionData
|
|
{
|
|
EPatchedSection Section = EPatchedSection::Summary;
|
|
int64 Offset = 0;
|
|
int64 Size = 0;
|
|
bool bRequired = false;
|
|
};
|
|
|
|
enum class ESummaryOffset
|
|
{
|
|
NameTable,
|
|
SoftObjectPathList,
|
|
GatherableTextDataTable,
|
|
ImportTable,
|
|
ExportTable,
|
|
CellImportTable,
|
|
CellExportTable,
|
|
DependsTable,
|
|
SoftPackageReferenceList,
|
|
SearchableNamesMap,
|
|
ThumbnailTable,
|
|
AssetRegistryData,
|
|
WorldTileInfoData,
|
|
PreloadDependency, // Should not be present - only for cooked data
|
|
BulkData,
|
|
PayloadToc
|
|
};
|
|
|
|
// To override MemoryReaders FName method
|
|
class FReadFNameAs2IntFromMemoryReader final : public FLargeMemoryReader
|
|
{
|
|
public:
|
|
FReadFNameAs2IntFromMemoryReader(TArray<FName>& InNameTable, const uint8* InData, const int64 Num, ELargeMemoryReaderFlags InFlags = ELargeMemoryReaderFlags::None, const FName InArchiveName = NAME_None)
|
|
: FLargeMemoryReader(InData, Num, InFlags, InArchiveName)
|
|
, NameTable(InNameTable)
|
|
{
|
|
}
|
|
|
|
// FLargeMemoryReader falls back to FMemoryArchive's imp of this method.
|
|
// which uses strings as the format for FName.
|
|
// We need the 2xint32 version when decoding the current file formats.
|
|
virtual FArchive& operator<<(FName& OutName) override
|
|
{
|
|
int32 NameIndex;
|
|
int32 Number;
|
|
FArchive& Ar = *this;
|
|
Ar << NameIndex;
|
|
Ar << Number;
|
|
|
|
if (NameTable.IsValidIndex(NameIndex))
|
|
{
|
|
FNameEntryId MappedName = NameTable[NameIndex].GetDisplayIndex();
|
|
OutName = FName::CreateFromDisplayId(MappedName, Number);
|
|
}
|
|
else
|
|
{
|
|
OutName = FName();
|
|
SetCriticalError();
|
|
}
|
|
|
|
return *this;
|
|
}
|
|
|
|
virtual FString GetArchiveName() const override
|
|
{
|
|
return TEXT("FReadFNameAs2IntFromMemoryReader");
|
|
}
|
|
private:
|
|
TArray<FName>& NameTable;
|
|
};
|
|
|
|
struct FSummaryOffsetMeta
|
|
{
|
|
// NOTE: The offsets in Summary get to a max of 312 bytes.
|
|
// So we could drop this to a uint16 but that is probably overkill at this point.
|
|
uint32 Offset : 31;
|
|
uint32 bIs64Bit : 1;
|
|
|
|
int64 Value(FPackageFileSummary& Summary) const
|
|
{
|
|
intptr_t Ptr = reinterpret_cast<intptr_t>(&Summary) + Offset;
|
|
if (bIs64Bit)
|
|
{
|
|
return *reinterpret_cast<int64*>(Ptr);
|
|
}
|
|
else
|
|
{
|
|
return *reinterpret_cast<int32*>(Ptr);
|
|
}
|
|
}
|
|
|
|
void PatchOffsetValue(FPackageFileSummary& Summary, int64 Value) const
|
|
{
|
|
intptr_t Ptr = reinterpret_cast<intptr_t>(&Summary) + Offset;
|
|
if (bIs64Bit)
|
|
{
|
|
int64& Dst = *reinterpret_cast<int64*>(Ptr);
|
|
Dst += Value;
|
|
}
|
|
else
|
|
{
|
|
int32& Dst = *reinterpret_cast<int32*>(Ptr);
|
|
*reinterpret_cast<int32*>(Ptr) = IntCastChecked<int32>((int64)Dst + Value);
|
|
}
|
|
}
|
|
};
|
|
|
|
void PatchSummaryOffsets(FPackageFileSummary& Dst, int64 OffsetFrom, int64 OffsetDelta)
|
|
{
|
|
if (!OffsetDelta)
|
|
{
|
|
return;
|
|
}
|
|
|
|
constexpr FSummaryOffsetMeta OffsetTable[] = {
|
|
|
|
#define UE_POPULATE_OFFSET_INFO(NAME) \
|
|
(uint32)STRUCT_OFFSET(FPackageFileSummary, NAME), \
|
|
std::is_same_v<decltype(((FPackageFileSummary*)0)->NAME), int64>
|
|
|
|
{ UE_POPULATE_OFFSET_INFO(NameOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(SoftObjectPathsOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(GatherableTextDataOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(MetaDataOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(ImportOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(ExportOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(CellImportOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(CellExportOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(DependsOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(SoftPackageReferencesOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(SearchableNamesOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(ThumbnailTableOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(AssetRegistryDataOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(BulkDataStartOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(WorldTileInfoDataOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(PreloadDependencyOffset) },
|
|
{ UE_POPULATE_OFFSET_INFO(PayloadTocOffset) },
|
|
|
|
#undef UE_POPULATE_OFFSET_INFO
|
|
};
|
|
|
|
for (const FSummaryOffsetMeta& OffsetData : OffsetTable)
|
|
{
|
|
if (OffsetData.Value(Dst) > OffsetFrom)
|
|
{
|
|
OffsetData.PatchOffsetValue(Dst, OffsetDelta);
|
|
}
|
|
}
|
|
};
|
|
|
|
FAssetDataTagMap MakeTagMap(const TArray<UE::AssetRegistry::FDeserializeTagData>& TagData)
|
|
{
|
|
FAssetDataTagMap Out;
|
|
Out.Reserve(TagData.Num());
|
|
for (const UE::AssetRegistry::FDeserializeTagData& Tag : TagData)
|
|
{
|
|
if (!Tag.Key.IsEmpty() && !Tag.Value.IsEmpty())
|
|
{
|
|
Out.Add(*Tag.Key, Tag.Value);
|
|
}
|
|
}
|
|
|
|
return Out;
|
|
}
|
|
|
|
// The information we need in the task to do patching.
|
|
class FAssetHeaderPatcherInner
|
|
{
|
|
public:
|
|
using EResult = FAssetHeaderPatcher::EResult;
|
|
|
|
struct FThumbnailEntry
|
|
{
|
|
FString ObjectShortClassName;
|
|
FString ObjectPathWithoutPackageName;
|
|
int32 FileOffset = 0;
|
|
};
|
|
|
|
FAssetHeaderPatcherInner(const FString& InSrcAsset, const FString& InDstAsset, const TMap<FString, FString>& InStringReplacements, const TMap<FString, FString>& InStringMountPointReplacements, FArchive* InDstArchive = nullptr)
|
|
: SrcAsset(InSrcAsset)
|
|
, DstAsset(InDstAsset)
|
|
, StringReplacements(InStringReplacements)
|
|
, StringMountPointReplacements(InStringMountPointReplacements)
|
|
, DstArchive(InDstArchive)
|
|
, bPatchPrimaryAssetTag(false)
|
|
, bIsPackagePathInNametable(false)
|
|
, bIsNonOneFilePerActorPackage(false)
|
|
{
|
|
for (auto TagToIgnore : TagsToIgnore)
|
|
{
|
|
IgnoredTags.Add(TagToIgnore);
|
|
}
|
|
}
|
|
|
|
// Reset anything not set via construction. Used for testing
|
|
void ResetInternalState()
|
|
{
|
|
AssetRegistryData = FAssetRegistryData();
|
|
HeaderInformation = FHeaderInformation();
|
|
AddedNames.Empty();
|
|
ExportTable.Empty();
|
|
ImportTable.Empty();
|
|
ImportTablePatchedNames.Empty();
|
|
ImportNameToImportTableIndexLookup.Empty();
|
|
GatherableTextDataTable.Empty();
|
|
NameTable.Empty();
|
|
NameToIndexMap.Empty();
|
|
RenameMap.Empty();
|
|
SearchableNamesMap.Empty();
|
|
SoftObjectPathTable.Empty();
|
|
SoftPackageReferencesTable.Empty();
|
|
Summary = FPackageFileSummary();
|
|
ThumbnailTable.Empty();
|
|
UnchangedNames.Empty();
|
|
}
|
|
|
|
bool DoPatch(FString& InOutString);
|
|
bool DoPatch(FName& InOutName);
|
|
bool DoPatch(FSoftObjectPath& InOutSoft);
|
|
bool DoPatch(FTopLevelAssetPath& InOutPath);
|
|
bool DoPatch(FGatherableTextData& InOutGatherablerTextData);
|
|
bool DoPatch(FThumbnailEntry& InOutThumbnailEntry);
|
|
bool RemapFName(FName SrcName, FName DstName);
|
|
bool AddFName(FName DstName);
|
|
bool ShouldReplaceMountPoint(const FStringView InPath, FStringView& OutSrcMountPoint, FStringView& OutDstMountPoint);
|
|
FCoreRedirectObjectName GetFullObjectNameFromObjectResource(const FObjectResource& InResource, bool bIsExport, bool bWalkImportsOnly = false);
|
|
|
|
struct FExportPatch
|
|
{
|
|
int32 TableIndex;
|
|
FName ObjectName;
|
|
};
|
|
|
|
struct FImportPatch
|
|
{
|
|
int32 TableIndex;
|
|
FName ObjectName;
|
|
FPackageIndex OuterIndex;
|
|
FName ClassName;
|
|
FName ClassPackage;
|
|
#if WITH_EDITORONLY_DATA
|
|
FName PackageName;
|
|
#endif
|
|
bool bUsedInGame = true;
|
|
};
|
|
|
|
void GetExportTablePatches(TArray<FExportPatch>& OutExportPatches);
|
|
FAssetHeaderPatcher::EResult GetImportTablePatches(TArray<FImportPatch>& OutImportPatches, int32& OutNewImportCount);
|
|
void PatchExportAndImportTables(const TArray<FExportPatch>& InExportPatches, const TArray<FImportPatch>& InImportPatches, const int32 InNewImportCount);
|
|
void PatchNameTable();
|
|
|
|
FAssetHeaderPatcher::EResult PatchHeader();
|
|
FAssetHeaderPatcher::EResult PatchHeader_Deserialize();
|
|
FAssetHeaderPatcher::EResult PatchHeader_PatchSections();
|
|
FAssetHeaderPatcher::EResult PatchHeader_WriteDestinationFile();
|
|
void DumpState(FStringView InDir);
|
|
|
|
TSet<FString> IgnoredTags;
|
|
|
|
const FString& SrcAsset;
|
|
const FString& DstAsset;
|
|
const TMap<FString, FString>& StringReplacements;
|
|
const TMap<FString, FString>& StringMountPointReplacements;
|
|
FArchive* DstArchive = nullptr;
|
|
TUniquePtr<FArchive> DstArchiveOwner;
|
|
|
|
TArray64<uint8> SrcBuffer;
|
|
|
|
struct FHeaderInformation
|
|
{
|
|
int64 SummarySize = -1;
|
|
int64 NameTableSize = -1;
|
|
int64 SoftObjectPathListSize = -1;
|
|
int64 GatherableTextDataSize = -1;
|
|
int64 ImportTableSize = -1;
|
|
int64 ExportTableSize = -1;
|
|
int64 SoftPackageReferencesListSize = -1;
|
|
int64 ThumbnailTableSize = -1;
|
|
int64 SearchableNamesMapSize = -1;
|
|
int64 AssetRegistryDataSize = -1;
|
|
int64 PackageTrailerSize = -1;
|
|
};
|
|
|
|
FHeaderInformation HeaderInformation;
|
|
FPackageFileSummary Summary;
|
|
FName OriginalPackagePath; // e.g. "/MountName/TopLevelPackageName"
|
|
FName OriginalNonOneFilePerActorPackagePath; // e.g. "/MountName/MountName"
|
|
FName DstPackagePath; // OriginalPackagePath, or the remapped name of it if it was remapped
|
|
FString OriginalPrimaryAssetName; // e.g. "MountName"
|
|
bool bPatchPrimaryAssetTag;
|
|
bool bIsPackagePathInNametable;
|
|
bool bIsNonOneFilePerActorPackage;
|
|
|
|
// NameTable Members
|
|
TArray<FName> NameTable;
|
|
TMap<FNameEntryId, int32> NameToIndexMap;
|
|
TSet<FNameEntryId> UnchangedNames;
|
|
TMap<FNameEntryId, FNameEntryId> RenameMap;
|
|
TSet<FNameEntryId> AddedNames;
|
|
// Export/Import table
|
|
TArray<TPair<FCoreRedirectObjectName, FCoreRedirectObjectName>> ImportTablePatchedNames;
|
|
TMap<FCoreRedirectObjectName, int32> ImportNameToImportTableIndexLookup;
|
|
|
|
TArray<FSoftObjectPath> SoftObjectPathTable;
|
|
TArray<FGatherableTextData> GatherableTextDataTable;
|
|
TArray<FObjectImport> ImportTable;
|
|
TArray<FObjectExport> ExportTable;
|
|
TArray<FName> SoftPackageReferencesTable;
|
|
TMap<FPackageIndex, TArray<FName>> SearchableNamesMap;
|
|
TArray<FThumbnailEntry> ThumbnailTable;
|
|
|
|
// Asset registry data information
|
|
struct FAssetRegistryObjectData
|
|
{
|
|
UE::AssetRegistry::FDeserializeObjectPackageData ObjectData;
|
|
TArray<UE::AssetRegistry::FDeserializeTagData> TagData;
|
|
};
|
|
|
|
struct FAssetRegistryData
|
|
{
|
|
int64 SectionSize = -1;
|
|
UE::AssetRegistry::FDeserializePackageData PkgData;
|
|
TArray<FAssetRegistryObjectData> ObjectData;
|
|
int64 DependencyDataSectionSize = -1;
|
|
TMap<int32, bool> ImportIndexUsedInGame;
|
|
TMap<FName, bool> SoftPackageReferenceUsedInGame;
|
|
TArray<TPair<FName, UE::AssetRegistry::EExtraDependencyFlags>> ExtraPackageDependencies;
|
|
};
|
|
FAssetRegistryData AssetRegistryData;
|
|
};
|
|
|
|
FAssetHeaderPatcher::EResult FAssetHeaderPatcher::DoPatch(const FString& InSrcAsset, const FString& InDstAsset, const FContext& InContext)
|
|
{
|
|
FAssetHeaderPatcherInner Inner(InSrcAsset, InDstAsset, InContext.StringReplacements, InContext.StringMountReplacements);
|
|
if (!FFileHelper::LoadFileToArray(Inner.SrcBuffer, *Inner.SrcAsset))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to load %s"), *Inner.SrcAsset);
|
|
return FAssetHeaderPatcherInner::EResult::ErrorFailedToLoadSourceAsset;
|
|
}
|
|
else
|
|
{
|
|
// Swap in the CoreRedirect context for the patcher since we might be running on a different thread with a separate context
|
|
// We do not use a FScopeCoreRedirectsContext as that will copy into a new context but we want to re-use the patcher's context
|
|
FCoreRedirectsContext& OriginalContext = FCoreRedirectsContext::GetThreadContext();
|
|
FCoreRedirectsContext::SetThreadContext(InContext.RedirectsContext);
|
|
ON_SCOPE_EXIT{ FCoreRedirectsContext::SetThreadContext(OriginalContext); };
|
|
|
|
return Inner.PatchHeader();
|
|
}
|
|
}
|
|
|
|
void FAssetHeaderPatcher::Reset()
|
|
{
|
|
ErroredFiles.Empty();
|
|
PatchedFiles.Empty();
|
|
|
|
PatchingTask = UE::Tasks::FTask();
|
|
Status = EResult::NotStarted;
|
|
bCancelled = false;
|
|
}
|
|
void FAssetHeaderPatcher::SetContext(FContext InContext)
|
|
{
|
|
checkf(!IsPatching(), TEXT("Cannot set the patcher context while patching"));
|
|
Context = InContext;
|
|
|
|
// Copy the global context into our own to inherit any global redirects already loaded
|
|
Context.RedirectsContext = FCoreRedirectsContext(FCoreRedirectsContext::GetGlobalContext());
|
|
Context.RedirectsContext.InitializeContext();
|
|
|
|
// Disable validation of the CoreRedirects as these redirects are handmade and should not need extra validation.
|
|
// RedirectionSummary is unimportant for patching and will result in unnecessary allocations when we add our redirects.
|
|
// Leave DebugMode on since if someone wants to debug, we should not prevent them.
|
|
FCoreRedirectsContext::EFlags NewFlags = Context.RedirectsContext.GetFlags();
|
|
NewFlags = NewFlags & ~(FCoreRedirectsContext::EFlags::ValidateAddedRedirects | FCoreRedirectsContext::EFlags::UseRedirectionSummary);
|
|
Context.RedirectsContext.SetFlags(NewFlags);
|
|
|
|
{
|
|
// Swap the thread context to the patcher's FCoreRedirectsContext so we may populate it once and share it with the task threads
|
|
// We do not use a FScopeCoreRedirectsContext as that will copy into a new context but we want to re-use the patcher's context
|
|
FCoreRedirectsContext& OriginalContext = FCoreRedirectsContext::GetThreadContext();
|
|
FCoreRedirectsContext::SetThreadContext(Context.RedirectsContext);
|
|
ON_SCOPE_EXIT{ FCoreRedirectsContext::SetThreadContext(OriginalContext); };
|
|
|
|
FCoreRedirects::AddRedirectList(Context.Redirects, TEXT("Asset Header Patcher"));
|
|
}
|
|
|
|
Reset();
|
|
}
|
|
|
|
UE::Tasks::FTask FAssetHeaderPatcher::PatchAsync(int32* InOutNumFilesToPatch, int32* InOutNumFilesPatched)
|
|
{
|
|
return PatchAsync(InOutNumFilesToPatch, InOutNumFilesPatched, FAssetHeaderPatcherCompletionDelegate(), FAssetHeaderPatcherCompletionDelegate());
|
|
}
|
|
|
|
UE::Tasks::FTask FAssetHeaderPatcher::PatchAsync(int32* InOutNumFilesToPatch, int32* InOutNumFilesPatched, FAssetHeaderPatcherCompletionDelegate InOnSuccess, FAssetHeaderPatcherCompletionDelegate InOnError)
|
|
{
|
|
PatchedFiles = Context.FilePathRenameMap;
|
|
if (InOutNumFilesToPatch)
|
|
{
|
|
*InOutNumFilesToPatch = PatchedFiles.Num();
|
|
}
|
|
|
|
// Spawn tasks (Scatter)
|
|
UE::Tasks::FTask PatchAssetsCleanupTask;
|
|
TArray<UE::Tasks::FTask> PatchAssetTasks;
|
|
|
|
// Note we are scheduling and launching tasks one at a time rather than preparing all jobs and launching all at once.
|
|
// While this means more overhead scheduling, it means that we won't have many tasks all hit the filesystem at the same time
|
|
// attempting to read and (more importantly) write to disk at the exact same time.
|
|
#if DEBUG_ASSET_HEADER_PATCHING
|
|
constexpr bool bSingleThreaded = true; // Useful for debugging
|
|
#else
|
|
constexpr bool bSingleThreaded = false;
|
|
#endif
|
|
for (const TTuple<FString, FString>& Filename : PatchedFiles)
|
|
{
|
|
auto DoPatchFn = [this,
|
|
SrcFilename = Filename.Key,
|
|
DstFilename = Filename.Value,
|
|
NumPatched = InOutNumFilesPatched,
|
|
OnSuccess = InOnSuccess,
|
|
OnError = InOnError]()
|
|
{
|
|
// Even if we are cancelled, increment our progress
|
|
if (NumPatched)
|
|
{
|
|
// We don't support C++20 in all modules and platforms yet and avoid using atomic_ref as a result
|
|
FPlatformAtomics::InterlockedAdd((volatile int32*)NumPatched, 1);
|
|
}
|
|
|
|
if (bCancelled)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FAssetHeaderPatcher::EResult Result = FAssetHeaderPatcher::DoPatch(SrcFilename, DstFilename, Context);
|
|
if (Result != FAssetHeaderPatcher::EResult::Success)
|
|
{
|
|
FScopeLock Lock(&ErroredFilesLock);
|
|
// Don't lose our cancelled state, even when there are errors
|
|
if (Status != EResult::Cancelled)
|
|
{
|
|
Status = Result;
|
|
}
|
|
ErroredFiles.Add(SrcFilename, Result);
|
|
|
|
OnError.ExecuteIfBound(SrcFilename, DstFilename);
|
|
}
|
|
else
|
|
{
|
|
OnSuccess.ExecuteIfBound(SrcFilename, DstFilename);
|
|
}
|
|
};
|
|
|
|
if constexpr (bSingleThreaded)
|
|
{
|
|
DoPatchFn();
|
|
}
|
|
else
|
|
{
|
|
PatchAssetTasks.Add(UE::Tasks::Launch(UE_SOURCE_LOCATION, MoveTemp(DoPatchFn)));
|
|
}
|
|
}
|
|
|
|
// Once all tasks have completed, remove the redirects before we declare Patching complete
|
|
UE::Tasks::FTask PatcherCleanupTask = UE::Tasks::Launch(UE_SOURCE_LOCATION, [this]()
|
|
{
|
|
if (Status != EResult::Cancelled && ErroredFiles.IsEmpty())
|
|
{
|
|
Status = EResult::Success;
|
|
}
|
|
|
|
{
|
|
FScopeLock Lock(&ErroredFilesLock);
|
|
for (auto& ErroredFile : ErroredFiles)
|
|
{
|
|
PatchedFiles.Remove(ErroredFile.Key);
|
|
}
|
|
}
|
|
|
|
}, UE::Tasks::Prerequisites(PatchAssetTasks));
|
|
|
|
Status = EResult::InProgress;
|
|
|
|
return PatcherCleanupTask;
|
|
}
|
|
|
|
FAssetHeaderPatcher::EResult FAssetHeaderPatcherInner::PatchHeader()
|
|
{
|
|
FAssetHeaderPatcher::EResult Result = PatchHeader_Deserialize();
|
|
if (Result != EResult::Success)
|
|
{
|
|
return Result;
|
|
}
|
|
|
|
if (DumpOutputDirectory.IsEmpty())
|
|
{
|
|
Result = PatchHeader_PatchSections();
|
|
if (Result != EResult::Success)
|
|
{
|
|
return Result;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
FString BaseDir = DumpOutputDirectory;
|
|
FPaths::NormalizeDirectoryName(BaseDir);
|
|
|
|
FString BeforeDir = BaseDir / FString(TEXT("Before"));
|
|
FPaths::RemoveDuplicateSlashes(BeforeDir);
|
|
DumpState(BeforeDir);
|
|
|
|
Result = PatchHeader_PatchSections();
|
|
if (Result != EResult::Success)
|
|
{
|
|
return Result;
|
|
}
|
|
|
|
FString AfterDir = BaseDir / FString(TEXT("After"));
|
|
FPaths::RemoveDuplicateSlashes(AfterDir);
|
|
DumpState(AfterDir);
|
|
}
|
|
|
|
return PatchHeader_WriteDestinationFile();
|
|
}
|
|
|
|
FAssetHeaderPatcher::EResult FAssetHeaderPatcherInner::PatchHeader_Deserialize()
|
|
{
|
|
FReadFNameAs2IntFromMemoryReader MemAr(NameTable, SrcBuffer.GetData(), SrcBuffer.Num());
|
|
|
|
MemAr << Summary;
|
|
HeaderInformation.SummarySize = MemAr.Tell();
|
|
|
|
// Summary.PackageName isn't always serialized. In such cases, determine the package name from the file name
|
|
if (Summary.PackageName.IsEmpty() || Summary.PackageName.Equals(TEXT("None")))
|
|
{
|
|
// e.g. "../../Some/Long/Path/MyPlugin/Plugins/MyPackage/Content/TopLevelAssetName.uasset"
|
|
TStringView Path(SrcAsset);
|
|
|
|
static const TStringView ContentDir(TEXT("/Content/"));
|
|
int32 Pos = Path.Find(ContentDir, ESearchCase::IgnoreCase);
|
|
if (Pos <= 0)
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Cannot patch '%s': Package header is missing a 'PackageName' string, nor could a PackageName be deduced."), *SrcAsset);
|
|
return FAssetHeaderPatcher::EResult::ErrorEmptyRequireSection;
|
|
}
|
|
|
|
int32 MountNamePos;
|
|
TStringView LeftPath(Path.GetData(), Pos);
|
|
if (!LeftPath.FindLastChar(TEXT('/'), MountNamePos))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Cannot patch '%s': Package header is missing a 'PackageName' string, nor could a PackageName be deduced."), *SrcAsset);
|
|
return FAssetHeaderPatcher::EResult::ErrorEmptyRequireSection;
|
|
}
|
|
|
|
int32 ExtensionPos;
|
|
TStringView RightPath(Path.GetData() + Pos + ContentDir.Len());
|
|
if (!RightPath.FindLastChar(TEXT('.'), ExtensionPos))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Cannot patch '%s': Package header is missing a 'PackageName' string, nor could a PackageName be deduced."), *SrcAsset);
|
|
return FAssetHeaderPatcher::EResult::ErrorEmptyRequireSection;
|
|
}
|
|
|
|
TStringView MountName(LeftPath.GetData() + MountNamePos, (Pos - MountNamePos) + 1); // + 1 so we can include the '/' from "/Content"
|
|
TStringView AssetPath(RightPath.GetData(), ExtensionPos);
|
|
Summary.PackageName.Empty(MountName.Len() + AssetPath.Len());
|
|
Summary.PackageName.Append(MountName);
|
|
Summary.PackageName.Append(AssetPath);
|
|
}
|
|
|
|
// Store the original name as an FName as it will be used when
|
|
// patching paths for other objects in the package
|
|
{
|
|
OriginalPackagePath = FName(Summary.PackageName, NAME_NO_NUMBER_INTERNAL);
|
|
|
|
// Some ObjectPaths have an implied package, however when it comes to
|
|
// non-One File Per Actor packages, the implied package is the map package
|
|
// so we determine which package we are and cache the map name in case we need it
|
|
{
|
|
bIsNonOneFilePerActorPackage = false;
|
|
TStringBuilder<256> PathBuilder;
|
|
PathBuilder.AppendChar(TEXT('/'));
|
|
PathBuilder.Append(FPackagePath::GetExternalActorsFolderName());
|
|
PathBuilder.AppendChar(TEXT('/'));
|
|
if (Summary.PackageName.Contains(PathBuilder))
|
|
{
|
|
bIsNonOneFilePerActorPackage = true;
|
|
}
|
|
else
|
|
{
|
|
PathBuilder.Reset();
|
|
PathBuilder.AppendChar(TEXT('/'));
|
|
PathBuilder.Append(FPackagePath::GetExternalObjectsFolderName());
|
|
PathBuilder.AppendChar(TEXT('/'));
|
|
bIsNonOneFilePerActorPackage = Summary.PackageName.Contains(PathBuilder);
|
|
}
|
|
|
|
int32 SlashPos = INDEX_NONE;
|
|
FStringView PackageRoot(Summary.PackageName);
|
|
if (!PackageRoot.FindChar(TEXT('/'), SlashPos) || SlashPos != 0)
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Cannot patch '%s': PackageName is malformed."), *SrcAsset);
|
|
return FAssetHeaderPatcher::EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
|
|
PackageRoot.RightChopInline(1); // Drop the first slash
|
|
if (!PackageRoot.FindChar(TEXT('/'), SlashPos))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Cannot patch '%s': PackageName is malformed."), *SrcAsset);
|
|
return FAssetHeaderPatcher::EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
|
|
PathBuilder.Reset();
|
|
PathBuilder.AppendChar(TEXT('/'));
|
|
PathBuilder.Append(PackageRoot.GetData(), SlashPos);
|
|
PathBuilder.AppendChar(TEXT('/'));
|
|
PathBuilder.Append(PackageRoot.GetData(), SlashPos);
|
|
OriginalNonOneFilePerActorPackagePath = FName(PathBuilder);
|
|
|
|
// While here set the OriginalPrimaryAssetName which is used in AssetRegistry Tag lookups for GameFeatureData
|
|
bPatchPrimaryAssetTag = FPathViews::GetBaseFilename(Summary.PackageName) == TEXT("GameFeatureData");
|
|
OriginalPrimaryAssetName.Empty();
|
|
OriginalPrimaryAssetName.Append(PackageRoot.GetData(), SlashPos);
|
|
}
|
|
}
|
|
|
|
// set version numbers so components branch correctly
|
|
MemAr.SetUEVer(Summary.GetFileVersionUE());
|
|
MemAr.SetLicenseeUEVer(Summary.GetFileVersionLicenseeUE());
|
|
MemAr.SetEngineVer(Summary.SavedByEngineVersion);
|
|
MemAr.SetCustomVersions(Summary.GetCustomVersionContainer());
|
|
if (Summary.GetPackageFlags() & PKG_FilterEditorOnly)
|
|
{
|
|
MemAr.SetFilterEditorOnly(true);
|
|
}
|
|
|
|
if (Summary.DataResourceOffset > 0)
|
|
{
|
|
// Should only be set in cooked data. If that changes, we need to add code to patch it
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Asset %s has an unexpected DataResourceOffset"), *SrcAsset);
|
|
return EResult::ErrorUnexpectedSectionOrder;
|
|
}
|
|
if (Summary.CellExportCount > 0 || Summary.CellImportCount > 0)
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Asset %s contains unexpected VCells"), *SrcAsset);
|
|
return EResult::ErrorUnexpectedSectionOrder;
|
|
}
|
|
|
|
if (Summary.NameCount > 0)
|
|
{
|
|
MemAr.Seek(Summary.NameOffset);
|
|
NameTable.Reserve(Summary.NameCount);
|
|
for (int32 NameMapIdx = 0; NameMapIdx < Summary.NameCount; ++NameMapIdx)
|
|
{
|
|
FNameEntrySerialized NameEntry(ENAME_LinkerConstructor);
|
|
MemAr << NameEntry;
|
|
NameTable.Add(FName(NameEntry));
|
|
}
|
|
|
|
HeaderInformation.NameTableSize = MemAr.Tell() - HeaderInformation.SummarySize;
|
|
|
|
// Initialize a mapping for Name to index in NameTable as we will use
|
|
// this for patching in new names and to determine if multiple FNames share the same
|
|
// value but might not after patching (i.e. their use of the name differs based on context, and
|
|
// post-patching the FNames in those contexts no longer match.
|
|
NameToIndexMap.Empty(NameTable.Num());
|
|
UnchangedNames.Reserve(NameTable.Num());
|
|
RenameMap.Reserve(NameTable.Num());
|
|
AddedNames.Empty();
|
|
for (int32 i = 0; i < NameTable.Num(); ++i)
|
|
{
|
|
NameToIndexMap.Add(NameTable[i].GetDisplayIndex(), i);
|
|
}
|
|
}
|
|
|
|
if (Summary.SoftObjectPathsCount > 0)
|
|
{
|
|
MemAr.Seek(Summary.SoftObjectPathsOffset);
|
|
SoftObjectPathTable.Reserve(Summary.SoftObjectPathsCount);
|
|
for (int32 Idx = 0; Idx < Summary.SoftObjectPathsCount; ++Idx)
|
|
{
|
|
// Note, a non IsPersistent() archive is used to preserve the original
|
|
// FSoftObjectPaths found in the header, since those will refer to entries in the
|
|
// NameTable. The call to SerializePath below will redirect FNames when using an
|
|
// IsPersistent archive which would make fixing up the nametable more difficult
|
|
FSoftObjectPath& PathRef = SoftObjectPathTable.AddDefaulted_GetRef();
|
|
PathRef.SerializePath(MemAr);
|
|
}
|
|
HeaderInformation.SoftObjectPathListSize = MemAr.Tell() - Summary.SoftObjectPathsOffset;
|
|
}
|
|
else if(Summary.GetFileVersionUE() >= EUnrealEngineObjectUE5Version::ADD_SOFTOBJECTPATH_LIST)
|
|
{
|
|
HeaderInformation.SoftObjectPathListSize = 0;
|
|
}
|
|
else
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Asset '%s' is too old to be used with AssetHeaderPatching. Please resave the file before trying to patch again."), *SrcAsset);
|
|
return EResult::ErrorUnkownSection;
|
|
}
|
|
|
|
if (Summary.GatherableTextDataCount > 0)
|
|
{
|
|
MemAr.Seek(Summary.GatherableTextDataOffset);
|
|
GatherableTextDataTable.Reserve(Summary.GatherableTextDataCount);
|
|
for (int32 GatherableTextDataIndex = 0; GatherableTextDataIndex < Summary.GatherableTextDataCount; ++GatherableTextDataIndex)
|
|
{
|
|
FGatherableTextData& GatherableTextData = GatherableTextDataTable.Emplace_GetRef();
|
|
MemAr << GatherableTextData;
|
|
}
|
|
|
|
HeaderInformation.GatherableTextDataSize = MemAr.Tell() - Summary.GatherableTextDataOffset;
|
|
}
|
|
else
|
|
{
|
|
HeaderInformation.GatherableTextDataSize = 0;
|
|
}
|
|
|
|
#define UE_CHECK_AND_SET_ERROR_AND_RETURN(EXP) \
|
|
do \
|
|
{ \
|
|
if (EXP) \
|
|
{ \
|
|
UE_LOG(LogAssetHeaderPatcher, Log, TEXT("Asset %s fails %s"), *SrcAsset, TEXT(#EXP)); \
|
|
return EResult::ErrorBadOffset; \
|
|
} \
|
|
} \
|
|
while(0)
|
|
|
|
if (Summary.ImportCount > 0)
|
|
{
|
|
UE_CHECK_AND_SET_ERROR_AND_RETURN(Summary.ImportOffset >= Summary.TotalHeaderSize);
|
|
UE_CHECK_AND_SET_ERROR_AND_RETURN(Summary.ImportOffset < 0);
|
|
|
|
MemAr.Seek(Summary.ImportOffset);
|
|
ImportTable.Reserve(Summary.ImportCount);
|
|
ImportTablePatchedNames.Reserve(ImportTable.Num());
|
|
ImportNameToImportTableIndexLookup.Reserve(ImportTable.Num());
|
|
for (int32 ImportIndex = 0; ImportIndex < Summary.ImportCount; ++ImportIndex)
|
|
{
|
|
FObjectImport& Import = ImportTable.Emplace_GetRef();
|
|
MemAr << Import;
|
|
}
|
|
|
|
HeaderInformation.ImportTableSize = MemAr.Tell() - Summary.ImportOffset;
|
|
}
|
|
else
|
|
{
|
|
HeaderInformation.ImportTableSize = 0;
|
|
}
|
|
|
|
if (Summary.ExportCount > 0)
|
|
{
|
|
UE_CHECK_AND_SET_ERROR_AND_RETURN(Summary.ExportOffset >= Summary.TotalHeaderSize);
|
|
UE_CHECK_AND_SET_ERROR_AND_RETURN(Summary.ExportOffset < 0);
|
|
|
|
MemAr.Seek(Summary.ExportOffset);
|
|
ExportTable.Reserve(Summary.ExportCount);
|
|
for (int32 ExportIndex = 0; ExportIndex < Summary.ExportCount; ++ExportIndex)
|
|
{
|
|
FObjectExport& Export = ExportTable.Emplace_GetRef();
|
|
MemAr << Export;
|
|
}
|
|
|
|
HeaderInformation.ExportTableSize = MemAr.Tell() - Summary.ExportOffset;
|
|
}
|
|
else
|
|
{
|
|
HeaderInformation.ExportTableSize = 0;
|
|
}
|
|
|
|
#undef UE_CHECK_AND_SET_ERROR_AND_RETURN
|
|
|
|
if (Summary.SoftPackageReferencesCount)
|
|
{
|
|
MemAr.Seek(Summary.SoftPackageReferencesOffset);
|
|
SoftPackageReferencesTable.Reserve(Summary.SoftPackageReferencesCount);
|
|
for (int32 Idx = 0; Idx < Summary.SoftPackageReferencesCount; ++Idx)
|
|
{
|
|
FName& Reference = SoftPackageReferencesTable.Emplace_GetRef();
|
|
MemAr << Reference;
|
|
}
|
|
|
|
HeaderInformation.SoftPackageReferencesListSize = MemAr.Tell() - Summary.SoftPackageReferencesOffset;
|
|
}
|
|
else
|
|
{
|
|
HeaderInformation.SoftPackageReferencesListSize = 0;
|
|
}
|
|
|
|
if (Summary.SearchableNamesOffset)
|
|
{
|
|
MemAr.Seek(Summary.SearchableNamesOffset);
|
|
FLinkerTables LinkerTables;
|
|
LinkerTables.SerializeSearchableNamesMap(MemAr);
|
|
SearchableNamesMap = MoveTemp(LinkerTables.SearchableNamesMap);
|
|
|
|
HeaderInformation.SearchableNamesMapSize = MemAr.Tell() - Summary.SearchableNamesOffset;
|
|
}
|
|
|
|
if (Summary.ThumbnailTableOffset)
|
|
{
|
|
MemAr.Seek(Summary.ThumbnailTableOffset);
|
|
|
|
int32 ThumbnailCount = 0;
|
|
MemAr << ThumbnailCount;
|
|
|
|
ThumbnailTable.Reserve(ThumbnailCount);
|
|
for (int32 Index = 0; Index < ThumbnailCount; ++Index)
|
|
{
|
|
FThumbnailEntry& Entry = ThumbnailTable.Emplace_GetRef();
|
|
MemAr << Entry.ObjectShortClassName;
|
|
MemAr << Entry.ObjectPathWithoutPackageName;
|
|
MemAr << Entry.FileOffset;
|
|
}
|
|
|
|
HeaderInformation.ThumbnailTableSize = MemAr.Tell() - Summary.ThumbnailTableOffset;
|
|
}
|
|
|
|
// Load AR data
|
|
if (Summary.AssetRegistryDataOffset)
|
|
{
|
|
MemAr.Seek(Summary.AssetRegistryDataOffset);
|
|
|
|
UE::AssetRegistry::EReadPackageDataMainErrorCode ErrorCode;
|
|
if (!AssetRegistryData.PkgData.DoSerialize(MemAr, Summary, ErrorCode))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to deserialize asset registry data for %s"), *SrcAsset);
|
|
return EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
|
|
AssetRegistryData.ObjectData.Reserve(AssetRegistryData.PkgData.ObjectCount);
|
|
for (int32 i = 0; i < AssetRegistryData.PkgData.ObjectCount; ++i)
|
|
{
|
|
FAssetRegistryObjectData& ObjData = AssetRegistryData.ObjectData.Emplace_GetRef();
|
|
if (!ObjData.ObjectData.DoSerialize(MemAr, ErrorCode))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to deserialize asset registry data for %s"), *SrcAsset);
|
|
return EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
|
|
ObjData.TagData.Reserve(ObjData.ObjectData.TagCount);
|
|
for (int32 j = 0; j < ObjData.ObjectData.TagCount; ++j)
|
|
{
|
|
if (!ObjData.TagData.Emplace_GetRef().DoSerialize(MemAr, ErrorCode))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to deserialize asset registry data for %s"), *SrcAsset);
|
|
return EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
}
|
|
}
|
|
|
|
AssetRegistryData.SectionSize = MemAr.Tell() - Summary.AssetRegistryDataOffset;
|
|
|
|
UE::AssetRegistry::FReadPackageDataDependenciesArgs DependenciesArgs;
|
|
DependenciesArgs.BinaryNameAwareArchive = &MemAr;
|
|
DependenciesArgs.AssetRegistryDependencyDataOffset = AssetRegistryData.PkgData.DependencyDataOffset;
|
|
DependenciesArgs.NumImports = Summary.ImportCount;
|
|
DependenciesArgs.NumSoftPackageReferences = Summary.SoftPackageReferencesCount;
|
|
DependenciesArgs.PackageVersion = Summary.GetFileVersionUE();
|
|
|
|
if (!UE::AssetRegistry::ReadPackageDataDependencies(DependenciesArgs))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to deserialize asset registry data for %s"), *SrcAsset);
|
|
return EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
|
|
if (DependenciesArgs.ImportUsedInGame.Num() != Summary.ImportCount ||
|
|
DependenciesArgs.SoftPackageUsedInGame.Num() != Summary.SoftPackageReferencesCount)
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error,
|
|
TEXT("Failed to deserialize asset registry data for %s. ReadPackageDataDependencies internal error: (%d != %d || %d != %d)."),
|
|
*SrcAsset, DependenciesArgs.ImportUsedInGame.Num(), Summary.ImportCount,
|
|
DependenciesArgs.SoftPackageUsedInGame.Num(), Summary.SoftPackageReferencesCount);
|
|
return EResult::ErrorFailedToDeserializeSourceAsset;
|
|
}
|
|
|
|
if ((AssetRegistryData.PkgData.DependencyDataOffset != INDEX_NONE) != (DependenciesArgs.AssetRegistryDependencyDataSize != 0))
|
|
{
|
|
// When writing the file, we use validity of Size to decide whether we write or skip the
|
|
// AssetRegistryDependencyData section, but in the earlier AssetRegistryData section we write the DependencyOffset
|
|
// based on whether the offset is valid. To prevent corruption, we need the validity of each to match.
|
|
UE_LOG(LogAssetHeaderPatcher, Error,
|
|
TEXT("Failed to deserialize asset registry data for %s. DependencyDataOffset (%" INT64_FMT ") != -1 does not match AssetRegistryDependencyDataSize (%" INT64_FMT ") != 0."),
|
|
*DstAsset, AssetRegistryData.PkgData.DependencyDataOffset, DependenciesArgs.AssetRegistryDependencyDataSize);
|
|
return EResult::ErrorFailedToOpenDestinationFile;
|
|
}
|
|
|
|
AssetRegistryData.DependencyDataSectionSize = DependenciesArgs.AssetRegistryDependencyDataSize;
|
|
AssetRegistryData.ImportIndexUsedInGame.Reserve(Summary.ImportCount);
|
|
for (int32 ImportIndex = 0; ImportIndex < Summary.ImportCount; ++ImportIndex)
|
|
{
|
|
AssetRegistryData.ImportIndexUsedInGame.Add(ImportIndex, DependenciesArgs.ImportUsedInGame[ImportIndex]);
|
|
}
|
|
check(SoftPackageReferencesTable.Num() == Summary.SoftPackageReferencesCount); // Constructed above
|
|
AssetRegistryData.SoftPackageReferenceUsedInGame.Reserve(Summary.SoftPackageReferencesCount);
|
|
for (int32 SoftPackageIndex = 0; SoftPackageIndex < Summary.SoftPackageReferencesCount; ++SoftPackageIndex)
|
|
{
|
|
AssetRegistryData.SoftPackageReferenceUsedInGame.Add(SoftPackageReferencesTable[SoftPackageIndex],
|
|
DependenciesArgs.SoftPackageUsedInGame[SoftPackageIndex]);
|
|
}
|
|
|
|
AssetRegistryData.ExtraPackageDependencies = MoveTemp(DependenciesArgs.ExtraPackageDependencies);
|
|
}
|
|
|
|
return EResult::Success;
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::ShouldReplaceMountPoint(const FStringView InPath, FStringView& OutSrcMountPoint, FStringView& OutDstMountPoint)
|
|
{
|
|
for (auto& MountPair : StringMountPointReplacements)
|
|
{
|
|
const FStringView SrcMount(MountPair.Key);
|
|
const FStringView DstMount(MountPair.Value);
|
|
|
|
if (InPath.StartsWith(SrcMount))
|
|
{
|
|
OutSrcMountPoint = SrcMount;
|
|
OutDstMountPoint = DstMount;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Note, like DoPatch(FName&) we should strive to remove this method in favour of one that understands
|
|
// the context for which this string belongs to. Patching it based on search and replace, is going to be
|
|
// error-prone and should be avoided.
|
|
bool FAssetHeaderPatcherInner::DoPatch(FString& InOutString)
|
|
{
|
|
// Attempt a direct replacement
|
|
{
|
|
// Find a Path, change a Path.
|
|
FStringView MaybeReplacement = Find(StringReplacements, InOutString);
|
|
if (!MaybeReplacement.IsEmpty())
|
|
{
|
|
InOutString = MaybeReplacement;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// Direct replacement failed so now try substring replacements
|
|
|
|
bool bDidPatch = false;
|
|
TStringBuilder<NAME_SIZE> DstStringBuilder;
|
|
{
|
|
// Patch Object paths with sub-object (not-necessarily quoted)
|
|
// Path occurs to the left of a ":"
|
|
int32 ColonPos;
|
|
FStringView PathView(InOutString);
|
|
while (PathView.FindChar(SUBOBJECT_DELIMITER_CHAR, ColonPos))
|
|
{
|
|
if ((ColonPos + 1) < PathView.Len() && PathView[ColonPos + 1] == SUBOBJECT_DELIMITER_CHAR)
|
|
{
|
|
// "::" is not a path delim
|
|
PathView.RightChopInline(ColonPos + 1);
|
|
continue;
|
|
}
|
|
|
|
// Presumably we have found the start of a path's sub-object path. Create a new
|
|
// view for our possible ObjectPath and walk backwards confirming we are in a path
|
|
// otherwise start over at the next ':'
|
|
FStringView ObjectPathView(PathView.GetData(), ColonPos);
|
|
|
|
int32 OuterDelimiterPos;
|
|
if (!ObjectPathView.FindLastChar(TEXT('.'), OuterDelimiterPos))
|
|
{
|
|
// A ':' but '.' before it is not an object path
|
|
PathView.RightChopInline(ColonPos + 1);
|
|
continue;
|
|
}
|
|
|
|
int32 LastPathDelimiterPos = INDEX_NONE;
|
|
int32 Index = OuterDelimiterPos;
|
|
while (--Index >= 0)
|
|
{
|
|
if (ObjectPathView[Index] == TEXT('/'))
|
|
{
|
|
LastPathDelimiterPos = Index;
|
|
}
|
|
else
|
|
{
|
|
// Confirm we are still in a path
|
|
int32 PosInvalidChar = 0;
|
|
if (InvalidObjectPathCharacters.FindChar(ObjectPathView[Index], PosInvalidChar))
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (LastPathDelimiterPos < 0)
|
|
{
|
|
// No '/' means we aren't in a path
|
|
PathView.RightChopInline(ColonPos + 1);
|
|
continue;
|
|
}
|
|
|
|
FStringView SrcMountPoint;
|
|
FStringView DstMountPoint;
|
|
FStringView ObjectPath(PathView.GetData() + LastPathDelimiterPos, ColonPos - LastPathDelimiterPos);
|
|
FStringView MaybeReplacement = Find(StringReplacements, ObjectPath);
|
|
if (!MaybeReplacement.IsEmpty())
|
|
{
|
|
FStringView LeftPart(*InOutString, int32(PathView.GetData() - *InOutString) + LastPathDelimiterPos);
|
|
FStringView RightPart(PathView.GetData() + ColonPos);
|
|
|
|
DstStringBuilder.Reset();
|
|
DstStringBuilder.Append(LeftPart);
|
|
DstStringBuilder.Append(MaybeReplacement);
|
|
DstStringBuilder.Append(RightPart);
|
|
|
|
InOutString = DstStringBuilder.ToString();
|
|
bDidPatch = true;
|
|
|
|
// Keep searching until the path is depleted since there might be more than one path to replace
|
|
PathView = FStringView(*InOutString + LeftPart.Len() + MaybeReplacement.Len() + 1);
|
|
}
|
|
else if (ShouldReplaceMountPoint(ObjectPath, SrcMountPoint, DstMountPoint))
|
|
{
|
|
FStringView LeftPart(*InOutString, int32(PathView.GetData() - *InOutString) + LastPathDelimiterPos);
|
|
FStringView RightPart(PathView.GetData() + LastPathDelimiterPos + SrcMountPoint.Len());
|
|
|
|
DstStringBuilder.Reset();
|
|
DstStringBuilder.Append(LeftPart);
|
|
DstStringBuilder.Append(DstMountPoint);
|
|
DstStringBuilder.Append(RightPart);
|
|
|
|
InOutString = DstStringBuilder.ToString();
|
|
bDidPatch = true;
|
|
|
|
// Keep searching until the path is depleted since there might be more than one path to replace
|
|
// Skip to the colon since we know we didn't have any matches within the quotes beyond the mount
|
|
PathView = FStringView(*InOutString + ColonPos + 1);
|
|
}
|
|
else
|
|
{
|
|
// No match but keep searching as there may be more than one ':'
|
|
PathView.RightChopInline(ColonPos + 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
{
|
|
// Patch quoted paths.
|
|
// Path occurs to the right of the first "'" or """
|
|
auto PatchQuotedPath = [this, &DstStringBuilder](FString& StringToPatch, FStringView Quote)
|
|
{
|
|
int32 FirstQuotePos = INDEX_NONE;
|
|
bool bFoundReplacement = false;
|
|
FStringView PathView(StringToPatch);
|
|
while ((FirstQuotePos = PathView.Find(Quote, 0, ESearchCase::CaseSensitive)) != INDEX_NONE)
|
|
{
|
|
int32 SecondQuotePos = PathView.Find(Quote, FirstQuotePos + 1, ESearchCase::CaseSensitive);
|
|
if (SecondQuotePos == INDEX_NONE)
|
|
{
|
|
// If there isn't a second quote we're done
|
|
break;
|
|
}
|
|
|
|
FStringView SrcMountPoint;
|
|
FStringView DstMountPoint;
|
|
FStringView StrippedQuotedPath = FStringView(PathView.GetData() + FirstQuotePos + 1, SecondQuotePos - FirstQuotePos - 1); // +1 and -1 are to skip the quotes
|
|
FStringView MaybeReplacement = Find(StringReplacements, StrippedQuotedPath);
|
|
if (!MaybeReplacement.IsEmpty())
|
|
{
|
|
FStringView LeftPart(*StringToPatch, int32(PathView.GetData() - *StringToPatch) + FirstQuotePos + 1); // +1 to ensure we include the quote
|
|
FStringView RightPart(PathView.GetData() + SecondQuotePos);
|
|
|
|
DstStringBuilder.Reset();
|
|
DstStringBuilder.Append(LeftPart);
|
|
DstStringBuilder.Append(MaybeReplacement);
|
|
DstStringBuilder.Append(RightPart);
|
|
|
|
StringToPatch = DstStringBuilder.ToString();
|
|
bFoundReplacement = true;
|
|
|
|
// Keep searching until the path is depleted since there might be more than one path to replace
|
|
PathView = FStringView(*StringToPatch + LeftPart.Len() + MaybeReplacement.Len() + 1);
|
|
}
|
|
else if (ShouldReplaceMountPoint(StrippedQuotedPath, SrcMountPoint, DstMountPoint))
|
|
{
|
|
FStringView LeftPart(*StringToPatch, int32(PathView.GetData() - *StringToPatch) + FirstQuotePos + 1); // +1 to ensure we include the quote
|
|
FStringView RightPart(PathView.GetData() + FirstQuotePos + SrcMountPoint.Len() + 1); // +1 to ensure we skip the first quote
|
|
|
|
DstStringBuilder.Reset();
|
|
DstStringBuilder.Append(LeftPart);
|
|
DstStringBuilder.Append(DstMountPoint);
|
|
DstStringBuilder.Append(RightPart);
|
|
|
|
StringToPatch = DstStringBuilder.ToString();
|
|
bFoundReplacement = true;
|
|
|
|
// Keep searching until the path is depleted since there might be more than one path to replace
|
|
// Skip to the end quote since we know we didn't have any matches within the quotes beyond the mount
|
|
PathView = FStringView(*StringToPatch + SecondQuotePos + 1 +(DstMountPoint.Len() - SrcMountPoint.Len())); // Dst - Src to account for new SecondQuotePos after replacement
|
|
}
|
|
else
|
|
{
|
|
// No match but keep searching as there may be more than one quoted path
|
|
PathView.RightChopInline(SecondQuotePos + 1);
|
|
}
|
|
}
|
|
return bFoundReplacement;
|
|
};
|
|
bDidPatch |= PatchQuotedPath(InOutString, TEXT("'"));
|
|
bDidPatch |= PatchQuotedPath(InOutString, TEXT("\""));
|
|
}
|
|
|
|
return bDidPatch;
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::AddFName(FName DstName)
|
|
{
|
|
if (DstName == NAME_None)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FNameEntryId DstComparisonId = DstName.GetDisplayIndex();
|
|
FNameEntryId* RemappedFName = RenameMap.Find(DstComparisonId);
|
|
if (RemappedFName)
|
|
{
|
|
// We hit a case where we thought we needed to change the name in the NameTable
|
|
// but have now discovered some part of the header needs to use the old name. In such a case
|
|
// remove the remap and make it an add instead.
|
|
AddedNames.Add(*RemappedFName);
|
|
RenameMap.Remove(DstComparisonId);
|
|
}
|
|
else
|
|
{
|
|
AddedNames.Add(DstComparisonId);
|
|
}
|
|
UnchangedNames.Add(DstComparisonId);
|
|
return true;
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::RemapFName(FName SrcName, FName DstName)
|
|
{
|
|
// None won't appear in the NameTable so skip any None passed in here
|
|
if (SrcName == NAME_None)
|
|
{
|
|
return false;
|
|
}
|
|
checkf(DstName != NAME_None, TEXT("There should never be a None FName in the NameTable"));
|
|
|
|
// NameTable entries only care about the comparison form (no number) so
|
|
// only consider that for remapping purposes
|
|
FNameEntryId SrcComparisonId = SrcName.GetDisplayIndex();
|
|
FNameEntryId DstComparisonId = DstName.GetDisplayIndex();
|
|
|
|
// Since we do fuzzy matching against AssetRegistryTag data, we might get SrcNames not in the FName table as input; ignore these.
|
|
if (!NameToIndexMap.Contains(SrcComparisonId))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Previously it was thought that any FName at the front of the NameTable is an FName used by export data and should not be patched
|
|
// since we can't know the context in which the user uses the FName. This is true, however it's quite common for PropertyTags and other
|
|
// common types to be "ExportData" that refers to paths that will be patched and must stay in sync. As such if we come across one of these
|
|
// paths when patching exports normally, we must replace it. We still do not do a generic walk of the FNameTable for patching without context
|
|
// as that will be error-prone however we allow patching the "ExportData" section of the NameTable when we know there is an overlap with
|
|
// header FNames and ExportData FNames.
|
|
constexpr bool bIsExportDataFName = false; // NameToIndexMap[SrcComparisonId] < Summary.NamesReferencedFromExportDataCount;
|
|
|
|
bool bNeedRemap = SrcComparisonId != DstComparisonId;
|
|
if (!bNeedRemap)
|
|
{
|
|
AddFName(SrcName);
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
FNameEntryId* RemappedFName = RenameMap.Find(SrcComparisonId);
|
|
|
|
bool bForceAddName = bIsExportDataFName || (RemappedFName && *RemappedFName != DstComparisonId) || UnchangedNames.Contains(SrcComparisonId);
|
|
if (bForceAddName)
|
|
{
|
|
// We already have a _different_ remapping, a header entry is still relying on the original name or this is a name used by the Export payload
|
|
// in which case we can't safely modify it. That is fine; we might have used the same FName in more than one place.
|
|
// We need to be certain we are renaming only the names we care about in context. If not, shared names in the NameTable might change
|
|
// incorrectly (e.g. A class FName may have matched a Package FName, thus sharing a NameTable entry, but after patching it's possible _only_
|
|
// the Package name has changed. In such a case we don't want to rename the class name inadvertently by patching the shared NameTable entry.
|
|
// If we have a mismatch with the new patched name, record the new name and we will append it to the NameTable later.
|
|
AddedNames.Add(DstComparisonId);
|
|
}
|
|
else
|
|
{
|
|
RenameMap.Add(SrcComparisonId, DstComparisonId);
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::DoPatch(FName& InOutName)
|
|
{
|
|
// If we are given an FName to patch we have no real context as to what that FName is
|
|
// so we conservatively assume it is a package path and attempt to patch that only
|
|
FCoreRedirectObjectName SrcPackageName(NAME_None, NAME_None, InOutName);
|
|
FCoreRedirectObjectName DstPackageName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, SrcPackageName, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
if (RemapFName(SrcPackageName.PackageName, DstPackageName.PackageName))
|
|
{
|
|
InOutName = DstPackageName.PackageName;
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void FAssetHeaderPatcherInner::GetExportTablePatches(TArray<FExportPatch>& OutExportPatches)
|
|
{
|
|
OutExportPatches.Reserve(ExportTable.Num());
|
|
for (int32 i = 0; i < ExportTable.Num(); ++i)
|
|
{
|
|
FObjectExport& Export = ExportTable[i];
|
|
FCoreRedirectObjectName SrcResourceName = GetFullObjectNameFromObjectResource(Export, true /*bIsExport*/);
|
|
FCoreRedirectObjectName DstResourceName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_AllMask, SrcResourceName, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
if (RemapFName(SrcResourceName.ObjectName, DstResourceName.ObjectName))
|
|
{
|
|
FExportPatch ExportPatch;
|
|
ExportPatch.TableIndex = i;
|
|
ExportPatch.ObjectName = DstResourceName.ObjectName;
|
|
OutExportPatches.Add(MoveTemp(ExportPatch));
|
|
}
|
|
}
|
|
}
|
|
|
|
static FName GetObjectResourceNameFromCoreRedirectObjectName(const FCoreRedirectObjectName& InName)
|
|
{
|
|
return !InName.ObjectName.IsNone() ? InName.ObjectName : InName.PackageName;
|
|
}
|
|
|
|
FAssetHeaderPatcher::EResult FAssetHeaderPatcherInner::GetImportTablePatches(TArray<FImportPatch>& OutImportPatches, int32& OutNewImportCount)
|
|
{
|
|
OutNewImportCount = 0;
|
|
OutImportPatches.Reserve(ImportTable.Num());
|
|
|
|
struct FPatchDataForImport
|
|
{
|
|
int32 PatchIndex = INDEX_NONE;
|
|
FCoreRedirectObjectName SrcImportPath;
|
|
FCoreRedirectObjectName DstImportPath;
|
|
bool bPatched = false;
|
|
bool bSkipImportTableWalkForRedirectedOuters = false;
|
|
};
|
|
TArray<FPatchDataForImport> ImportIndexToPatchData;
|
|
ImportIndexToPatchData.SetNum(ImportTable.Num());
|
|
|
|
for (int32 ImportIndex = 0; ImportIndex < ImportTable.Num(); ++ImportIndex)
|
|
{
|
|
FObjectImport& Import = ImportTable[ImportIndex];
|
|
FPatchDataForImport& PatchDataForImport = ImportIndexToPatchData[ImportIndex];
|
|
|
|
// For each FObjectResource, immediately patch the FNames that do not impact other imports or exports.
|
|
const FCoreRedirectObjectName SrcImportClass(Import.ClassName, NAME_None, Import.ClassPackage);
|
|
const FCoreRedirectObjectName DstImportClass = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Class | ECoreRedirectFlags::Type_Package, SrcImportClass, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
|
|
Import.ClassName = RemapFName(SrcImportClass.ObjectName, DstImportClass.ObjectName)
|
|
? DstImportClass.ObjectName : SrcImportClass.ObjectName;
|
|
|
|
Import.ClassPackage = RemapFName(SrcImportClass.PackageName, DstImportClass.PackageName)
|
|
? DstImportClass.PackageName : SrcImportClass.PackageName;
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
const FCoreRedirectObjectName SrcImportPackageName(NAME_None, NAME_None, Import.PackageName);
|
|
const FCoreRedirectObjectName DstImportPackageName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, SrcImportPackageName, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
Import.PackageName = RemapFName(SrcImportPackageName.PackageName, DstImportPackageName.PackageName)
|
|
? DstImportPackageName.PackageName : SrcImportPackageName.PackageName;
|
|
#endif
|
|
|
|
// Look up whether there is a specific redirect for the full path of the import.
|
|
PatchDataForImport.SrcImportPath = GetFullObjectNameFromObjectResource(Import, false /*bIsExport*/, false /*bWalkImportsOnly*/);
|
|
PatchDataForImport.DstImportPath = FCoreRedirects::GetRedirectedName(FCoreRedirects::GetFlagsForTypeName(Import.ClassPackage, Import.ClassName), PatchDataForImport.SrcImportPath, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
|
|
// Record the remapping of the FName in the FObjectResource (whether or not it was redirected)
|
|
FName SrcObjectResourceName = GetObjectResourceNameFromCoreRedirectObjectName(PatchDataForImport.SrcImportPath);
|
|
FName DstObjectResourceName = GetObjectResourceNameFromCoreRedirectObjectName(PatchDataForImport.DstImportPath);
|
|
RemapFName(SrcObjectResourceName, DstObjectResourceName);
|
|
|
|
// All objects within a redirected outer are moved along with that outer, but the CoreRedirects system does not find the redirect of the outer unless
|
|
// we look for it specifically. We handle this case in the loop below, after we have processed all imports looking for redirects specific to them.
|
|
PatchDataForImport.bPatched = PatchDataForImport.SrcImportPath != PatchDataForImport.DstImportPath;
|
|
if (PatchDataForImport.bPatched)
|
|
{
|
|
// If our outer was patched (i.e. we had a direct match) or there is no outer but our package changed, ensure we don't use the ImportTable to
|
|
// check for redirected outers that should apply to this Import as we may incorrectly change an OuterName/PackageName back to the SrcName
|
|
// if that outer name is still in use after redirection. (e.g. an object moved out of a package to a new package but both packages are still valid imports)
|
|
PatchDataForImport.bSkipImportTableWalkForRedirectedOuters = (PatchDataForImport.SrcImportPath.OuterName != PatchDataForImport.DstImportPath.OuterName) ||
|
|
(PatchDataForImport.DstImportPath.OuterName.IsNone() && PatchDataForImport.SrcImportPath.PackageName != PatchDataForImport.DstImportPath.PackageName);
|
|
PatchDataForImport.PatchIndex = OutImportPatches.Num();
|
|
|
|
FImportPatch& ImportPatch = OutImportPatches.Emplace_GetRef();
|
|
ImportPatch.ClassName = Import.ClassName;
|
|
ImportPatch.ClassPackage = Import.ClassPackage;
|
|
#if WITH_EDITORONLY_DATA
|
|
ImportPatch.PackageName = Import.PackageName;
|
|
#endif
|
|
bool* UsedInGame = AssetRegistryData.ImportIndexUsedInGame.Find(ImportIndex);
|
|
ImportPatch.bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
ImportPatch.TableIndex = ImportIndex;
|
|
ImportPatch.ObjectName = DstObjectResourceName;
|
|
ImportPatch.OuterIndex = Import.OuterIndex;
|
|
}
|
|
}
|
|
|
|
// Now that redirects specifically for each import have been detected, go through the list of imports again and assign
|
|
// DstImportPath based on their (possibly redirected) outer.
|
|
for (int32 ImportIndex = 0; ImportIndex < ImportTable.Num(); ++ImportIndex)
|
|
{
|
|
FObjectImport& Import = ImportTable[ImportIndex];
|
|
FPatchDataForImport& PatchDataForImport = ImportIndexToPatchData[ImportIndex];
|
|
|
|
if (!PatchDataForImport.bSkipImportTableWalkForRedirectedOuters)
|
|
{
|
|
// Recursively evaluate all outers of the import, and then evaluate the import
|
|
TArray<int32, TInlineAllocator<10>> ImportsToEvaluate;
|
|
ImportsToEvaluate.Add(ImportIndex);
|
|
for (FPackageIndex OuterIndex = Import.OuterIndex; !OuterIndex.IsNull(); )
|
|
{
|
|
if (OuterIndex.IsExport())
|
|
{
|
|
// We don't have data on the export, so stop.
|
|
// TODO: This is incorrect, we need to get the remapped destination from the export to know
|
|
// the correct destination for the import.
|
|
break;
|
|
}
|
|
check(OuterIndex.IsImport());
|
|
int32 OuterImportIndex = OuterIndex.ToImport();
|
|
FObjectImport& OuterImport = ImportTable[OuterImportIndex];
|
|
FPatchDataForImport& PatchDataForOuter = ImportIndexToPatchData[OuterImportIndex];
|
|
if (PatchDataForOuter.bSkipImportTableWalkForRedirectedOuters)
|
|
{
|
|
break;
|
|
}
|
|
ImportsToEvaluate.Add(OuterImportIndex);
|
|
OuterIndex = OuterImport.OuterIndex;
|
|
}
|
|
|
|
while (!ImportsToEvaluate.IsEmpty())
|
|
{
|
|
int32 ImportIndexToEvaluate = ImportsToEvaluate.Pop(EAllowShrinking::No);
|
|
FObjectImport& ImportToEvaluate = ImportTable[ImportIndexToEvaluate];
|
|
FPatchDataForImport& PatchDataToEvaluate = ImportIndexToPatchData[ImportIndexToEvaluate];
|
|
if (PatchDataToEvaluate.bSkipImportTableWalkForRedirectedOuters)
|
|
{
|
|
// Shouldn't be possible, but somehow we already evaluated this, cycle in the outer chain?
|
|
continue;
|
|
}
|
|
|
|
if (ImportToEvaluate.OuterIndex.IsNull())
|
|
{
|
|
// This import has no outer, it is a package. We did not find a redirect specifically for it,
|
|
// and it has no outer to move along with, so it has no redirect. Mark it skipped and do nothing.
|
|
PatchDataToEvaluate.bSkipImportTableWalkForRedirectedOuters = true;
|
|
}
|
|
else if (ImportToEvaluate.OuterIndex.IsExport())
|
|
{
|
|
// We don't have data on the export, so we cannot read the transformed path for the import under it.
|
|
// TODO: This is incorrect, we need to get the remapped destination from the export to know
|
|
// the correct destination for the import.
|
|
PatchDataToEvaluate.bSkipImportTableWalkForRedirectedOuters = true;
|
|
}
|
|
else
|
|
{
|
|
int32 OuterImportIndex = ImportToEvaluate.OuterIndex.ToImport();
|
|
FPatchDataForImport& PatchDataForOuter = ImportIndexToPatchData[OuterImportIndex];
|
|
PatchDataToEvaluate.DstImportPath = FCoreRedirectObjectName::AppendObjectName(
|
|
PatchDataForOuter.DstImportPath, PatchDataToEvaluate.DstImportPath.ObjectName);
|
|
PatchDataToEvaluate.bSkipImportTableWalkForRedirectedOuters = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
check(PatchDataForImport.bSkipImportTableWalkForRedirectedOuters);
|
|
|
|
// Record that the (possibly redirected) full path of the destination of this import is in this import index
|
|
ImportNameToImportTableIndexLookup.Add(PatchDataForImport.DstImportPath, ImportIndex);
|
|
ImportTablePatchedNames.Add({ PatchDataForImport.SrcImportPath, PatchDataForImport.DstImportPath });
|
|
}
|
|
|
|
// Loop over all imports, and update their outerindex. If an import for their outer does not already exist, add it on to the end.
|
|
// Keep iterating over the added elements that we add on to the end until we don't add any more.
|
|
for (int32 ImportIndex = 0;
|
|
ImportIndex < ImportIndexToPatchData.Num(); // Num can change during the loop
|
|
++ImportIndex)
|
|
{
|
|
FCoreRedirectObjectName ImportPath = ImportIndexToPatchData[ImportIndex].DstImportPath;
|
|
// Do not create a pointer into ImportIndexToPatchData after we have done any necessary adds into ImportIndexToPatchData
|
|
FPatchDataForImport* PatchDataForImport = nullptr;
|
|
|
|
FPackageIndex DstOuterIndex;
|
|
if (ImportPath.ObjectName.IsNone())
|
|
{
|
|
// A package, no outer
|
|
DstOuterIndex = FPackageIndex();
|
|
}
|
|
else
|
|
{
|
|
FCoreRedirectObjectName OuterPath = FCoreRedirectObjectName::GetParent(ImportPath);
|
|
if (OuterPath.PackageName == this->DstPackagePath)
|
|
{
|
|
// The outer of the import is an export (this happens when the import is an external actor package,
|
|
// and the external actor is a child of a ULevel in this map package).
|
|
// Keep the old OuterIndex. TODO: Read the correct package path from a map that we construct from remapped export name to export index,
|
|
// so that we can handle replacing the outerindex for new outer imports
|
|
PatchDataForImport = &ImportIndexToPatchData[ImportIndex];
|
|
if (PatchDataForImport->bPatched)
|
|
{
|
|
DstOuterIndex = OutImportPatches[PatchDataForImport->PatchIndex].OuterIndex;
|
|
}
|
|
else
|
|
{
|
|
// The only unpatched imports have to be ones from the ImportTable of the pretransformed package
|
|
check(ImportIndex < ImportTable.Num());
|
|
DstOuterIndex = ImportTable[ImportIndex].OuterIndex;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// The outer of an import is an import, and we need to find or add it in our importtable
|
|
int32& ExistingOuterIndex = ImportNameToImportTableIndexLookup.FindOrAdd(OuterPath, INDEX_NONE);
|
|
if (ExistingOuterIndex == INDEX_NONE)
|
|
{
|
|
// If the outer is not already in the list of imports, add on another FImportPatch to hold the outer
|
|
int32 OuterPatchIndex = OutImportPatches.Num();
|
|
FImportPatch& OuterPatch = OutImportPatches.Emplace_GetRef();
|
|
int32 OuterImportIndex = ImportIndexToPatchData.Num();
|
|
FPatchDataForImport& OuterPatchData = ImportIndexToPatchData.Emplace_GetRef();
|
|
OuterPatchData.bPatched = true;
|
|
OuterPatchData.DstImportPath = OuterPath;
|
|
OuterPatchData.PatchIndex = OuterPatchIndex;
|
|
ExistingOuterIndex = OuterImportIndex;
|
|
|
|
if (OuterPath.ObjectName.IsNone())
|
|
{
|
|
// Outer is a package
|
|
OuterPatch.ClassName = FName(TEXT("Package"));
|
|
OuterPatch.ClassPackage = FName(TEXT("/Script/CoreUObject"));
|
|
}
|
|
else
|
|
{
|
|
// Outer is an object inside a package.
|
|
// We don't know the class name and package of the outer, so set it to /Script/CoreUObject.Object.
|
|
// TODO: Is there anyway to find this out? A better guess is to copy it from the previous outer.
|
|
OuterPatch.ClassName = FName(TEXT("Object"));
|
|
OuterPatch.ClassPackage = FName(TEXT("/Script/CoreUObject"));
|
|
}
|
|
AddFName(OuterPatch.ClassName);
|
|
AddFName(OuterPatch.ClassPackage);
|
|
#if WITH_EDITORONLY_DATA
|
|
// We don't have any information about whether the outer object has an external package. For now we
|
|
// just don't support that case for redirects, and assume the outer is not in an external package.
|
|
OuterPatch.PackageName = NAME_None;
|
|
#endif
|
|
// Set the Outer to UsedInGame if the import that has it as the outer is UsedInGame.
|
|
PatchDataForImport = &ImportIndexToPatchData[ImportIndex];
|
|
if (PatchDataForImport->bPatched)
|
|
{
|
|
OuterPatch.bUsedInGame = OutImportPatches[PatchDataForImport->PatchIndex].bUsedInGame;
|
|
}
|
|
else
|
|
{
|
|
bool* UsedInGame = AssetRegistryData.ImportIndexUsedInGame.Find(ImportIndex);
|
|
OuterPatch.bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
}
|
|
OuterPatch.TableIndex = OuterImportIndex;
|
|
OuterPatch.ObjectName = GetObjectResourceNameFromCoreRedirectObjectName(OuterPath);
|
|
AddFName(OuterPatch.ObjectName);
|
|
// OuterIndex of the outer is not yet known. Set to null. If the outer is a package this is correct,
|
|
// otherwise it is incorrect and we have to overwrite it later when we reach the outer in our iteration over
|
|
// ImportIndexToPatchData.
|
|
OuterPatch.OuterIndex = FPackageIndex();
|
|
}
|
|
DstOuterIndex = FPackageIndex::FromImport(ExistingOuterIndex);
|
|
}
|
|
}
|
|
|
|
// If we have already patched, just assign the outer index.
|
|
PatchDataForImport = &ImportIndexToPatchData[ImportIndex];
|
|
if (PatchDataForImport->bPatched)
|
|
{
|
|
OutImportPatches[PatchDataForImport->PatchIndex].OuterIndex = DstOuterIndex;
|
|
}
|
|
else
|
|
{
|
|
// The only unpatched imports have to be ones from the ImportTable of the pretransformed package
|
|
check(ImportIndex < ImportTable.Num());
|
|
FObjectImport& Import = ImportTable[ImportIndex];
|
|
|
|
// If the outer has changed and we have not already patched, turn this into a patch.
|
|
if (Import.OuterIndex != DstOuterIndex)
|
|
{
|
|
PatchDataForImport->bPatched = true;
|
|
PatchDataForImport->PatchIndex = OutImportPatches.Num();
|
|
FImportPatch& ImportPatch = OutImportPatches.Emplace_GetRef();
|
|
ImportPatch.ClassName = Import.ClassName;
|
|
ImportPatch.ClassPackage = Import.ClassPackage;
|
|
#if WITH_EDITORONLY_DATA
|
|
ImportPatch.PackageName = Import.PackageName;
|
|
#endif
|
|
bool* UsedInGame = AssetRegistryData.ImportIndexUsedInGame.Find(ImportIndex);
|
|
ImportPatch.bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
ImportPatch.TableIndex = ImportIndex;
|
|
ImportPatch.ObjectName = Import.ObjectName;
|
|
ImportPatch.OuterIndex = DstOuterIndex;
|
|
}
|
|
}
|
|
}
|
|
|
|
OutNewImportCount = ImportIndexToPatchData.Num() - ImportTable.Num();
|
|
return FAssetHeaderPatcher::EResult::Success;
|
|
}
|
|
|
|
void FAssetHeaderPatcherInner::PatchExportAndImportTables(const TArray<FExportPatch>& InExportPatches, const TArray<FImportPatch>& InImportPatches, const int32 InNewImportCount)
|
|
{
|
|
// Any FPackageIndex that IsExport() will not change since we do not add or remove entries from the ExportTable.
|
|
// For Imports we may have changed import names such that we resulted in new imports and now imports that are no longer
|
|
// in use and should be removed (to avoid unnecessary overhead when loading). However, as far as I can tell we can't
|
|
// determine what to remove based on what is used since we append extra imports during serialization that don't have any
|
|
// Exports or Imports referring to them (e.g. CDO subobjects are like this). As such we append our import patches as new imports
|
|
// if we can't stomp over existing entries in the table. We then fixup any FPackageIndex members that might be using old indices that we
|
|
// know we have explicitly changed due to additions.
|
|
|
|
ImportTable.SetNum(ImportTable.Num() + InNewImportCount);
|
|
for (const FImportPatch& ImportPatch : InImportPatches)
|
|
{
|
|
const int32 Index = ImportPatch.TableIndex;
|
|
check(Index < ImportTable.Num());
|
|
|
|
FObjectImport& Import = ImportTable[Index];
|
|
Import.ObjectName = ImportPatch.ObjectName;
|
|
Import.OuterIndex = ImportPatch.OuterIndex;
|
|
Import.ClassName = ImportPatch.ClassName;
|
|
Import.ClassPackage = ImportPatch.ClassPackage;
|
|
#if WITH_EDITORONLY_DATA
|
|
Import.OldClassName = NAME_None;
|
|
Import.PackageName = ImportPatch.PackageName;
|
|
#endif
|
|
AssetRegistryData.ImportIndexUsedInGame.Add(Index, ImportPatch.bUsedInGame);
|
|
}
|
|
|
|
for (const FExportPatch& ExportPatch : InExportPatches)
|
|
{
|
|
// Patching cannot result in new Exports
|
|
const int32 Index = ExportPatch.TableIndex;
|
|
check(Index < ExportTable.Num());
|
|
|
|
FObjectExport& Export = ExportTable[Index];
|
|
Export.ObjectName = ExportPatch.ObjectName;
|
|
#if WITH_EDITORONLY_DATA
|
|
Export.OldClassName = NAME_None;
|
|
#endif
|
|
}
|
|
|
|
// Walk through our Exports and ensure any FPackageIndex they have is pointing to the correct location
|
|
auto RemapIndex = [this](FPackageIndex& Index)
|
|
{
|
|
if (Index.IsImport())
|
|
{
|
|
FCoreRedirectObjectName PatchedName = ImportTablePatchedNames[Index.ToImport()].Value;
|
|
Index = FPackageIndex::FromImport(ImportNameToImportTableIndexLookup[PatchedName]);
|
|
}
|
|
};
|
|
|
|
for (FObjectExport& Export : ExportTable)
|
|
{
|
|
RemapIndex(Export.ClassIndex);
|
|
RemapIndex(Export.SuperIndex);
|
|
RemapIndex(Export.TemplateIndex);
|
|
RemapIndex(Export.OuterIndex);
|
|
}
|
|
|
|
// We may have added new Imports so ensure the Summary is accurate
|
|
Summary.ImportCount = ImportTable.Num();
|
|
}
|
|
|
|
void FAssetHeaderPatcherInner::PatchNameTable()
|
|
{
|
|
// Note, no number is assigned when replacing FNames as the NameTable only tracks unnumbered names
|
|
|
|
// Update the NameTable with the known patched values and add our new patched names to the NameToIndex
|
|
// map so we can validate that we always have a FName mapping to an entry in the name table when writing
|
|
for (auto& Pair : RenameMap)
|
|
{
|
|
FNameEntryId SrcName = Pair.Key;
|
|
FNameEntryId DstName = Pair.Value;
|
|
int32* pSrcIndex = NameToIndexMap.Find(SrcName);
|
|
checkf(pSrcIndex && *pSrcIndex < NameTable.Num(), TEXT("An FName remapping was done for a name (%s) not in the NameTable."),
|
|
*FName::CreateFromDisplayId(DstName, NAME_NO_NUMBER_INTERNAL).ToString());
|
|
int32 SrcIndex = *pSrcIndex;
|
|
|
|
NameTable[SrcIndex] = FName::CreateFromDisplayId(DstName, NAME_NO_NUMBER_INTERNAL);
|
|
NameToIndexMap.Remove(SrcName);
|
|
NameToIndexMap.Add(DstName, SrcIndex);
|
|
}
|
|
|
|
for (FNameEntryId NewName : AddedNames)
|
|
{
|
|
if (NameToIndexMap.Contains(NewName))
|
|
{
|
|
// Definition of an AddedName is that an original name was remapped to a name, and we couldn't remove the
|
|
// original name for one of several reasons, so we want to add the remapped name to the nametable.
|
|
// But it is possible that remapped name already exists, so we don't need to add it.
|
|
continue;
|
|
}
|
|
FName NewFName = FName::CreateFromDisplayId(NewName, NAME_NO_NUMBER_INTERNAL);
|
|
int32 NameTableIndex = NameTable.Num();
|
|
|
|
NameTable.Add(NewFName);
|
|
NameToIndexMap.Add(NewName, NameTableIndex);
|
|
}
|
|
|
|
Summary.NameCount = NameTable.Num();
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::DoPatch(FSoftObjectPath& InOutSoft)
|
|
{
|
|
// FSoftObjectPaths are special in that while we may have an explict remapping
|
|
// provided to the patcher, there may also be redirects stored in the global GRedirectCollector
|
|
// reflecting on-disk object redirectors.
|
|
// Redirectors will be applied during FSoftObjectPath serialization so we need to account for
|
|
// that when patching so the nametable can be updated appropriately.
|
|
|
|
// Honour explictly remapped paths first. Even if this succeeds we need to handle redirectors
|
|
// because FSoftObjectPath serialization will always apply them.
|
|
|
|
// Special care is needed to check to see if a patch is done via remapping or via redirects before we assume the
|
|
// name should be marked as unchanging (causing any future patching to the name to be an add in the NameTable). If we have a redirect
|
|
// but not remapping, the failed remapping could marked the srcname as unchanging erroneously.
|
|
FTopLevelAssetPath SrcTopLevelAssetPath = InOutSoft.GetAssetPath();
|
|
const FCoreRedirectObjectName DstTopLevelAssetPathObjectName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_AllMask, SrcTopLevelAssetPath, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
FTopLevelAssetPath DstTopLevelAssetPath(DstTopLevelAssetPathObjectName.ToString());
|
|
bool bPatched = SrcTopLevelAssetPath != DstTopLevelAssetPath;
|
|
if (bPatched)
|
|
{
|
|
InOutSoft.SetPath(DstTopLevelAssetPath, InOutSoft.GetSubPathString());
|
|
}
|
|
|
|
// Only run in Editor builds as FSoftObjectPath only runs PreSavePath during SerializePath in editor builds
|
|
#if WITH_EDITOR
|
|
if (InOutSoft.PreSavePath(nullptr))
|
|
{
|
|
bPatched = true;
|
|
DstTopLevelAssetPath = InOutSoft.GetAssetPath();
|
|
}
|
|
#endif
|
|
|
|
if (bPatched)
|
|
{
|
|
RemapFName(SrcTopLevelAssetPath.GetAssetName(), DstTopLevelAssetPath.GetAssetName());
|
|
RemapFName(SrcTopLevelAssetPath.GetPackageName(), DstTopLevelAssetPath.GetPackageName());
|
|
}
|
|
|
|
return bPatched;
|
|
}
|
|
|
|
FCoreRedirectObjectName FAssetHeaderPatcherInner::GetFullObjectNameFromObjectResource(const FObjectResource& InResource, bool bIsExport, bool bWalkImportsOnly)
|
|
{
|
|
bool bOutermostIsExport = bIsExport;
|
|
FPackageIndex OuterIndex = InResource.OuterIndex;
|
|
TArray<FName, TInlineAllocator<8>> OuterStack;
|
|
while (!OuterIndex.IsNull())
|
|
{
|
|
const FObjectResource* OuterResource;
|
|
if (OuterIndex.IsImport())
|
|
{
|
|
bOutermostIsExport = false;
|
|
OuterResource = &ImportTable[OuterIndex.ToImport()];
|
|
}
|
|
else if (bWalkImportsOnly)
|
|
{
|
|
break;
|
|
}
|
|
else
|
|
{
|
|
bOutermostIsExport = true;
|
|
OuterResource = &ExportTable[OuterIndex.ToExport()];
|
|
}
|
|
|
|
OuterStack.Push(OuterResource->ObjectName);
|
|
OuterIndex = OuterResource->OuterIndex;
|
|
}
|
|
|
|
FName SrcObjectName;
|
|
FName SrcOuterName;
|
|
FName SrcPackageName;
|
|
bool bRemapByPackage = false;
|
|
if (OuterStack.Num() == 0)
|
|
{
|
|
if (bOutermostIsExport)
|
|
{
|
|
SrcPackageName = OriginalPackagePath; // /Package/Package
|
|
SrcOuterName = NAME_None;
|
|
SrcObjectName = InResource.ObjectName; // MyObject
|
|
}
|
|
else
|
|
{
|
|
// The ObjectName is a package
|
|
SrcPackageName = InResource.ObjectName; // /Package/Package
|
|
SrcOuterName = NAME_None;
|
|
SrcObjectName = NAME_None;
|
|
bRemapByPackage = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
SrcPackageName = bOutermostIsExport ? OriginalPackagePath : OuterStack.Pop();
|
|
|
|
TStringBuilder<NAME_SIZE> OuterString;
|
|
while (!OuterStack.IsEmpty())
|
|
{
|
|
FName Outer = OuterStack.Pop();
|
|
Outer.AppendString(OuterString);
|
|
OuterString.AppendChar(TEXT('.'));
|
|
}
|
|
if (OuterString.Len())
|
|
{
|
|
OuterString.RemoveSuffix(1);
|
|
}
|
|
SrcOuterName = FName(OuterString);
|
|
SrcObjectName = InResource.ObjectName;
|
|
}
|
|
|
|
return FCoreRedirectObjectName(SrcObjectName, SrcOuterName, SrcPackageName);
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::DoPatch(FTopLevelAssetPath& InOutPath)
|
|
{
|
|
const FCoreRedirectObjectName SrcTopLevelAssetPath = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_AllMask, InOutPath, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
const FTopLevelAssetPath DstTopLevelAssetPath(SrcTopLevelAssetPath.ToString());
|
|
|
|
bool bPatched = RemapFName(InOutPath.GetAssetName(), DstTopLevelAssetPath.GetAssetName());
|
|
bPatched |= RemapFName(InOutPath.GetPackageName(), DstTopLevelAssetPath.GetPackageName());
|
|
|
|
if (bPatched)
|
|
{
|
|
InOutPath = DstTopLevelAssetPath;
|
|
}
|
|
|
|
return bPatched;
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::DoPatch(FGatherableTextData& InOutGatherableTextData)
|
|
{
|
|
// There are various fields in FGatherableTextData however only one pertains to
|
|
// asset paths and types, SourceSiteContexts.SiteDescription. The rest are contextual
|
|
// key-value pairs of text which are not references to assets/types and thus do not need patching
|
|
// (at least we can't understand the context a priori to know if specialized code
|
|
// may try to load from these strings)
|
|
|
|
bool bDidPatch = false;
|
|
for (FTextSourceSiteContext& SourceSiteContext : InOutGatherableTextData.SourceSiteContexts)
|
|
{
|
|
FStringView ClassName;
|
|
FStringView PackagePath;
|
|
FStringView ObjectName;
|
|
FStringView SubObjectName;
|
|
FPackageName::SplitFullObjectPath(SourceSiteContext.SiteDescription, ClassName, PackagePath, ObjectName, SubObjectName, true /*bDetectClassName*/);
|
|
|
|
// Todo to use StringView logic above to reduce string copies
|
|
FSoftObjectPath SiteDescriptionPath(SourceSiteContext.SiteDescription);
|
|
if (!SiteDescriptionPath.IsValid())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FTopLevelAssetPath TopLevelAssetPath = SiteDescriptionPath.GetAssetPath();
|
|
const FCoreRedirectObjectName RedirectedTopLevelAssetPath = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_AllMask, TopLevelAssetPath, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
const FTopLevelAssetPath PatchedTopLevelAssetPath(RedirectedTopLevelAssetPath.ToString());
|
|
if (TopLevelAssetPath == PatchedTopLevelAssetPath)
|
|
{
|
|
continue;
|
|
}
|
|
bDidPatch = true;
|
|
SiteDescriptionPath.SetPath(PatchedTopLevelAssetPath, SiteDescriptionPath.GetSubPathString());
|
|
SourceSiteContext.SiteDescription = SiteDescriptionPath.ToString();
|
|
}
|
|
|
|
return bDidPatch;
|
|
}
|
|
|
|
bool FAssetHeaderPatcherInner::DoPatch(FThumbnailEntry& InThumbnailEntry)
|
|
{
|
|
// These objects can potentially be paths to sub-objects. For renaming purposes we
|
|
// want to drop the sub-object path and grab the AssetName
|
|
FStringView SrcObjectPathWithoutPackageName(InThumbnailEntry.ObjectPathWithoutPackageName);
|
|
int32 ColonPos = INDEX_NONE;
|
|
if (SrcObjectPathWithoutPackageName.FindChar(TEXT(':'), ColonPos))
|
|
{
|
|
SrcObjectPathWithoutPackageName.LeftChopInline(SrcObjectPathWithoutPackageName.Len() - ColonPos);
|
|
}
|
|
|
|
FName PackageFName = OriginalPackagePath;
|
|
if (bIsNonOneFilePerActorPackage)
|
|
{
|
|
PackageFName = OriginalNonOneFilePerActorPackagePath;
|
|
}
|
|
|
|
const FCoreRedirectObjectName SrcTopLevelAssetName(FName(SrcObjectPathWithoutPackageName), NAME_None, PackageFName);
|
|
const FCoreRedirectObjectName RedirectedTopLevelAssetName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Object, SrcTopLevelAssetName, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
bool bPatched = RemapFName(SrcTopLevelAssetName.ObjectName, RedirectedTopLevelAssetName.ObjectName);
|
|
|
|
const FCoreRedirectObjectName SrcClassName(FName(InThumbnailEntry.ObjectShortClassName), NAME_None, NAME_None);
|
|
const FCoreRedirectObjectName RedirectedClassName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Class, SrcClassName, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
bPatched |= RemapFName(SrcClassName.ObjectName, RedirectedClassName.ObjectName);
|
|
|
|
if (bPatched)
|
|
{
|
|
InThumbnailEntry.ObjectShortClassName = RedirectedClassName.ObjectName.ToString();
|
|
InThumbnailEntry.ObjectPathWithoutPackageName = RedirectedTopLevelAssetName.ObjectName.ToString();
|
|
}
|
|
|
|
return bPatched;
|
|
}
|
|
|
|
FAssetHeaderPatcher::EResult FAssetHeaderPatcherInner::PatchHeader_PatchSections()
|
|
{
|
|
// Package Summary
|
|
{
|
|
const FCoreRedirectObjectName DstPackageName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(NAME_None, NAME_None, OriginalPackagePath), ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
|
|
// This is a string, so we do not want to Remap the patched name unless it's a non-OFPA
|
|
// package, in which case there will be an FName entry for this path
|
|
Summary.PackageName = DstPackageName.PackageName.ToString();
|
|
DstPackagePath = DstPackageName.PackageName;
|
|
|
|
// It seems that non-OFPA packages tend to have the package name in the nametable,
|
|
// however it isn't a guarantee, so we confirm the name is there before remapping and
|
|
// extend this special case of NameTable patching to all packages, OFPA or not.
|
|
if (NameToIndexMap.Find(OriginalPackagePath.GetDisplayIndex()))
|
|
{
|
|
bIsPackagePathInNametable = true;
|
|
RemapFName(OriginalPackagePath, DstPackageName.PackageName);
|
|
}
|
|
}
|
|
|
|
// For Import and Export Tables we need to generate patches for both and apply them afterwards. This is due to the ObjectPaths
|
|
// being patched being split across multiple Export and Import entries that refer to one another. Patching them as we go would
|
|
// change the ObjectPaths of Exports/Imports we may need to patch but won't be able to deduce the original, unpatched ObjectPath.
|
|
TArray<FExportPatch> ExportPatches;
|
|
int32 NewImportCount = 0;
|
|
TArray<FImportPatch> ImportPatches;
|
|
GetExportTablePatches(ExportPatches);
|
|
FAssetHeaderPatcher::EResult Result = GetImportTablePatches(ImportPatches, NewImportCount);
|
|
if (Result != FAssetHeaderPatcher::EResult::Success)
|
|
{
|
|
return Result;
|
|
}
|
|
PatchExportAndImportTables(ExportPatches, ImportPatches, NewImportCount);
|
|
|
|
// Soft paths
|
|
for (FSoftObjectPath& SoftObjectPath : SoftObjectPathTable)
|
|
{
|
|
DoPatch(SoftObjectPath);
|
|
}
|
|
|
|
// GatherableTextData table
|
|
for (FGatherableTextData& GatherableTextData : GatherableTextDataTable)
|
|
{
|
|
DoPatch(GatherableTextData);
|
|
}
|
|
|
|
// Soft Package References
|
|
for (FName& Reference : SoftPackageReferencesTable)
|
|
{
|
|
FCoreRedirectObjectName SrcPackagePath(NAME_None, NAME_None, Reference);
|
|
FCoreRedirectObjectName DstPackageName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, SrcPackagePath, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
bool* UsedInGame = AssetRegistryData.SoftPackageReferenceUsedInGame.Find(SrcPackagePath.PackageName);
|
|
bool bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
AssetRegistryData.SoftPackageReferenceUsedInGame.Add(DstPackageName.PackageName, bUsedInGame);
|
|
if (RemapFName(SrcPackagePath.PackageName, DstPackageName.PackageName))
|
|
{
|
|
Reference = DstPackageName.PackageName;
|
|
}
|
|
}
|
|
|
|
// SearchableNamesMap
|
|
for (auto& Pair : SearchableNamesMap)
|
|
{
|
|
TArray<FName>& Names = Pair.Value;
|
|
for (FName& Name : Names)
|
|
{
|
|
DoPatch(Name);
|
|
}
|
|
}
|
|
|
|
// Thumbnail Table
|
|
for (FThumbnailEntry& ThumbnailEntry : ThumbnailTable)
|
|
{
|
|
DoPatch(ThumbnailEntry);
|
|
}
|
|
|
|
// Asset Registry Data
|
|
for (FAssetRegistryObjectData& ObjData : AssetRegistryData.ObjectData)
|
|
{
|
|
// ObjectPath is a toss-up.
|
|
// Sometimes it's a FTopLevelAssetPath with an implied PackageName (this package's name) and AssetName.
|
|
// Sometimes it's a full FSoftPath (e.g. when dealing with ExternalObjects)
|
|
FSoftObjectPath SrcObjectPath(ObjData.ObjectData.ObjectPath);
|
|
{
|
|
if (SrcObjectPath.IsValid())
|
|
{
|
|
FSoftObjectPath SrdDstObjectPath = SrcObjectPath;
|
|
|
|
if (DoPatch(SrdDstObjectPath))
|
|
{
|
|
ObjData.ObjectData.ObjectPath = SrdDstObjectPath.ToString();
|
|
}
|
|
}
|
|
// Only use OriginalPackagePath if it was in the nametable to begin with,
|
|
// since if it wasn't we can't have a remapping for the ObjectPath
|
|
else if(bIsPackagePathInNametable)
|
|
{
|
|
FTopLevelAssetPath SrcDstTopLevelAssetPath(OriginalPackagePath, FName(ObjData.ObjectData.ObjectPath));
|
|
SrcObjectPath.SetPath(SrcDstTopLevelAssetPath, SrcObjectPath.GetSubPathString());
|
|
|
|
if (DoPatch(SrcDstTopLevelAssetPath))
|
|
{
|
|
ObjData.ObjectData.ObjectPath = SrcDstTopLevelAssetPath.GetAssetName().ToString();
|
|
}
|
|
}
|
|
}
|
|
|
|
// ObjData.ObjectData.ObjectClassName is a FTopLevelAssetPath stored as a string
|
|
FTopLevelAssetPath SrcObjectClassName(ObjData.ObjectData.ObjectClassName);
|
|
{
|
|
FTopLevelAssetPath SrcDstObjectClassName = SrcObjectClassName;
|
|
|
|
if (DoPatch(SrcDstObjectClassName))
|
|
{
|
|
ObjData.ObjectData.ObjectClassName = SrcDstObjectClassName.ToString();
|
|
}
|
|
}
|
|
|
|
for (UE::AssetRegistry::FDeserializeTagData& TagData : ObjData.TagData)
|
|
{
|
|
if (IgnoredTags.Contains(TagData.Key))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// WorldPartitionActor metadata is special. It's an encoded string blob which needs
|
|
// handling internally, so we make use of a custom patcher to let us intercept
|
|
// various elements that might need patching.
|
|
if (TagData.Key == FWorldPartitionActorDescUtils::ActorMetaDataTagName())
|
|
{
|
|
const FString LongPackageName(SrcAsset);
|
|
const FString ObjectPath(ObjData.ObjectData.ObjectPath);
|
|
const FTopLevelAssetPath AssetClassPathName(ObjData.ObjectData.ObjectClassName);
|
|
const FAssetDataTagMap Tags(MakeTagMap(ObjData.TagData));
|
|
const FAssetData AssetData(LongPackageName, ObjectPath, AssetClassPathName, Tags);
|
|
|
|
struct FWorldPartitionAssetDataPatcherInner : FWorldPartitionAssetDataPatcher
|
|
{
|
|
FWorldPartitionAssetDataPatcherInner(FAssetHeaderPatcherInner* InInner) : Inner(InInner) {}
|
|
virtual bool DoPatch(FString& InOutString) override
|
|
{
|
|
return Inner->DoPatch(InOutString);
|
|
}
|
|
virtual bool DoPatch(FName& InOutName) override
|
|
{
|
|
// FNames are actually strings inside WorldPartitionActor metadata, and since a lone
|
|
// FName has no context for how to patch it, convert it to a string to perform a
|
|
// best-effort search.
|
|
FString NameString;
|
|
InOutName.ToString(NameString);
|
|
if (Inner->DoPatch(NameString))
|
|
{
|
|
InOutName = FName(NameString);
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
virtual bool DoPatch(FSoftObjectPath& InOutSoft) override
|
|
{
|
|
return Inner->DoPatch(InOutSoft);
|
|
}
|
|
virtual bool DoPatch(FTopLevelAssetPath& InOutPath) override
|
|
{
|
|
return Inner->DoPatch(InOutPath);
|
|
}
|
|
FAssetHeaderPatcherInner* Inner;
|
|
};
|
|
|
|
FString PatchedAssetData;
|
|
FWorldPartitionAssetDataPatcherInner Patcher(this);
|
|
if (FWorldPartitionActorDescUtils::GetPatchedAssetDataFromAssetData(AssetData, PatchedAssetData, &Patcher))
|
|
{
|
|
TagData.Value = PatchedAssetData;
|
|
}
|
|
}
|
|
// Special case for common Tag
|
|
else if (bPatchPrimaryAssetTag && TagData.Key == TEXT("PrimaryAssetName"))
|
|
{
|
|
if (TagData.Value == OriginalPrimaryAssetName)
|
|
{
|
|
const FCoreRedirectObjectName DstPackageName = FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package,
|
|
FCoreRedirectObjectName(NAME_None, NAME_None, OriginalPackagePath), ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
|
|
TStringBuilder<256> Builder;
|
|
DstPackageName.PackageName.ToString(Builder);
|
|
FStringView PrimaryAssetView = Builder.ToView();
|
|
ensure(PrimaryAssetView.Len() && PrimaryAssetView[0] == TEXT('/'));
|
|
PrimaryAssetView.RemovePrefix(1);
|
|
|
|
int32 SlashPos = INDEX_NONE;
|
|
if (PrimaryAssetView.FindChar(TEXT('/'), SlashPos))
|
|
{
|
|
TagData.Value.Empty();
|
|
TagData.Value.Append(PrimaryAssetView.GetData(), SlashPos);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
DoPatch(TagData.Value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// AssetRegistryDependencyData
|
|
{
|
|
using namespace UE::AssetRegistry;
|
|
|
|
// Move into a TMap to avoid O(n^2) insertions/removals
|
|
TMap<FName, EExtraDependencyFlags> Dependencies;
|
|
Dependencies.Reserve(AssetRegistryData.ExtraPackageDependencies.Num());
|
|
for (const TPair<FName, EExtraDependencyFlags>& Pair : AssetRegistryData.ExtraPackageDependencies)
|
|
{
|
|
Dependencies.Add(Pair.Key, Pair.Value);
|
|
}
|
|
|
|
TArray<TPair<FName, EExtraDependencyFlags>> AddedKeys;
|
|
TSet<FName> RemovedKeys;
|
|
for (const TPair<FName, EExtraDependencyFlags>& Pair : Dependencies)
|
|
{
|
|
FCoreRedirectObjectName SrcPackagePath(NAME_None, NAME_None, Pair.Key);
|
|
FCoreRedirectObjectName DstPackageName =
|
|
FCoreRedirects::GetRedirectedName(ECoreRedirectFlags::Type_Package, SrcPackagePath, ECoreRedirectMatchFlags::DisallowPartialLHSMatch);
|
|
if (RemapFName(SrcPackagePath.PackageName, DstPackageName.PackageName))
|
|
{
|
|
AddedKeys.Add({ DstPackageName.PackageName, Pair.Value});
|
|
RemovedKeys.Add(SrcPackagePath.PackageName);
|
|
}
|
|
}
|
|
if (!AddedKeys.IsEmpty() || !RemovedKeys.IsEmpty())
|
|
{
|
|
for (TPair<FName, EExtraDependencyFlags>& Pair : AddedKeys)
|
|
{
|
|
// If the added name already existed, take the union of the old and new values
|
|
EExtraDependencyFlags& Existing = Dependencies.FindOrAdd(Pair.Key, EExtraDependencyFlags::None);
|
|
Existing |= Pair.Value;
|
|
|
|
// If an added key adds back a key that was removed, then no longer remove the key
|
|
RemovedKeys.Remove(Pair.Key);
|
|
}
|
|
for (FName RemoveKey : RemovedKeys)
|
|
{
|
|
Dependencies.Remove(RemoveKey);
|
|
}
|
|
|
|
// Return the remapped values to the array and restore sortedness
|
|
AssetRegistryData.ExtraPackageDependencies = Dependencies.Array();
|
|
Algo::Sort(AssetRegistryData.ExtraPackageDependencies,
|
|
[](const TPair<FName, EExtraDependencyFlags>& A, const TPair<FName, EExtraDependencyFlags>& B)
|
|
{
|
|
return A.Key.LexicalLess(B.Key);
|
|
});
|
|
}
|
|
}
|
|
|
|
// Do nametable patching last since we want to ensure we have determined all the remappings necessary
|
|
PatchNameTable();
|
|
|
|
return FAssetHeaderPatcher::EResult::Success;
|
|
}
|
|
|
|
FAssetHeaderPatcher::EResult FAssetHeaderPatcherInner::PatchHeader_WriteDestinationFile()
|
|
{
|
|
// Serialize modified sections and reconstruct the file
|
|
// Original offsets and sizes of any sections that will be patched
|
|
// Tag Offset Size bRequired
|
|
const FSectionData SourceSections[] = {
|
|
{ EPatchedSection::Summary, 0, HeaderInformation.SummarySize, true },
|
|
{ EPatchedSection::NameTable, Summary.NameOffset, HeaderInformation.NameTableSize, true },
|
|
{ EPatchedSection::SoftPathTable, Summary.SoftObjectPathsOffset, HeaderInformation.SoftObjectPathListSize, false },
|
|
{ EPatchedSection::GatherableTextDataTable, Summary.GatherableTextDataOffset, HeaderInformation.GatherableTextDataSize, false },
|
|
{ EPatchedSection::ImportTable, Summary.ImportOffset, HeaderInformation.ImportTableSize, true },
|
|
{ EPatchedSection::ExportTable, Summary.ExportOffset, HeaderInformation.ExportTableSize, true },
|
|
{ EPatchedSection::SoftPackageReferencesTable, Summary.SoftPackageReferencesOffset, HeaderInformation.SoftPackageReferencesListSize, false },
|
|
{ EPatchedSection::SearchableNamesMap, Summary.SearchableNamesOffset, HeaderInformation.SearchableNamesMapSize, false },
|
|
{ EPatchedSection::ThumbnailTable, Summary.ThumbnailTableOffset, HeaderInformation.ThumbnailTableSize, false },
|
|
{ EPatchedSection::AssetRegistryData, Summary.AssetRegistryDataOffset, AssetRegistryData.SectionSize, true },
|
|
{ EPatchedSection::AssetRegistryDependencyData, AssetRegistryData.PkgData.DependencyDataOffset, AssetRegistryData.DependencyDataSectionSize, false },
|
|
};
|
|
|
|
const int32 SourceTotalHeaderSize = Summary.TotalHeaderSize;
|
|
|
|
// Ensure the sections are in the expected order.
|
|
for (int32 SectionIdx = 1; SectionIdx < UE_ARRAY_COUNT(SourceSections); ++SectionIdx)
|
|
{
|
|
const FSectionData& SourceSection = SourceSections[SectionIdx];
|
|
const FSectionData& PrevSection = SourceSections[SectionIdx - 1];
|
|
|
|
// Verify sections are ordered as expected
|
|
if (SourceSection.Offset < 0 || (SourceSection.bRequired && (SourceSection.Offset < PrevSection.Offset)))
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Unexpected section order for %s (%d %" INT64_FMT " < %" INT64_FMT ") "),
|
|
*SrcAsset, SectionIdx, SourceSection.Offset, PrevSection.Offset);
|
|
return EResult::ErrorUnexpectedSectionOrder;
|
|
}
|
|
}
|
|
|
|
// Ensure the required sections have data
|
|
for (int32 SectionIdx = 0; SectionIdx < UE_ARRAY_COUNT(SourceSections); ++SectionIdx)
|
|
{
|
|
const FSectionData& SourceSection = SourceSections[SectionIdx];
|
|
if (SourceSection.bRequired && SourceSection.Size <= 0)
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Unexpected missing required section for %s: %d is required but has zero size."),
|
|
*SrcAsset, SectionIdx);
|
|
return EResult::ErrorEmptyRequireSection;
|
|
}
|
|
}
|
|
|
|
// Create the destination file if not open already
|
|
if (!DstArchive)
|
|
{
|
|
TUniquePtr<FArchive> FileWriter(IFileManager::Get().CreateFileWriter(*DstAsset, FILEWRITE_EvenIfReadOnly));
|
|
if (!FileWriter)
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to open %s for write"), *DstAsset);
|
|
return EResult::ErrorFailedToOpenDestinationFile;
|
|
}
|
|
DstArchiveOwner = MoveTemp(FileWriter);
|
|
DstArchive = DstArchiveOwner.Get();
|
|
}
|
|
|
|
FNamePatchingWriter Writer(*DstArchive, NameToIndexMap);
|
|
|
|
// set version numbers so components branch correctly
|
|
Writer.SetUEVer(Summary.GetFileVersionUE());
|
|
Writer.SetLicenseeUEVer(Summary.GetFileVersionLicenseeUE());
|
|
Writer.SetEngineVer(Summary.SavedByEngineVersion);
|
|
Writer.SetCustomVersions(Summary.GetCustomVersionContainer());
|
|
if (Summary.GetPackageFlags() & PKG_FilterEditorOnly)
|
|
{
|
|
Writer.SetFilterEditorOnly(true);
|
|
}
|
|
|
|
int64 LastSectionEndedAt = 0;
|
|
|
|
for (int32 SectionIdx = 0; SectionIdx < UE_ARRAY_COUNT(SourceSections); ++SectionIdx)
|
|
{
|
|
const FSectionData& SourceSection = SourceSections[SectionIdx];
|
|
|
|
// skip processing empty non required chunks.
|
|
if (!SourceSection.bRequired && SourceSection.Size <= 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// copy the blob from the end of the last section, to the start of this one
|
|
if (LastSectionEndedAt)
|
|
{
|
|
int64 SizeToCopy = SourceSection.Offset - LastSectionEndedAt;
|
|
checkf(SizeToCopy >= 0, TEXT("Section %d of %s\n%" INT64_FMT " -> %" INT64_FMT" %" INT64_FMT),
|
|
SectionIdx, *SrcAsset, SourceSection.Offset, LastSectionEndedAt, SizeToCopy);
|
|
Writer.Serialize(SrcBuffer.GetData() + LastSectionEndedAt, SizeToCopy);
|
|
}
|
|
LastSectionEndedAt = SourceSection.Offset + SourceSection.Size;
|
|
|
|
// Serialize the current patched section and patch summary offsets
|
|
switch (SourceSection.Section)
|
|
{
|
|
case EPatchedSection::Summary:
|
|
{
|
|
// We will write the Summary twice.
|
|
// The first is so we get its new size (if the name was changed in patching)
|
|
// The second is done after the loop, to patch up all the offsets.
|
|
check(Writer.Tell() == 0);
|
|
Writer << Summary;
|
|
const int64 SummarySize = Writer.Tell();
|
|
const int64 Delta = SummarySize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, 0, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::NameTable:
|
|
{
|
|
const int64 NameTableStartOffset = Writer.Tell();
|
|
for (FName& Name : NameTable)
|
|
{
|
|
const FNameEntry* Entry = FName::GetEntry(Name.GetDisplayIndex());
|
|
check(Entry);
|
|
Entry->Write(Writer);
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 NameTableSize = Writer.Tell() - NameTableStartOffset;
|
|
const int64 Delta = NameTableSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, NameTableStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
check(Summary.NameCount == NameTable.Num());
|
|
check(Summary.NameOffset == NameTableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::SoftPathTable:
|
|
{
|
|
const int64 TableStartOffset = Writer.Tell();
|
|
for (FSoftObjectPath& PathRef : SoftObjectPathTable)
|
|
{
|
|
PathRef.SerializePath(Writer);
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 TableSize = Writer.Tell() - TableStartOffset;
|
|
const int64 Delta = TableSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, TableStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
check(Summary.SoftObjectPathsCount == SoftObjectPathTable.Num());
|
|
check(Summary.SoftObjectPathsOffset == TableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::GatherableTextDataTable:
|
|
{
|
|
const int64 GatherableTableStartOffset = Writer.Tell();
|
|
for (FGatherableTextData& GatherableTextData : GatherableTextDataTable)
|
|
{
|
|
Writer << GatherableTextData;
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 TableSize = Writer.Tell() - GatherableTableStartOffset;
|
|
const int64 Delta = TableSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, GatherableTableStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
check(Summary.GatherableTextDataCount == GatherableTextDataTable.Num());
|
|
check(Summary.GatherableTextDataOffset == GatherableTableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::SearchableNamesMap:
|
|
{
|
|
const int64 TableStartOffset = Writer.Tell();
|
|
|
|
FLinkerTables LinkerTables;
|
|
LinkerTables.SearchableNamesMap = SearchableNamesMap;
|
|
LinkerTables.SerializeSearchableNamesMap(Writer);
|
|
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 TableSize = Writer.Tell() - TableStartOffset;
|
|
const int64 Delta = TableSize - SourceSection.Size;
|
|
checkf(Delta == 0, TEXT("Delta should be Zero. is %d"), (int)Delta);
|
|
check(Summary.SearchableNamesOffset == TableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::ImportTable:
|
|
{
|
|
const int64 ImportTableStartOffset = Writer.Tell();
|
|
for (FObjectImport& Import : ImportTable)
|
|
{
|
|
Writer << Import;
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 ImportTableSize = Writer.Tell() - ImportTableStartOffset;
|
|
const int64 Delta = ImportTableSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, ImportTableStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
checkf(Summary.ImportCount == ImportTable.Num(), TEXT("%d == %d"), Summary.ImportCount, ImportTable.Num());
|
|
checkf(Summary.ImportOffset == ImportTableStartOffset, TEXT("%d == %" INT64_FMT), Summary.ImportOffset, ImportTableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::ExportTable:
|
|
{
|
|
// The export table offsets aren't correct yet.
|
|
// Once we know them, we will seek back and write it a second time.
|
|
const int64 ExportTableStartOffset = Writer.Tell();
|
|
for (FObjectExport& Export : ExportTable)
|
|
{
|
|
Writer << Export;
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 ExportTableSize = Writer.Tell() - ExportTableStartOffset;
|
|
const int64 Delta = ExportTableSize - SourceSection.Size;
|
|
check(Delta == 0);
|
|
checkf(ExportTableSize == SourceSection.Size, TEXT("%d == %d"), (int)ExportTableSize, (int)SourceSection.Size); // We only patch export table offsets, we should not be patching size
|
|
checkf(Summary.ExportCount == ExportTable.Num(), TEXT("%d == %d"), Summary.ExportCount, ExportTable.Num());
|
|
checkf(Summary.ExportOffset == ExportTableStartOffset, TEXT("%d == %" INT64_FMT), Summary.ExportOffset, ExportTableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::SoftPackageReferencesTable:
|
|
{
|
|
const int64 TableStartOffset = Writer.Tell();
|
|
for (FName& Reference : SoftPackageReferencesTable)
|
|
{
|
|
Writer << Reference;
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 TableSize = Writer.Tell() - TableStartOffset;
|
|
const int64 Delta = TableSize - SourceSection.Size;
|
|
checkf(Delta == 0, TEXT("Delta should be Zero. is %d"), (int)Delta);
|
|
check(Summary.SoftPackageReferencesCount == SoftPackageReferencesTable.Num());
|
|
check(Summary.SoftPackageReferencesOffset == TableStartOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::ThumbnailTable:
|
|
{
|
|
const int64 ThumbnailTableStartOffset = Writer.Tell();
|
|
const int64 ThumbnailTableDeltaOffset = ThumbnailTableStartOffset - SourceSection.Offset;
|
|
int32 ThumbnailCount = ThumbnailTable.Num();
|
|
Writer << ThumbnailCount;
|
|
for (FThumbnailEntry& Entry : ThumbnailTable)
|
|
{
|
|
Writer << Entry.ObjectShortClassName;
|
|
Writer << Entry.ObjectPathWithoutPackageName;
|
|
Entry.FileOffset += (int32)ThumbnailTableDeltaOffset;
|
|
Writer << Entry.FileOffset;
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 ThumbnailTableSize = Writer.Tell() - ThumbnailTableStartOffset;
|
|
const int64 Delta = ThumbnailTableSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, ThumbnailTableStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
checkf(ThumbnailTableStartOffset == Summary.ThumbnailTableOffset, TEXT("%" INT64_FMT " == %" INT64_FMT),
|
|
ThumbnailTableStartOffset, Summary.ThumbnailTableOffset);
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::AssetRegistryData:
|
|
{
|
|
const int64 AssetRegistryDataStartOffset = Writer.Tell();
|
|
checkf(AssetRegistryDataStartOffset == Summary.AssetRegistryDataOffset, TEXT("%" INT64_FMT " == %" INT64_FMT),
|
|
AssetRegistryDataStartOffset, Summary.AssetRegistryDataOffset);
|
|
|
|
// Code to write this section copied and modified from UE::AssetRegistry::WritePackageData.
|
|
// TODO: Factor this into a public function in SavePackageUtilities and call that.
|
|
|
|
if (AssetRegistryData.PkgData.DependencyDataOffset != INDEX_NONE)
|
|
{
|
|
// This field is conditionally written depending on package version and cookedness. If it is written it
|
|
// is guaranteed to != INDEX_NONE. We are not changing those properties for the package, so we write
|
|
// the field if and only if we found it present in the package originally.
|
|
Writer << AssetRegistryData.PkgData.DependencyDataOffset;
|
|
}
|
|
Writer << AssetRegistryData.PkgData.ObjectCount;
|
|
|
|
check(AssetRegistryData.PkgData.ObjectCount == AssetRegistryData.ObjectData.Num());
|
|
for (FAssetRegistryObjectData& ObjData : AssetRegistryData.ObjectData)
|
|
{
|
|
Writer << ObjData.ObjectData.ObjectPath;
|
|
Writer << ObjData.ObjectData.ObjectClassName;
|
|
Writer << ObjData.ObjectData.TagCount;
|
|
|
|
check(ObjData.ObjectData.TagCount == ObjData.TagData.Num());
|
|
for (UE::AssetRegistry::FDeserializeTagData& TagData : ObjData.TagData)
|
|
{
|
|
Writer << TagData.Key;
|
|
Writer << TagData.Value;
|
|
}
|
|
}
|
|
checkf(!Writer.IsCriticalError(), TEXT("Issue writing %s"), *Writer.GetErrorMessage());
|
|
|
|
const int64 AssetRegistryDataSize = Writer.Tell() - AssetRegistryDataStartOffset;
|
|
const int64 Delta = AssetRegistryDataSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, AssetRegistryDataStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
|
|
break;
|
|
}
|
|
|
|
case EPatchedSection::AssetRegistryDependencyData:
|
|
{
|
|
int64 AssetRegistryDependencyDataStartOffset = Writer.Tell();
|
|
|
|
// Re-write the offset of this section into AssetRegistryData.PkgData.DependencyDataOffset in the
|
|
// earlier EPatchedSection::AssetRegistryData section.
|
|
Writer.Seek(Summary.AssetRegistryDataOffset);
|
|
Writer << AssetRegistryDependencyDataStartOffset;
|
|
if (Writer.IsError())
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to write to %s"), *DstAsset);
|
|
return EResult::ErrorFailedToWriteToDestinationFile;
|
|
}
|
|
Writer.Seek(AssetRegistryDependencyDataStartOffset);
|
|
AssetRegistryData.PkgData.DependencyDataOffset = AssetRegistryDependencyDataStartOffset;
|
|
|
|
// Code to write this section copied and modified from UE::AssetRegistry::WritePackageData.
|
|
// TODO: Factor this into a public function in SavePackageUtilities and call that.
|
|
|
|
TBitArray<> ImportUsedInGameBits;
|
|
TBitArray<> SoftPackageUsedInGameBits;
|
|
ImportUsedInGameBits.Reserve(ImportTable.Num());
|
|
for (int32 ImportIndex = 0; ImportIndex < ImportTable.Num(); ++ImportIndex)
|
|
{
|
|
bool* UsedInGame = AssetRegistryData.ImportIndexUsedInGame.Find(ImportIndex);
|
|
bool bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
ImportUsedInGameBits.Add(bUsedInGame);
|
|
}
|
|
SoftPackageUsedInGameBits.Reserve(SoftPackageReferencesTable.Num());
|
|
for (int32 SoftPackageIndex = 0; SoftPackageIndex < SoftPackageReferencesTable.Num(); ++SoftPackageIndex)
|
|
{
|
|
bool* UsedInGame = AssetRegistryData.SoftPackageReferenceUsedInGame.Find(SoftPackageReferencesTable[SoftPackageIndex]);
|
|
bool bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
SoftPackageUsedInGameBits.Add(bUsedInGame);
|
|
}
|
|
|
|
Writer << ImportUsedInGameBits;
|
|
Writer << SoftPackageUsedInGameBits;
|
|
|
|
TArray<TPair<FName, uint32>> ExtraPackageDependenciesInts;
|
|
ExtraPackageDependenciesInts.Reserve(AssetRegistryData.ExtraPackageDependencies.Num());
|
|
for (const TPair<FName, UE::AssetRegistry::EExtraDependencyFlags>& DependencyPair : AssetRegistryData.ExtraPackageDependencies)
|
|
{
|
|
ExtraPackageDependenciesInts.Add({ DependencyPair.Key, static_cast<uint32>(DependencyPair.Value) });
|
|
}
|
|
Writer << ExtraPackageDependenciesInts;
|
|
|
|
const int64 AssetRegistryDependencyDataSize = Writer.Tell() - AssetRegistryDependencyDataStartOffset;
|
|
const int64 Delta = AssetRegistryDependencyDataSize - SourceSection.Size;
|
|
PatchSummaryOffsets(Summary, AssetRegistryDependencyDataStartOffset, Delta);
|
|
Summary.TotalHeaderSize += (int32)Delta;
|
|
|
|
break;
|
|
}
|
|
|
|
default:
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Unexpected section for %s"), *SrcAsset);
|
|
return EResult::ErrorUnkownSection;
|
|
}
|
|
|
|
if (Writer.IsError())
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to write to %s"), *DstAsset);
|
|
return EResult::ErrorFailedToWriteToDestinationFile;
|
|
}
|
|
}
|
|
|
|
{ // copy the last blob
|
|
int64 SizeToCopy = SrcBuffer.Num() - LastSectionEndedAt;
|
|
checkf(SizeToCopy >= 0, TEXT("Section last of %s\n%" INT64_FMT " -> %" INT64_FMT " %" INT64_FMT),
|
|
*SrcAsset, SrcBuffer.Num(), LastSectionEndedAt, SizeToCopy);
|
|
Writer.Serialize(SrcBuffer.GetData() + LastSectionEndedAt, SizeToCopy);
|
|
}
|
|
|
|
if (Writer.IsError())
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to write to %s"), *DstAsset);
|
|
return EResult::ErrorFailedToWriteToDestinationFile;
|
|
}
|
|
|
|
// Re-write summary with patched offsets
|
|
Writer.Seek(0);
|
|
Writer << Summary;
|
|
|
|
{
|
|
// Re-write export table with patched offsets
|
|
// Patch Export table offsets now that we have patched all the header sections
|
|
Writer.Seek(Summary.ExportOffset);
|
|
const int64 ExportOffsetDelta = static_cast<int64>(Summary.TotalHeaderSize) - SourceTotalHeaderSize;
|
|
for (FObjectExport& Export : ExportTable)
|
|
{
|
|
Export.SerialOffset += ExportOffsetDelta;
|
|
Writer << Export;
|
|
}
|
|
}
|
|
|
|
if (Writer.IsError())
|
|
{
|
|
UE_LOG(LogAssetHeaderPatcher, Error, TEXT("Failed to write to %s"), *DstAsset);
|
|
return EResult::ErrorFailedToWriteToDestinationFile;
|
|
}
|
|
|
|
return EResult::Success;
|
|
}
|
|
|
|
void FAssetHeaderPatcherInner::DumpState(FStringView OutputDirectory)
|
|
{
|
|
TStringBuilder<1024> Builder;
|
|
auto GetDebugFNameString = [this](FName Name)
|
|
{
|
|
int32* Index = NameToIndexMap.Find(Name.GetDisplayIndex());
|
|
if (Index)
|
|
{
|
|
FName NameTableName = NameTable[*Index];
|
|
return FString::Printf(TEXT("%s (nametable index: %d, fname:{'%s', %d})"), *NameTableName.ToString(), *Index, *NameTableName.GetPlainNameString(), NameTableName.GetNumber());
|
|
}
|
|
else
|
|
{
|
|
return FString(TEXT("None (nametable index: -1, fname {'None', 0})"));
|
|
}
|
|
};
|
|
|
|
Builder.Append(TEXT("{\n"));
|
|
|
|
Builder.Append(TEXT("\t\"Summary\":{ "));
|
|
{
|
|
Builder.Appendf(TEXT("\n\t\t\"PackageName\": \"%s\""), *Summary.PackageName);
|
|
Builder.Appendf(TEXT(",\n\t\t\"NamesReferencedFromExportDataCount\": \"%d\""), Summary.NamesReferencedFromExportDataCount);
|
|
Builder.Appendf(TEXT(",\n\t\t\"ExportCount\": \"%d\""), Summary.ExportCount);
|
|
Builder.Appendf(TEXT(",\n\t\t\"ImportCount\": \"%d\""), Summary.ImportCount);
|
|
}
|
|
Builder.Append(TEXT("\n\t},\n"));
|
|
|
|
Builder.Append(TEXT("\t\"NameTable\":[ "));
|
|
for (const FName& Name : NameTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t\""));
|
|
Builder.Append(GetDebugFNameString(Name));
|
|
Builder.Append(TEXT("\","));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"ExportTable\":[ "));
|
|
int ExportIndex = 0;
|
|
for (const auto& Export : ExportTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t{\n"));
|
|
|
|
Builder.Appendf(TEXT("\t\t\t\"Index\": \"%d\""), ExportIndex++);
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ObjectName\": \""));
|
|
Builder.Append(GetDebugFNameString(Export.ObjectName));
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"Outer\": \""));
|
|
if (Export.OuterIndex.IsNull())
|
|
{
|
|
Builder.Append(TEXT("None"));
|
|
}
|
|
else if (Export.OuterIndex.IsExport())
|
|
{
|
|
Builder.Appendf(TEXT("Export(%d) - %s"), Export.OuterIndex.ToExport(), *GetDebugFNameString(ExportTable[Export.OuterIndex.ToExport()].ObjectName));
|
|
}
|
|
else if (Export.OuterIndex.IsImport())
|
|
{
|
|
Builder.Appendf(TEXT("Import(%d) - %s"), Export.OuterIndex.ToImport(), *GetDebugFNameString(ImportTable[Export.OuterIndex.ToImport()].ObjectName));
|
|
}
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ClassIndex\": \""));
|
|
if (Export.ClassIndex.IsNull())
|
|
{
|
|
Builder.Append(TEXT("None"));
|
|
}
|
|
else if (Export.ClassIndex.IsExport())
|
|
{
|
|
Builder.Appendf(TEXT("Export(%d) - %s"), Export.ClassIndex.ToExport(), *GetDebugFNameString(ExportTable[Export.ClassIndex.ToExport()].ObjectName));
|
|
}
|
|
else if (Export.ClassIndex.IsImport())
|
|
{
|
|
Builder.Appendf(TEXT("Import(%d) - %s"), Export.ClassIndex.ToImport(), *GetDebugFNameString(ImportTable[Export.ClassIndex.ToImport()].ObjectName));
|
|
}
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"SuperIndex\": \""));
|
|
if (Export.SuperIndex.IsNull())
|
|
{
|
|
Builder.Append(TEXT("None"));
|
|
}
|
|
else if (Export.SuperIndex.IsExport())
|
|
{
|
|
Builder.Appendf(TEXT("Export(%d) - %s"), Export.SuperIndex.ToExport(), *GetDebugFNameString(ExportTable[Export.SuperIndex.ToExport()].ObjectName));
|
|
}
|
|
else if (Export.SuperIndex.IsImport())
|
|
{
|
|
Builder.Appendf(TEXT("Import(%d) - %s"), Export.SuperIndex.ToImport(), *GetDebugFNameString(ImportTable[Export.SuperIndex.ToImport()].ObjectName));
|
|
}
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"TemplateIndex\": \""));
|
|
if (Export.TemplateIndex.IsNull())
|
|
{
|
|
Builder.Append(TEXT("None"));
|
|
}
|
|
else if (Export.TemplateIndex.IsExport())
|
|
{
|
|
Builder.Appendf(TEXT("Export(%d) - %s"), Export.TemplateIndex.ToExport(), *GetDebugFNameString(ExportTable[Export.TemplateIndex.ToExport()].ObjectName));
|
|
}
|
|
else if (Export.TemplateIndex.IsImport())
|
|
{
|
|
Builder.Appendf(TEXT("Import(%d) - %s"), Export.TemplateIndex.ToImport(), *GetDebugFNameString(ImportTable[Export.TemplateIndex.ToImport()].ObjectName));
|
|
}
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
Builder.Append(TEXT("\t\t\t\"OldClassName\": \""));
|
|
Builder.Append(GetDebugFNameString(Export.OldClassName));
|
|
Builder.Append(TEXT("\""));
|
|
#endif
|
|
Builder.Append(TEXT("\n\t\t},"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"ImportTable\":[ "));
|
|
int ImportIndex = 0;
|
|
for (const auto& Import : ImportTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t{\n"));
|
|
|
|
Builder.Appendf(TEXT("\t\t\t\"Index\": \"%d\""), ImportIndex++);
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ObjectName\": \""));
|
|
Builder.Append(GetDebugFNameString(Import.ObjectName));
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"Outer\": \""));
|
|
if (Import.OuterIndex.IsNull())
|
|
{
|
|
Builder.Append(TEXT("None"));
|
|
}
|
|
else if (Import.OuterIndex.IsExport())
|
|
{
|
|
Builder.Appendf(TEXT("Export(%d) - %s"), Import.OuterIndex.ToExport(), *GetDebugFNameString(ExportTable[Import.OuterIndex.ToExport()].ObjectName));
|
|
}
|
|
else if (Import.OuterIndex.IsImport())
|
|
{
|
|
Builder.Appendf(TEXT("Import(%d) - %s"), Import.OuterIndex.ToImport(), *GetDebugFNameString(ImportTable[Import.OuterIndex.ToImport()].ObjectName));
|
|
}
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
Builder.Append(TEXT("\t\t\t\"OldClassName\": \""));
|
|
Builder.Append(GetDebugFNameString(Import.OldClassName));
|
|
Builder.Append(TEXT("\",\n"));
|
|
#endif
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ClassPackage\": \""));
|
|
Builder.Append(GetDebugFNameString(Import.ClassPackage));
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ClassName\": \""));
|
|
Builder.Append(GetDebugFNameString(Import.ClassName));
|
|
Builder.Append(TEXT("\""));
|
|
|
|
#if WITH_EDITORONLY_DATA
|
|
Builder.Append(TEXT(",\n\t\t\t\"PackageName\": \""));
|
|
Builder.Append(GetDebugFNameString(Import.PackageName));
|
|
Builder.Append(TEXT("\""));
|
|
#endif
|
|
Builder.Append(TEXT("\n\t\t},"));
|
|
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"SoftObjectPathTable\":[ "));
|
|
for (const FSoftObjectPath& SoftObjectPath : SoftObjectPathTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t{\n"));
|
|
|
|
FTopLevelAssetPath TLAP = SoftObjectPath.GetAssetPath();
|
|
FString Subpath = SoftObjectPath.GetSubPathString();
|
|
|
|
Builder.Append(TEXT("\t\t\t\"AssetPath\": {\n\""));
|
|
Builder.Append(TEXT("\t\t\t\t\"PackageName\": \""));
|
|
Builder.Append(GetDebugFNameString(TLAP.GetPackageName()));
|
|
Builder.Append(TEXT("\",\n"));
|
|
Builder.Append(TEXT("\t\t\t\t\"AssetName\": \""));
|
|
Builder.Append(GetDebugFNameString(TLAP.GetAssetName()));
|
|
Builder.Append(TEXT("\"\n"));
|
|
Builder.Append(TEXT("\t\t\t},\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"Subpath (string)\": \""));
|
|
Builder.Append(Subpath);
|
|
Builder.Append(TEXT("\""));
|
|
|
|
Builder.Append(TEXT("\n\t\t},"));
|
|
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"SoftPackageReferencesTable\":[ "));
|
|
for (const FName SoftPackageRef : SoftPackageReferencesTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t\""));
|
|
Builder.Append(GetDebugFNameString(SoftPackageRef));
|
|
Builder.Append(TEXT("\","));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"GatherableTextDataTable\":[ "));
|
|
for (const FGatherableTextData& GatherableTextData : GatherableTextDataTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t{\n"));
|
|
Builder.Append(TEXT("\t\t\t\"SourceSiteContexts.SiteDescription (string)\": ["));
|
|
for (auto& SiteContext : GatherableTextData.SourceSiteContexts)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t\t\t\""));
|
|
Builder.Append(SiteContext.SiteDescription);
|
|
Builder.Append(TEXT("\","));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t\t\t]"));
|
|
Builder.Append(TEXT("\n\t\t},"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"ThumbnailTable\":[ "));
|
|
for (const FThumbnailEntry& ThumbnailEntry : ThumbnailTable)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t{\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ObjectPathWithoutPackageName (string)\": \""));
|
|
Builder.Append(ThumbnailEntry.ObjectPathWithoutPackageName);
|
|
Builder.Append(TEXT("\",\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ObjectShortClassName (string)\": \""));
|
|
Builder.Append(ThumbnailEntry.ObjectShortClassName);
|
|
Builder.Append(TEXT("\""));
|
|
|
|
Builder.Append(TEXT("\n\t\t},"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t],\n"));
|
|
|
|
Builder.Append(TEXT("\t\"AssetRegistryData\":[ "));
|
|
for (const FAssetRegistryObjectData& ObjData : AssetRegistryData.ObjectData)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t{\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"ObjectData\": {\n"));
|
|
Builder.Append(TEXT("\t\t\t\t\"ObjectPath (string)\": \""));
|
|
Builder.Append(ObjData.ObjectData.ObjectPath);
|
|
Builder.Append(TEXT("\",\n"));
|
|
Builder.Append(TEXT("\t\t\t\t\"ObjectClassName (string)\": \""));
|
|
Builder.Append(ObjData.ObjectData.ObjectClassName);
|
|
Builder.Append(TEXT("\"\n"));
|
|
Builder.Append(TEXT("\t\t\t},\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\"TagData\": [\n"));
|
|
for (const auto& TagData : ObjData.TagData)
|
|
{
|
|
FString Value = TagData.Value;
|
|
bool bNeedDecode = TagData.Key == FWorldPartitionActorDescUtils::ActorMetaDataTagName();
|
|
if (bNeedDecode)
|
|
{
|
|
const FString LongPackageName(SrcAsset);
|
|
const FString ObjectPath(ObjData.ObjectData.ObjectPath);
|
|
const FTopLevelAssetPath AssetClassPathName(ObjData.ObjectData.ObjectClassName);
|
|
const FAssetDataTagMap Tags(MakeTagMap(ObjData.TagData));
|
|
const FAssetData AssetData(LongPackageName, ObjectPath, AssetClassPathName, Tags);
|
|
|
|
struct FWorldPartitionAssetDataPrinter : FWorldPartitionAssetDataPatcher
|
|
{
|
|
FWorldPartitionAssetDataPrinter(int32 InIndentDepth)
|
|
: IndentDepth(InIndentDepth)
|
|
{
|
|
}
|
|
|
|
virtual bool DoPatch(FString& InOutString) override
|
|
{
|
|
Builder.Append(TEXT("\n"));
|
|
Indent();
|
|
Builder.Append(TEXT("string=\""));
|
|
Builder.Append(InOutString);
|
|
Builder.Append(TEXT("\""));
|
|
return false;
|
|
}
|
|
virtual bool DoPatch(FName& InOutName) override
|
|
{
|
|
Builder.Append(TEXT("\n"));
|
|
Indent();
|
|
Builder.Append(TEXT("FName=\""));
|
|
Builder.Append(InOutName.ToString());
|
|
Builder.Append(TEXT("\""));
|
|
return false;
|
|
}
|
|
virtual bool DoPatch(FSoftObjectPath& InOutSoft) override
|
|
{
|
|
Builder.Append(TEXT("\n"));
|
|
Indent();
|
|
Builder.Append(TEXT("FSoftObjectPath="));
|
|
FTopLevelAssetPath TLAP = InOutSoft.GetAssetPath();
|
|
Builder.Append(TEXT("{{PackageName=\""));
|
|
Builder.Append(TLAP.GetPackageName().ToString());
|
|
Builder.Append(TEXT("\", AssetName=\""));
|
|
Builder.Append(TLAP.GetAssetName().ToString());
|
|
Builder.Append(TEXT("\"}, SubPath (string)=\""));
|
|
Builder.Append(InOutSoft.GetSubPathString());
|
|
Builder.Append(TEXT("\"}"));
|
|
return false;
|
|
}
|
|
virtual bool DoPatch(FTopLevelAssetPath& InOutPath) override
|
|
{
|
|
Builder.Append(TEXT("\n"));
|
|
Indent();
|
|
Builder.Append(TEXT("FTopLevelAssetPath="));
|
|
Builder.Append(TEXT("{PackageName=\""));
|
|
Builder.Append(InOutPath.GetPackageName().ToString());
|
|
Builder.Append(TEXT("\", AssetName=\""));
|
|
Builder.Append(InOutPath.GetAssetName().ToString());
|
|
Builder.Append(TEXT("\"}"));
|
|
return false;
|
|
}
|
|
void Indent()
|
|
{
|
|
for (int32 i = 0; i < IndentDepth; ++i)
|
|
{
|
|
Builder.Append(TEXT("\t"));
|
|
}
|
|
}
|
|
const TCHAR* ToString()
|
|
{
|
|
return Builder.ToString();
|
|
}
|
|
int32 IndentDepth;
|
|
TStringBuilder<1024> Builder;
|
|
};
|
|
|
|
FString PatchedAssetData;
|
|
FWorldPartitionAssetDataPrinter Patcher(5);
|
|
FWorldPartitionActorDescUtils::GetPatchedAssetDataFromAssetData(AssetData, PatchedAssetData, &Patcher);
|
|
Value = Patcher.ToString();
|
|
}
|
|
|
|
Builder.Append(TEXT("\n\t\t\t\t{\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\t\t\"Key (string)\": \""));
|
|
Builder.Append(TagData.Key);
|
|
Builder.Append(TEXT("\",\n"));
|
|
Builder.Append(TEXT("\t\t\t\t\t\"Value"));
|
|
if (bNeedDecode)
|
|
{
|
|
Builder.Append(TEXT(" (decoded string)"));
|
|
}
|
|
else
|
|
{
|
|
Builder.Append(TEXT("(string)"));
|
|
}
|
|
Builder.Append(TEXT("\": \""));
|
|
Builder.Append(Value);
|
|
Builder.Append(TEXT("\"\n"));
|
|
|
|
Builder.Append(TEXT("\t\t\t\t},"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t\t\t]\n"));
|
|
|
|
Builder.Append(TEXT("\n\t\t},"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t]\n"));
|
|
|
|
Builder.Append(TEXT("\t\"AssetRegistryDependencyData\":{ "));
|
|
{
|
|
Builder.Append(TEXT("\n\t\t\"ImportIndexUsedInGame\":{ "));
|
|
for (ImportIndex = 0; ImportIndex < ImportTable.Num(); ++ImportIndex)
|
|
{
|
|
bool* UsedInGame = AssetRegistryData.ImportIndexUsedInGame.Find(ImportIndex);
|
|
bool bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
Builder.Appendf(TEXT("\n\t\t\t%d : %s,"), ImportIndex, bUsedInGame ? TEXT("true") : TEXT("false"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t\t}"));
|
|
|
|
Builder.Append(TEXT(",\n\t\t\"SoftPackageReferenceUsedInGame\":{ "));
|
|
for (FName SoftPackageReference : SoftPackageReferencesTable)
|
|
{
|
|
bool* UsedInGame = AssetRegistryData.SoftPackageReferenceUsedInGame.Find(SoftPackageReference);
|
|
bool bUsedInGame = UsedInGame ? *UsedInGame : true;
|
|
Builder.Append(TEXT("\n\t\t\t"));
|
|
Builder << SoftPackageReference;
|
|
Builder.Appendf(TEXT(" : %s,"), bUsedInGame ? TEXT("true") : TEXT("false"));
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t\t}"));
|
|
|
|
Builder.Append(TEXT(",\n\t\t\"ExtraPackageDependencies\":[ "));
|
|
for (const TPair<FName, UE::AssetRegistry::EExtraDependencyFlags>& Pair : AssetRegistryData.ExtraPackageDependencies)
|
|
{
|
|
Builder.Append(TEXT("\n\t\t\t[ \""));
|
|
Builder << Pair.Key;
|
|
Builder.Appendf(TEXT("\", 0x%x],"), (uint32) Pair.Value);
|
|
}
|
|
Builder.RemoveSuffix(1);
|
|
Builder.Append(TEXT("\n\t\t]"));
|
|
}
|
|
Builder.Append(TEXT("\n\t},\n"));
|
|
|
|
Builder.Append(TEXT("}"));
|
|
|
|
// Write to disk
|
|
TStringBuilder<256> OutPath;
|
|
OutPath.Append(OutputDirectory);
|
|
FString SubPath = SrcAsset;
|
|
FPaths::CollapseRelativeDirectories(SubPath);
|
|
if (SubPath.StartsWith(TEXT("../")))
|
|
{
|
|
int32 Pos = SubPath.Find(TEXT("../"), ESearchCase::CaseSensitive, ESearchDir::FromEnd);
|
|
if (Pos >= 0)
|
|
{
|
|
SubPath.RightChopInline(Pos + 3);
|
|
}
|
|
}
|
|
else if (SubPath.Len() > 2 && SubPath[1] == TEXT(':'))
|
|
{
|
|
SubPath.RightChopInline(2); // Drop the drive
|
|
}
|
|
OutPath = FPaths::Combine(OutPath, SubPath);
|
|
OutPath.Append(TEXT(".txt"));
|
|
FFileHelper::SaveStringToFile(Builder.ToString(), *OutPath);
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
#if WITH_TESTS
|
|
|
|
#include "Tests/TestHarnessAdapter.h"
|
|
|
|
TEST_CASE_NAMED(FAssetHeaderPatcherTests, "AssetHeaderPatcher", "[AssetHeaderPatcher][EngineFilter]")
|
|
{
|
|
// Useful when iterating so you halt if something fails while a debugger is attached
|
|
#if DEBUG_ASSET_HEADER_PATCHING
|
|
#undef CHECK
|
|
#undef CHECK_EQUALS
|
|
#undef CHECK_NOT_EQUALS
|
|
#define CHECK(...) if (!(__VA_ARGS__)) { UE_DEBUG_BREAK(); FAutomationTestFramework::Get().GetCurrentTest()->AddError(TEXT("Condition failed")); }
|
|
#define CHECK_EQUALS(What, X, Y) if(!FAutomationTestFramework::Get().GetCurrentTest()->TestEqual(What, X, Y)) { UE_DEBUG_BREAK(); };
|
|
#define CHECK_NOT_EQUALS(What, X, Y) if(!FAutomationTestFramework::Get().GetCurrentTest()->TestNotEqual(What, X, Y)) { UE_DEBUG_BREAK(); };
|
|
#endif
|
|
|
|
struct FTestPatcherContext : FAssetHeaderPatcher::FContext
|
|
{
|
|
FTestPatcherContext(TMap<FString, FString> PackageRenameMap, bool bGatherDependentPackages = true) : FAssetHeaderPatcher::FContext(PackageRenameMap, bGatherDependentPackages) {}
|
|
const TMap<FString, FString>& GetStringReplacements()
|
|
{
|
|
return StringReplacements;
|
|
}
|
|
|
|
void GenerateRemappings()
|
|
{
|
|
GenerateAdditionalRemappings();
|
|
}
|
|
|
|
const TArray<FCoreRedirect>& GetRedirects()
|
|
{
|
|
return Redirects;
|
|
}
|
|
|
|
const TArray<FString>& GetVerseMountPoints()
|
|
{
|
|
return VerseMountPoints;
|
|
}
|
|
};
|
|
|
|
// To avoid having to deal with serialization, we mock some data and inject it directly
|
|
// into the patcher as if done via serialization
|
|
const FString DummySrcDstAsset = TEXT("/SrcMount/SomePath/SrcPackage");
|
|
const TCHAR* SrcPackagePath = TEXT("/SrcMount/SomePath/SrcPackage");
|
|
const TCHAR* DstPackagePath = TEXT("/DstMount/SomePath/DstPackage");
|
|
const TCHAR* SrcPackageObjectPath = TEXT("/SrcMount/SomePath/SrcPackage.SrcPackage");
|
|
const TCHAR* DstPackageObjectPath = TEXT("/DstMount/SomePath/DstPackage.DstPackage");
|
|
const TCHAR* SrcMountName = TEXT("/SourceSpecialMount/");
|
|
const TCHAR* DstMountName = TEXT("/DestinationSpecialMount/");
|
|
const FSoftObjectPath SoftObjectPathToRedirect(TEXT("/ToBeRedirectedMount/SomePath/ToBeRedirectedPackage.ToBeRedirectedPackage:Some.ToBeRedirectedPackage.Subobject"));
|
|
const FSoftObjectPath RedirectedSoftObjectPath(TEXT("/RedirectedMount/SomePath/RedirectedPackage.RedirectedPackage:Some.RedirectedPackage.Subobject"));
|
|
const FName SrcPackagePathFName(SrcPackagePath);
|
|
const FName DstPackagePathFName(DstPackagePath);
|
|
const FName SrcAssetFName(TEXT("SrcPackage"));
|
|
const FName DstAssetFName(TEXT("DstPackage"));
|
|
const FName SrcExportObjectFName = SrcAssetFName;
|
|
const FName DstExportObjectFName = DstAssetFName;
|
|
const FName DummyImportPackagePathFName(TEXT("/DummyMount/DummyPackage"));
|
|
// Import FNames
|
|
const FName SrcEngineModuleImportObjectName(TEXT("/Script/SrcEngineModule"));
|
|
const FName SrcTypeAImportObjectName(TEXT("SrcTypeA"));
|
|
const FName SrcTypeBImportObjectName(TEXT("SrcTypeB"));
|
|
const FName OnlySubTypeChangedImportObjectName(TEXT("OnlySubTypeChanged"));
|
|
const FName MovedButNotRenamedTypeImportObjectName(TEXT("MovedButNotRenamedType"));
|
|
const FName SrcPropertyToChangeAImportObjectName(TEXT("SrcPropertyToChangeA"));
|
|
const FName SrcPropertyToChangeBImportObjectName(TEXT("SrcPropertyToChangeB"));
|
|
const FName MovedButNotRenamedPropertyImportObjectName(TEXT("MovedButNotRenamedProperty"));
|
|
const FName NewOuterImportObjectName(TEXT("NewOuter"));
|
|
const FName MovedToNewOuterImportObjectName(TEXT("MovedToNewOuter"));
|
|
const FName InnerMovedButNotRenamedPropertyImportObjectName(TEXT("InnerMovedButNotRenamedProperty"));
|
|
const FName InnerInnerMovedButNotRenamedPropertyImportObjectName(TEXT("InnerInnerMovedButNotRenamedProperty"));
|
|
const FName UnchangedPropertyImportObjectName(TEXT("UnchangedProperty"));
|
|
const FName SrcImportClassName(TEXT("SrcClass"));
|
|
const FName SrcImportClassPackage(TEXT("/Engine/SrcClassPackage"));
|
|
const FName SrcVerseAssetName(TEXT("/Module/_Verse/VerseAsset"));
|
|
const FName SrcVerseImportObjectName(TEXT("some_verse_class"));
|
|
const FName UnchangedVerseImportSubObject1Name(TEXT("__verse_0x7A8CDEBC_VerseObject1"));
|
|
const FName UnchangedVerseImportSubObject2Name(TEXT("__verse_0x5614AC82_VerseObject2"));
|
|
|
|
const FName DstEngineModuleImportObjectName(TEXT("/Script/DstEngineModule"));
|
|
const FName DstTypeAImportObjectName(TEXT("DstTypeA"));
|
|
const FName DstTypeBImportObjectName(TEXT("DstTypeB"));
|
|
const FName DstPropertyToChangeAImportObjectName(TEXT("DstPropertyToChangeA"));
|
|
const FName DstPropertyToChangeBImportObjectName(TEXT("DstPropertyToChangeB"));
|
|
const FName DstVerseAssetName(TEXT("/Module/_Verse"));
|
|
const FName DstVerseImportObjectName(TEXT("VerseAsset-some_verse_class"));
|
|
const FName DstImportClassName(TEXT("DstClass"));
|
|
const FName DstImportClassPackage(TEXT("/Engine/DstClassPackage"));
|
|
|
|
|
|
TMap<FString, FString> MountPointReplacementMap =
|
|
{
|
|
{ SrcMountName, DstMountName },
|
|
};
|
|
|
|
TMap<FString, FString> PackageRenameMap =
|
|
{
|
|
{ SrcPackagePath, DstPackagePath },
|
|
};
|
|
|
|
auto MakeImport = [SrcPackagePathFName](const FName ObjectName, const FPackageIndex OuterIndex, const FName ClassPackage, const FName ClassName, const FName PackageName)
|
|
{
|
|
FObjectImport Import;
|
|
Import.ObjectName = ObjectName;
|
|
Import.OuterIndex = OuterIndex;
|
|
#if WITH_EDITORONLY_DATA
|
|
Import.OldClassName = NAME_None;
|
|
Import.PackageName = PackageName;
|
|
#endif
|
|
Import.ClassPackage = ClassPackage;
|
|
Import.ClassName = ClassName;
|
|
return Import;
|
|
};
|
|
|
|
auto MakeExport = [](const FName ObjectName, const FPackageIndex ThisIndex, const FPackageIndex OuterIndex,
|
|
const FPackageIndex SuperIndex, const FPackageIndex ClassIndex, const FPackageIndex TemplateIndex)
|
|
{
|
|
FObjectExport Export;
|
|
Export.ObjectName = ObjectName;
|
|
Export.ThisIndex = ThisIndex;
|
|
Export.OuterIndex = OuterIndex;
|
|
Export.SuperIndex = SuperIndex;
|
|
Export.ClassIndex = ClassIndex;
|
|
Export.TemplateIndex = TemplateIndex;
|
|
#if WITH_EDITORONLY_DATA
|
|
Export.OldClassName = NAME_None;
|
|
#endif
|
|
return Export;
|
|
};
|
|
|
|
struct FImportTestCase
|
|
{
|
|
FObjectImport Src;
|
|
FObjectImport Dst;
|
|
bool bExistingImport;
|
|
};
|
|
// Note the order of these cases defines the ImportTable entry order before/after patching
|
|
TArray<FImportTestCase> ImportTestCases
|
|
{
|
|
// /Script/SrcEngineModule -> (remains unchanged)
|
|
{
|
|
.Src = MakeImport(SrcEngineModuleImportObjectName, FPackageIndex(), GLongCoreUObjectPackageName, NAME_Package, SrcPackagePathFName),
|
|
.Dst = MakeImport(SrcEngineModuleImportObjectName, FPackageIndex(), GLongCoreUObjectPackageName, NAME_Package, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeA -> /Script/DstEngineModule.DstTypeA
|
|
{
|
|
.Src = MakeImport(SrcTypeAImportObjectName, FPackageIndex::FromImport(0), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(DstTypeAImportObjectName, FPackageIndex::FromImport(18), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeA.SrcPropertyToChangeA -> /Script/DstEngineModule.DstTypeA.DstPropertyToChangeA
|
|
{
|
|
.Src = MakeImport(SrcPropertyToChangeAImportObjectName, FPackageIndex::FromImport(1), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(DstPropertyToChangeAImportObjectName, FPackageIndex::FromImport(1), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeA.MovedButNotRenamedProperty -> /Script/DstEngineModule.DstTypeA.MovedButNotRenamedProperty
|
|
{
|
|
.Src = MakeImport(MovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(1), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(MovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(1), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeA.MovedButNotRenamedProperty.InnerMovedButNotRenamedProperty
|
|
// ->
|
|
// /Script/DstEngineModule.DstTypeA.MovedButNotRenamedProperty.InnerMovedButNotRenamedProperty
|
|
{
|
|
.Src = MakeImport(InnerMovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(3), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(InnerMovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(3), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeA.MovedButNotRenamedProperty.InnerMovedButNotRenamedProperty.InnerInnerMovedButNotRenamedProperty
|
|
// ->
|
|
// /Script/DstEngineModule.DstTypeA.MovedButNotRenamedProperty.InnerMovedButNotRenamedProperty.InnerInnerMovedButNotRenamedProperty
|
|
{
|
|
.Src = MakeImport(InnerInnerMovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(4), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(InnerInnerMovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(4), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.OnlySubTypeChanged -> (remains unchanged)
|
|
{
|
|
.Src = MakeImport(OnlySubTypeChangedImportObjectName, FPackageIndex::FromImport(0), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(OnlySubTypeChangedImportObjectName, FPackageIndex::FromImport(0), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.OnlySubTypeChanged.SrcPropertyToChangeB -> /Script/SrcEngineModule.OnlySubTypeChanged.DstPropertyToChangeB
|
|
{
|
|
.Src = MakeImport(SrcPropertyToChangeBImportObjectName, FPackageIndex::FromImport(6), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(DstPropertyToChangeBImportObjectName, FPackageIndex::FromImport(6), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.OnlySubTypeChanged.UnchangedProperty -> (remains unchanged)
|
|
{
|
|
.Src = MakeImport(UnchangedPropertyImportObjectName, FPackageIndex::FromImport(6), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(UnchangedPropertyImportObjectName, FPackageIndex::FromImport(6), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeB -> /Script/SrcEngineModule.DstTypeB
|
|
{
|
|
.Src = MakeImport(SrcTypeBImportObjectName, FPackageIndex::FromImport(0), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(DstTypeBImportObjectName, FPackageIndex::FromImport(0), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeB.MovedButNotRenamedProperty -> /Script/SrcEngineModule.DstTypeB.MovedButNotRenamedProperty
|
|
{
|
|
.Src = MakeImport(MovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(9), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(MovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(9), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.SrcTypeB.MovedToNewOuter -> /Script/SrcEngineModule.NewOuter.MovedToNewOuter
|
|
{
|
|
.Src = MakeImport(MovedToNewOuterImportObjectName, FPackageIndex::FromImport(9), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(MovedToNewOuterImportObjectName, FPackageIndex::FromImport(19), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.MovedButNotRenamedType -> /Script/DstEngineModule.MovedButNotRenamedType
|
|
{
|
|
.Src = MakeImport(MovedButNotRenamedTypeImportObjectName, FPackageIndex::FromImport(0), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(MovedButNotRenamedTypeImportObjectName, FPackageIndex::FromImport(18), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Script/SrcEngineModule.MovedButNotRenamedType.MovedButNotRenamedProperty -> /Script/DstEngineModule.MovedButNotRenamedType.MovedButNotRenamedProperty
|
|
{
|
|
.Src = MakeImport(MovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(12), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(MovedButNotRenamedPropertyImportObjectName, FPackageIndex::FromImport(12), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// Note, the verse transformations test a case where package is moved into a new package with a different path, while at the same time renaming top-level assets
|
|
// in the new path. We want to ensure we don't keep using the old top-level asset name. These redirections are done using CoreRedirects from the context
|
|
// rather than explicit remappings in the patcher.
|
|
|
|
// /Module/_Verse/VerseAsset -> /Module/_Verse
|
|
{
|
|
.Src = MakeImport(SrcVerseAssetName, FPackageIndex(), GLongCoreUObjectPackageName, NAME_Package, SrcPackagePathFName),
|
|
.Dst = MakeImport(DstVerseAssetName, FPackageIndex(), GLongCoreUObjectPackageName, NAME_Package, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Module/_Verse/VerseAsset.some_verse_class -> /Module/_Verse.VerseAsset-some_verse_class
|
|
{
|
|
.Src = MakeImport(SrcVerseImportObjectName, FPackageIndex::FromImport(14), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(DstVerseImportObjectName, FPackageIndex::FromImport(14), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Module/_Verse/VerseAsset.some_verse_class.__verse_0x7A8CDEBC_VerseObject1
|
|
// ->
|
|
// /Module/_Verse.VerseAsset-some_verse_class.__verse_0x7A8CDEBC_VerseObject1
|
|
{
|
|
.Src = MakeImport(UnchangedVerseImportSubObject1Name, FPackageIndex::FromImport(15), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(UnchangedVerseImportSubObject1Name, FPackageIndex::FromImport(15), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// /Module/_Verse/VerseAsset.some_verse_class.__verse_0x7A8CDEBC_VerseObject1.__verse_0x5614AC82_VerseObject2
|
|
// ->
|
|
// /Module/_Verse.VerseAsset-some_verse_class.__verse_0x7A8CDEBC_VerseObject1.__verse_0x5614AC82_VerseObject2
|
|
{
|
|
.Src = MakeImport(UnchangedVerseImportSubObject2Name, FPackageIndex::FromImport(16), SrcImportClassPackage, SrcImportClassName, SrcPackagePathFName),
|
|
.Dst = MakeImport(UnchangedVerseImportSubObject2Name, FPackageIndex::FromImport(16), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = true,
|
|
},
|
|
// <none> -> /Script/DstEngineModule
|
|
{
|
|
.Src = FObjectImport(),
|
|
.Dst = MakeImport(DstEngineModuleImportObjectName, FPackageIndex(), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = false,
|
|
},
|
|
// <none> -> /Script/SrcEngineModule.NewOuter
|
|
{
|
|
.Src = FObjectImport(),
|
|
.Dst = MakeImport(NewOuterImportObjectName, FPackageIndex::FromImport(0), DstImportClassPackage, DstImportClassName, DstPackagePathFName),
|
|
.bExistingImport = false,
|
|
},
|
|
};
|
|
|
|
struct FExportTestCase
|
|
{
|
|
FObjectExport Src;
|
|
FObjectExport Dst;
|
|
};
|
|
TArray<FExportTestCase> ExportTestCases
|
|
{
|
|
// SrcPackage -> DstPackage
|
|
// Super: /Script/SrcEngineModule -> /Script/DstEngineModule
|
|
// Class: /Script/SrcEngineModule.SrcTypeA.SrcPropertyToChangeA -> /Script/DstEngineModule.DstTypeA.DstPropertyToChangeA
|
|
// Template: /Script/SrcEngineModule.OnlySubTypeChanged -> /Script/SrcEngineModule.OnlySubTypeChanged
|
|
{
|
|
.Src = MakeExport(SrcExportObjectFName, FPackageIndex::FromExport(0), FPackageIndex(), FPackageIndex::FromImport(0), FPackageIndex::FromImport(2), FPackageIndex::FromImport(4)),
|
|
.Dst = MakeExport(DstExportObjectFName, FPackageIndex::FromExport(0), FPackageIndex(), FPackageIndex::FromImport(11), FPackageIndex::FromImport(2), FPackageIndex::FromImport(4))
|
|
},
|
|
// SrcPackage.SrcPackage -> DstPackage.DstPackage
|
|
// Super: /Script/SrcEngineModule.SrcTypeA -> /Script/DstEngineModule.DstTypeA
|
|
// Class: /Script/SrcEngineModule.SrcTypeA.MovedButNotRenamedProperty -> /Script/DstEngineModule.DstTypeA.MovedButNotRenamedProperty
|
|
// Template: /Script/SrcEngineModule.OnlySubTypeChanged.SrcPropertyToChangeB -> /Script/SrcEngineModule.OnlySubTypeChanged.DstPropertyToChangeB
|
|
{
|
|
.Src = MakeExport(SrcExportObjectFName, FPackageIndex::FromExport(1), FPackageIndex::FromExport(0), FPackageIndex::FromImport(1), FPackageIndex::FromImport(3), FPackageIndex::FromImport(5)),
|
|
.Dst = MakeExport(DstExportObjectFName, FPackageIndex::FromExport(1), FPackageIndex::FromExport(0), FPackageIndex::FromImport(1), FPackageIndex::FromImport(3), FPackageIndex::FromImport(5))
|
|
}
|
|
};
|
|
|
|
FCoreRedirectsContext TestRedirectContext;
|
|
TestRedirectContext.InitializeContext();
|
|
CHECK(TestRedirectContext.IsInitialized());
|
|
FCoreRedirectsContext& OriginalContext = FCoreRedirectsContext::GetThreadContext();
|
|
FCoreRedirectsContext::SetThreadContext(TestRedirectContext);
|
|
ON_SCOPE_EXIT{ FCoreRedirectsContext::SetThreadContext(OriginalContext); };
|
|
|
|
FTestPatcherContext Context(PackageRenameMap, false /*bGatherDependentPackages*/);
|
|
const TMap<FString, FString>& StringReplacements = Context.GetStringReplacements();
|
|
CHECK(StringReplacements.Num() > PackageRenameMap.Num()); // Ensure we generated more mappings off of the PackageRenameMap
|
|
CHECK(FCoreRedirects::AddRedirectList(Context.GetRedirects(), TEXT("Asset Header Patcher Tests")));
|
|
|
|
FAssetHeaderPatcherInner Patcher(DummySrcDstAsset, DummySrcDstAsset, StringReplacements, MountPointReplacementMap);
|
|
|
|
auto AddToNameTable = [&Patcher](FName Name)
|
|
{
|
|
Patcher.NameToIndexMap.Add(Name.GetDisplayIndex(), Patcher.NameTable.Num());
|
|
Patcher.NameTable.Add(Name);
|
|
};
|
|
int32 OriginalNameTableCount = 0;
|
|
auto ResetPatcher = [&]()
|
|
{
|
|
// Reset Patcher
|
|
Patcher.ResetInternalState();
|
|
|
|
// Repopulate patcher state with our test data normally set through deserialization
|
|
///////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// NameTable
|
|
AddToNameTable(SrcPackagePathFName);
|
|
AddToNameTable(SrcAssetFName);
|
|
AddToNameTable(DummyImportPackagePathFName);
|
|
// Redirected names (softpaths don't include the subpath in the name table
|
|
AddToNameTable(SoftObjectPathToRedirect.GetAssetPath().GetPackageName());
|
|
AddToNameTable(SoftObjectPathToRedirect.GetAssetPath().GetAssetName());
|
|
// Export Table Names
|
|
AddToNameTable(SrcExportObjectFName);
|
|
// Import Table Names
|
|
AddToNameTable(SrcEngineModuleImportObjectName);
|
|
AddToNameTable(SrcTypeAImportObjectName);
|
|
AddToNameTable(SrcTypeBImportObjectName);
|
|
AddToNameTable(OnlySubTypeChangedImportObjectName);
|
|
AddToNameTable(MovedButNotRenamedTypeImportObjectName);
|
|
AddToNameTable(SrcPropertyToChangeAImportObjectName);
|
|
AddToNameTable(SrcPropertyToChangeBImportObjectName);
|
|
AddToNameTable(MovedButNotRenamedPropertyImportObjectName);
|
|
AddToNameTable(NewOuterImportObjectName);
|
|
AddToNameTable(MovedToNewOuterImportObjectName);
|
|
AddToNameTable(InnerMovedButNotRenamedPropertyImportObjectName);
|
|
AddToNameTable(InnerInnerMovedButNotRenamedPropertyImportObjectName);
|
|
AddToNameTable(UnchangedPropertyImportObjectName);
|
|
AddToNameTable(SrcVerseAssetName);
|
|
AddToNameTable(SrcVerseImportObjectName);
|
|
AddToNameTable(UnchangedVerseImportSubObject1Name);
|
|
AddToNameTable(UnchangedVerseImportSubObject2Name);
|
|
AddToNameTable(SrcImportClassName);
|
|
AddToNameTable(SrcImportClassPackage);
|
|
AddToNameTable(GLongCoreUObjectPackageName);
|
|
AddToNameTable(NAME_Package);
|
|
|
|
auto CheckNameTableInit = [&Patcher](FName Name)
|
|
{
|
|
// Skip lookups for None, as we already verified the NameTable
|
|
// does not / should not contains None
|
|
if (Name == NAME_None)
|
|
{
|
|
return;
|
|
}
|
|
CHECK(Patcher.NameToIndexMap.Contains(Name.GetDisplayIndex()));
|
|
CHECK(Patcher.NameTable[Patcher.NameToIndexMap[Name.GetDisplayIndex()]] == Name);
|
|
};
|
|
CHECK(!Patcher.NameToIndexMap.Contains(FName(NAME_None).GetDisplayIndex()));
|
|
CHECK(!Patcher.NameTable.Contains(NAME_None));
|
|
// Import/Export Table (see breakdown of import names for the intended tests in the FObjectResource test section below)
|
|
for (const FImportTestCase& TestCase : ImportTestCases)
|
|
{
|
|
const FObjectImport& Import = TestCase.Src;
|
|
if (Import.ObjectName == NAME_None)
|
|
{
|
|
// We have more cases than initial starting states so if we see an empty
|
|
// name we can stop adding to the initial state
|
|
break;
|
|
}
|
|
CheckNameTableInit(Import.ObjectName);
|
|
CheckNameTableInit(Import.ClassName);
|
|
CheckNameTableInit(Import.ClassPackage);
|
|
#if WITH_EDITORONLY_DATA
|
|
CheckNameTableInit(Import.PackageName);
|
|
CheckNameTableInit(Import.OldClassName);
|
|
#endif
|
|
Patcher.ImportTable.Add(Import);
|
|
}
|
|
|
|
for (const FExportTestCase& TestCase : ExportTestCases)
|
|
{
|
|
const FObjectExport& Export = TestCase.Src;
|
|
CheckNameTableInit(Export.ObjectName);
|
|
#if WITH_EDITORONLY_DATA
|
|
CheckNameTableInit(Export.OldClassName);
|
|
#endif
|
|
Patcher.ExportTable.Add(Export);
|
|
}
|
|
|
|
// Summary
|
|
Patcher.Summary.NameCount = Patcher.NameTable.Num();
|
|
Patcher.OriginalPackagePath = SrcPackagePathFName;
|
|
OriginalNameTableCount = Patcher.NameTable.Num();
|
|
};
|
|
|
|
SECTION("FContext Additional Remappings")
|
|
{
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch string with direct match"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage.SrcPackage)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage.DstPackage)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated Top-Level Asset mapping"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage.SrcPackage_C)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage.DstPackage_C)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated Blueprint Generated Class mapping"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage.Default__SrcPackage_C)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage.Default__DstPackage_C)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated Blueprint Generated Class Default Object mapping"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage.SrcPackageEditorOnlyData)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage.DstPackageEditorOnlyData)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated MaterialFunctionInterface Editor Only Data mapping"), Actual, Expected);
|
|
}
|
|
|
|
SECTION("Verse Mountpoints")
|
|
{
|
|
for (const FString& VerseMount : Context.GetVerseMountPoints())
|
|
{
|
|
// We only generate verse paths for objects, so this package path will not have a mapping
|
|
{
|
|
FString Actual = FString::Printf(TEXT(R"(/%s/SrcMount/SomePath/SrcPackage)"), *VerseMount);
|
|
const FString Expected = FString::Printf(TEXT(R"(/%s/DstMount/SomePath/DstPackage)"), *VerseMount);
|
|
CHECK(!Patcher.DoPatch(Actual));
|
|
CHECK_NOT_EQUALS(TEXT("Patch string with direct match"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual = FString::Printf(TEXT(R"(/%s/SrcMount/SomePath/SrcPackage/SrcPackage)"), *VerseMount);
|
|
const FString Expected = FString::Printf(TEXT(R"(/%s/DstMount/SomePath/DstPackage/DstPackage)"), *VerseMount);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch string with direct match"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual = FString::Printf(TEXT(R"(/%s/SrcMount/SomePath/SrcPackage/SrcPackage)"), *VerseMount);
|
|
const FString Expected = FString::Printf(TEXT(R"(/%s/DstMount/SomePath/DstPackage/DstPackage)"), *VerseMount);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated Top-Level Asset mapping"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual = FString::Printf(TEXT(R"(/%s/SrcMount/SomePath/SrcPackage/SrcPackage_C)"), *VerseMount);
|
|
const FString Expected = FString::Printf(TEXT(R"(/%s/DstMount/SomePath/DstPackage/DstPackage_C)"), *VerseMount);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated Blueprint Generated Class mapping"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual = FString::Printf(TEXT(R"(/%s/SrcMount/SomePath/SrcPackage/Default__SrcPackage_C)"), *VerseMount);
|
|
const FString Expected = FString::Printf(TEXT(R"(/%s/DstMount/SomePath/DstPackage/Default__DstPackage_C)"), *VerseMount);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated Blueprint Generated Class Default Object mapping"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual = FString::Printf(TEXT(R"(/%s/SrcMount/SomePath/SrcPackage/SrcPackageEditorOnlyData)"), *VerseMount);
|
|
const FString Expected = FString::Printf(TEXT(R"(/%s/DstMount/SomePath/DstPackage/DstPackageEditorOnlyData)"), *VerseMount);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Generated MaterialFunctionInterface Editor Only Data mapping"), Actual, Expected);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("DoPatch(FString)")
|
|
{
|
|
SECTION("Direct match")
|
|
{
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch string with direct match"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage2)"));
|
|
const FString Expected = Actual; // Must be a copy
|
|
CHECK(!Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch string with no direct match"), Actual, Expected);
|
|
}
|
|
|
|
// Do not replace strings that happen to overlap with ObjectNames that are remapped
|
|
{
|
|
FString Actual(TEXT(R"(SrcPackage)"));
|
|
const FString Expected(TEXT(R"(SrcPackage)"));
|
|
CHECK(!Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Do not remap a string that matches a non-fully-qualified ObjectName"), Actual, Expected);
|
|
}
|
|
}
|
|
|
|
SECTION("Sub-Object Paths")
|
|
{
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage.SrcPackage:AnOuter.To.A.SubObject)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage.DstPackage:AnOuter.To.A.SubObject)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch sub-object path"), Actual, Expected);
|
|
}
|
|
|
|
// Worth adding support for in the future, but at the moment we cannot patch various parts of unquoted
|
|
// sub-object paths (that are specifically strings in the header, FNames are fine). In this case we
|
|
// can't patch the package path because the top-level asset (UnmappedObject) has no mapping for patching
|
|
{
|
|
FString Actual(TEXT(R"(/SrcMount/SomePath/SrcPackage.UnmappedObject:AnOuter.To.A.SubObject)"));
|
|
const FString Expected(TEXT(R"(/DstMount/SomePath/DstPackage.UnmappedObject:AnOuter.To.A.SubObject)"));
|
|
CHECK(!Patcher.DoPatch(Actual));
|
|
CHECK_NOT_EQUALS(TEXT("Can't patch sub-object paths, for "), Actual, Expected);
|
|
}
|
|
}
|
|
|
|
SECTION("Quoted match")
|
|
{
|
|
SECTION("Single Quote")
|
|
{
|
|
{
|
|
FString Actual(TEXT(R"('/SrcMount/SomePath/SrcPackage')"));
|
|
const FString Expected(TEXT(R"('/DstMount/SomePath/DstPackage')"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch package path with quotes"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"('/SrcMount/SomePath/SrcPackage.SrcPackage')"));
|
|
const FString Expected(TEXT(R"('/DstMount/SomePath/DstPackage.DstPackage')"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch object path with quotes"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"('/SrcMount/SomePath/SrcPackage.SrcPackage_C')"));
|
|
const FString Expected(TEXT(R"('/DstMount/SomePath/DstPackage.DstPackage_C')"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch blueprint generated class with quotes"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"('/SrcMount/SomePath/SrcPackage.Default__SrcPackage_C')"));
|
|
const FString Expected(TEXT(R"('/DstMount/SomePath/DstPackage.Default__DstPackage_C')"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch default blueprint generated class object path with quotes"), Actual, Expected);
|
|
}
|
|
|
|
// Do not replace strings that happen to overlap with ObjectNames that are remapped
|
|
{
|
|
FString Actual(TEXT(R"('SrcPackage')"));
|
|
const FString Expected(TEXT(R"('SrcPackage')"));
|
|
CHECK(!Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Do not remap a string that matches a non-fully-qualified ObjectName"), Actual, Expected);
|
|
}
|
|
}
|
|
|
|
SECTION("Double Quote")
|
|
{
|
|
{
|
|
FString Actual(TEXT(R"("/SrcMount/SomePath/SrcPackage")"));
|
|
const FString Expected(TEXT(R"("/DstMount/SomePath/DstPackage")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch package path with quotes"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"("/SrcMount/SomePath/SrcPackage.SrcPackage")"));
|
|
const FString Expected(TEXT(R"("/DstMount/SomePath/DstPackage.DstPackage")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch object path with quotes"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"("/SrcMount/SomePath/SrcPackage.SrcPackage_C")"));
|
|
const FString Expected(TEXT(R"("/DstMount/SomePath/DstPackage.DstPackage_C")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch blueprint generated class with quotes"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"("/SrcMount/SomePath/SrcPackage.Default__SrcPackage_C")"));
|
|
const FString Expected(TEXT(R"("/DstMount/SomePath/DstPackage.Default__DstPackage_C")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch default blueprint generated class object path with quotes"), Actual, Expected);
|
|
}
|
|
|
|
// Do not replace strings that happen to overlap with ObjectNames that are remapped
|
|
{
|
|
FString Actual(TEXT(R"("SrcPackage")"));
|
|
const FString Expected(TEXT(R"("SrcPackage")"));
|
|
CHECK(!Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Do not remap a string that matches a non-fully-qualified ObjectName"), Actual, Expected);
|
|
}
|
|
}
|
|
|
|
SECTION("Substring match")
|
|
{
|
|
{
|
|
FString Actual(
|
|
TEXT(R"(((ReferenceNodePath="/SrcMount/SomePath/SrcPackage.SrcPackage:RigVMModel.Setup Arm",)")
|
|
TEXT(R"(((Package="/SrcMount/SomePath/SrcPackage",)")
|
|
TEXT(R"(HostObject="/SrcMount/SomePath/SrcPackage.SrcPackage_C")))"));
|
|
FString Expected(
|
|
TEXT(R"(((ReferenceNodePath="/DstMount/SomePath/DstPackage.DstPackage:RigVMModel.Setup Arm",)")
|
|
TEXT(R"(((Package="/DstMount/SomePath/DstPackage",)")
|
|
TEXT(R"(HostObject="/DstMount/SomePath/DstPackage.DstPackage_C")))"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch substring with quoted package, object and sub-object paths"), Actual, Expected);
|
|
}
|
|
|
|
// Do not replace strings that happen to overlap with ObjectNames that are remapped(i.e "SrcPackage=" is not transformed to "DstPackage=")
|
|
{
|
|
FString Actual(
|
|
TEXT(R"(((SrcPackage="/SrcMount/SomePath/SrcPackage.SrcPackage:RigVMModel.Setup Arm",)")
|
|
TEXT(R"(((SrcPackage="/SrcMount/SomePath/SrcPackage",)")
|
|
TEXT(R"(SrcPackage="/SrcMount/SomePath/SrcPackage.SrcPackage_C")))"));
|
|
FString Expected(
|
|
TEXT(R"(((SrcPackage="/DstMount/SomePath/DstPackage.DstPackage:RigVMModel.Setup Arm",)")
|
|
TEXT(R"(((SrcPackage="/DstMount/SomePath/DstPackage",)")
|
|
TEXT(R"(SrcPackage="/DstMount/SomePath/DstPackage.DstPackage_C")))"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch substring with quoted package, object and sub-object paths. No non-fully-qualified ObjectNames are patched."), Actual, Expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("Mountpoint match")
|
|
{
|
|
/*
|
|
We currently don't support mount point replacement _for strings_ that don't
|
|
provide some kind of delimiter for us to scan for. As such package paths
|
|
and top-level asset paths are not supported unless they are quoted. Sub-object
|
|
paths are supported.
|
|
*/
|
|
/*
|
|
{
|
|
FString Actual(TEXT(R"(/SourceSpecialMount/SomePath/SomePackage)"));
|
|
const FString Expected(TEXT(R"(/DestinationSpecialMount/SomePath/SomePackage)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch package path replaces only mount"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SourceSpecialMount/SomePath/SomePackage.SomePackage)"));
|
|
const FString Expected(TEXT(R"(/DestinationSpecialMount/SomePath/SomePackage.SomePackage)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch package object path replaces only mount"), Actual, Expected);
|
|
}
|
|
*/
|
|
|
|
{
|
|
FString Actual(TEXT(R"(/SourceSpecialMount/SomePath/SomePackage.TopLevel:SubObject1.SubObject2)"));
|
|
const FString Expected(TEXT(R"(/DestinationSpecialMount/SomePath/SomePackage.TopLevel:SubObject1.SubObject2)"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch package sub-object path replaces only mount"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"("/SourceSpecialMount/SomePath/SomePackage")"));
|
|
const FString Expected(TEXT(R"("/DestinationSpecialMount/SomePath/SomePackage")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch double quoted path replaces only mount"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"('/SourceSpecialMount/SomePath/SomePackage')"));
|
|
const FString Expected(TEXT(R"('/DestinationSpecialMount/SomePath/SomePackage')"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Patch single quoted path replaces only mount"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(SomePrefix="/SourceSpecialMount/SomePath/SomePackage")"));
|
|
const FString Expected(TEXT(R"(SomePrefix="/DestinationSpecialMount/SomePath/SomePackage")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Substring patch replaces only mount when double quoted"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(TEXT(R"(SomePrefix='/SourceSpecialMount/SomePath/SomePackage')"));
|
|
const FString Expected(TEXT(R"(SomePrefix='/DestinationSpecialMount/SomePath/SomePackage')"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Substring patch replaces only mount when single quoted"), Actual, Expected);
|
|
}
|
|
|
|
{
|
|
FString Actual(
|
|
TEXT(R"("/SourceSpecialMount/SomePath/SomePackage1",)")
|
|
TEXT(R"("/SourceSpecialMount/SomePath/SomePackage2")"));
|
|
const FString Expected(
|
|
TEXT(R"("/DestinationSpecialMount/SomePath/SomePackage1",)")
|
|
TEXT(R"("/DestinationSpecialMount/SomePath/SomePackage2")"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("Substring patch replaces mount in multiple double quoted paths"), Actual, Expected);
|
|
}
|
|
}
|
|
}
|
|
|
|
SECTION("DoPatch(FSoftObjectPath)")
|
|
{
|
|
{
|
|
ResetPatcher();
|
|
|
|
FSoftObjectPath Actual(TEXT("/SrcMount/SomePath/SrcPackage.SrcPackage"));
|
|
FSoftObjectPath Expected(TEXT("/DstMount/SomePath/DstPackage.DstPackage"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("SoftObjectPath patching"), Actual, Expected);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
Patcher.PatchNameTable();
|
|
CHECK_EQUALS(TEXT("SoftObject patching updates NameTable entry"), Patcher.NameTable[0], DstPackagePathFName);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
}
|
|
|
|
{
|
|
ResetPatcher();
|
|
|
|
FSoftObjectPath Actual(TEXT("/SrcMount/SomePath/SrcPackage.SrcPackage:Some.SrcPackage.Subobject"));
|
|
// Note we do not replace the sub-object "SrcPackage" despite it matching the original package and object name
|
|
FSoftObjectPath Expected(TEXT("/DstMount/SomePath/DstPackage.DstPackage:Some.SrcPackage.Subobject"));
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("SoftObjectPath with sub-object path patching"), Actual, Expected);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
Patcher.PatchNameTable();
|
|
CHECK_EQUALS(TEXT("SoftObject patching updates NameTable entry"), Patcher.NameTable[0], DstPackagePathFName);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
}
|
|
|
|
#if WITH_EDITOR
|
|
SECTION("DoPatch(FSoftObjectPath)-WithRedirector")
|
|
{
|
|
ResetPatcher();
|
|
|
|
GRedirectCollector.AddAssetPathRedirection(SoftObjectPathToRedirect, RedirectedSoftObjectPath);
|
|
ON_SCOPE_EXIT
|
|
{
|
|
GRedirectCollector.RemoveAssetPathRedirection(SoftObjectPathToRedirect);
|
|
};
|
|
|
|
FSoftObjectPath Actual = SoftObjectPathToRedirect;
|
|
FSoftObjectPath Expected = RedirectedSoftObjectPath;
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("SoftObjectPath with sub-object path patching"), Actual, Expected);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
Patcher.PatchNameTable();
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
}
|
|
|
|
SECTION("DoPatch(FSoftObjectPath)-WithRedirectorAndExplicitPatch")
|
|
{
|
|
ResetPatcher();
|
|
|
|
// Note SrcPackageObjectPath is NOT being redirected to DstPackageObjectPath (that mapping
|
|
// provided to the patcher explicitly already)
|
|
FSoftObjectPath NameToRedirect(SrcPackageObjectPath);
|
|
GRedirectCollector.AddAssetPathRedirection(NameToRedirect, RedirectedSoftObjectPath);
|
|
ON_SCOPE_EXIT
|
|
{
|
|
GRedirectCollector.RemoveAssetPathRedirection(NameToRedirect);
|
|
};
|
|
|
|
// Even though we have a redirector, we also have a mapping specified to the patcher.
|
|
// We give the patcher priority in such cases
|
|
FSoftObjectPath Actual = NameToRedirect;
|
|
FSoftObjectPath Expected(DstPackageObjectPath);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("SoftObjectPath with sub-object path patching"), Actual, Expected);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
Patcher.PatchNameTable();
|
|
CHECK_EQUALS(TEXT("SoftObject patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
}
|
|
#endif
|
|
}
|
|
|
|
SECTION("DoPatch(FTopLevelAssetPath)")
|
|
{
|
|
{
|
|
ResetPatcher();
|
|
|
|
FTopLevelAssetPath Actual(SrcPackagePath, SrcAssetFName);
|
|
FTopLevelAssetPath Expected(DstPackagePath, DstAssetFName);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(FName,FName) patching"), Actual, Expected);
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(FName,FName) patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(FName,FName) patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
Patcher.PatchNameTable();
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(FName,FName) patching updates NameTable entry"), Patcher.NameTable[0], DstPackagePathFName);
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(FName,FName) patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
}
|
|
|
|
{
|
|
ResetPatcher();
|
|
|
|
FTopLevelAssetPath Actual(SrcPackageObjectPath);
|
|
FTopLevelAssetPath Expected(DstPackageObjectPath);
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(string) patching"), Actual, Expected);
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(string) patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(string) patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
Patcher.PatchNameTable();
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(string) patching updates NameTable entry"), Patcher.NameTable[0], DstPackagePathFName);
|
|
CHECK_EQUALS(TEXT("TopLevelAssetPatch(string) patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
}
|
|
}
|
|
|
|
SECTION("DoPatch(FGatherableTextData)")
|
|
{
|
|
{
|
|
ResetPatcher();
|
|
|
|
FGatherableTextData Actual;
|
|
Actual.NamespaceName = SrcPackagePath;
|
|
Actual.SourceData.SourceString = SrcPackagePath;
|
|
FTextSourceSiteContext SrcSiteContext;
|
|
SrcSiteContext.KeyName = SrcPackagePath;
|
|
SrcSiteContext.SiteDescription = SrcPackagePath;
|
|
Actual.SourceSiteContexts.Add(SrcSiteContext);
|
|
|
|
FGatherableTextData Expected = Actual;
|
|
Expected.SourceSiteContexts = TArray<FTextSourceSiteContext>();
|
|
FTextSourceSiteContext DstSiteContext;
|
|
DstSiteContext.KeyName = SrcPackagePath;
|
|
DstSiteContext.SiteDescription = DstPackagePath;
|
|
Expected.SourceSiteContexts.Add(DstSiteContext);
|
|
|
|
CHECK(Patcher.DoPatch(Actual));
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't update NamespaceName"), Actual.NamespaceName, Expected.NamespaceName);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't update SourceData.SourceString"), Actual.SourceData.SourceString, Expected.SourceData.SourceString);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't update SourceSiteContexts[].KeyName"), Actual.SourceSiteContexts[0].KeyName, Expected.SourceSiteContexts[0].KeyName);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching does update SourceData.SourceString[].SiteDescription"), Actual.SourceSiteContexts[0].SiteDescription, Expected.SourceSiteContexts[0].SiteDescription);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't implicitly update the NameTable"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't implicitly update the NameTable"), Patcher.NameTable[1], SrcAssetFName);
|
|
Patcher.PatchNameTable();
|
|
// FGatherableTexData doesn't contain FNames so we shouldn't have updated the NameTable at all
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't implicitly update the PackageFileSummary"), Patcher.Summary.NameCount, OriginalNameTableCount);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching updates NameTable entry"), Patcher.NameTable[0], SrcPackagePathFName);
|
|
CHECK_EQUALS(TEXT("FGatherableTextData patching doesn't implicitly update the NameTable"), Patcher.NameTable[1], SrcAssetFName);
|
|
}
|
|
}
|
|
|
|
// Import Test Breakdown
|
|
//
|
|
// Assume that SrcEngineModule and DstEngineModule both exist and types are moving between them and/or possibly being renamed while moving
|
|
//
|
|
// Coreredirects are added to make the following transformations:
|
|
//
|
|
// /Engine/SrcClassPackage -> /Engine/DstClassPackage
|
|
// /Engine/SrcClassPackage.SrcClass -> /Engine/DstClassPackage.DstClass
|
|
// /Script/SrcEngineModule.SrcTypeA -> /Script/DstEngineModule.DstTypeA
|
|
// /Script/SrcEngineModule.SrcTypeA.SrcPropertyToChangeA -> /Script/DstEngineModule.DstTypeA.DstPropertyToChangeA
|
|
// /Script/SrcEngineModule.SrcTypeA.MovedButNotRenamedProperty -> /Script/DstEngineModule.DstTypeA.MovedButNotRenamedProperty
|
|
// /Script/SrcEngineModule.OnlySubTypeChanged.SrcPropertyToChangeB -> /Script/SrcEngineModule.OnlySubTypeChanged.DstPropertyToChangeB
|
|
// /Script/SrcEngineModule.SrcTypeB -> /Script/SrcEngineModule.DstTypeB
|
|
// /Script/SrcEngineModule.SrcTypeB.MovedButNotRenamedProperty -> /Script/SrcEngineModule.DstTypeB.MovedButNotRenamedProperty
|
|
// /Script/SrcEngineModule.SrcTypeB.MovedToNewOuter -> /Script/SrcEngineModule.NewOuter.MovedToNewOuter
|
|
// /Script/SrcEngineModule.MovedButNotRenamedType -> /Script/DstEngineModule.MovedButNotRenamedType
|
|
// /Script/SrcEngineModule.MovedButNotRenamedType.MovedButNotRenamedProperty -> /Script/DstEngineModule.MovedButNotRenamedType.MovedButNotRenamedProperty
|
|
// /Module/_Verse/VerseAsset -> /Module/_Verse
|
|
// /Module/_Verse/VerseAsset.some_verse_class -> /Module/_Verse.VerseAsset-some_verse_class
|
|
//
|
|
// Our ImportTable will be transformed from the left side to the right where each line is its own ImportEntry outered to the import "up and to the left" of it:
|
|
//
|
|
// /Script/SrcEngineModule /Script/SrcEngineModule
|
|
// .SrcTypeA .OnlySubTypeChanged
|
|
// .SrcPropertyToChangeA .DstPropertyToChangeB
|
|
// .MovedButNotRenamedProperty .UnchangedProperty
|
|
// .OnlySubTypeChanged .DstTypeB
|
|
// .SrcPropertyToChangeB .MovedButNotRenamedProperty
|
|
// .UnchangedProperty --> .NewOuter
|
|
// .SrcTypeB .MovedToNewOuter
|
|
// .MovedButNotRenamedProperty /Script/DstEngineModule
|
|
// .MovedToNewOuter .DstTypeA
|
|
// .MovedButNotRenamedType .DstPropertyToChangeA
|
|
// .MovedButNotRenamedProperty .MovedButNotRenamedProperty
|
|
// /Module/_Verse/VerseAsset .MovedButNotRenamedType
|
|
// .some_verse_class .MovedButNotRenamedProperty
|
|
// .__verse_0x7A8CDEBC_VerseObject1 /Module/_Verse
|
|
// .__verse_0x5614AC82_VerseObject2 .VerseAsset-some_verse_class
|
|
// .__verse_0x7A8CDEBC_VerseObject1
|
|
// .__verse_0x5614AC82_VerseObject2
|
|
SECTION("FObjectResource Patching")
|
|
{
|
|
SECTION("Redirect object to new package keeps the original package name if still in use")
|
|
{
|
|
auto CheckFNames = [&Patcher](FName Expected, FName Actual)
|
|
{
|
|
CHECK(Expected == Actual);
|
|
// It's fine to compare Expected == Actual when None but the NameTable
|
|
// should not contain None, so don't look for it
|
|
if (!Actual.IsNone())
|
|
{
|
|
CHECK(Patcher.NameTable.Contains(Actual));
|
|
}
|
|
};
|
|
|
|
ResetPatcher();
|
|
TArray<FCoreRedirect> ImportTableRedirects =
|
|
{
|
|
{ ECoreRedirectFlags::Type_Package, TEXT("/Engine/SrcClassPackage"), TEXT("/Engine/DstClassPackage")},
|
|
{ ECoreRedirectFlags::Type_Class, TEXT("/Engine/SrcClassPackage.SrcClass"), TEXT("/Engine/DstClassPackage.DstClass")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.SrcTypeA"), TEXT("/Script/DstEngineModule.DstTypeA")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.SrcTypeA.SrcPropertyToChangeA"), TEXT("/Script/DstEngineModule.DstTypeA.DstPropertyToChangeA")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.SrcTypeA.MovedButNotRenamedProperty"), TEXT("/Script/DstEngineModule.DstTypeA.MovedButNotRenamedProperty")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.OnlySubTypeChanged.SrcPropertyToChangeB"), TEXT("/Script/SrcEngineModule.OnlySubTypeChanged.DstPropertyToChangeB")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.SrcTypeB"), TEXT("/Script/SrcEngineModule.DstTypeB")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.SrcTypeB.MovedButNotRenamedProperty"), TEXT("/Script/SrcEngineModule.DstTypeB.MovedButNotRenamedProperty")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.SrcTypeB.MovedToNewOuter"), TEXT("/Script/SrcEngineModule.NewOuter.MovedToNewOuter")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.MovedButNotRenamedType"), TEXT("/Script/DstEngineModule.MovedButNotRenamedType")},
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Script/SrcEngineModule.MovedButNotRenamedType.MovedButNotRenamedProperty"), TEXT("/Script/DstEngineModule.MovedButNotRenamedType.MovedButNotRenamedProperty")},
|
|
{ ECoreRedirectFlags::Type_Package, TEXT("/Module/_Verse/VerseAsset"), TEXT("/Module/_Verse") },
|
|
{ ECoreRedirectFlags::Type_Object, TEXT("/Module/_Verse/VerseAsset.some_verse_class"), TEXT("/Module/_Verse.VerseAsset-some_verse_class") }
|
|
};
|
|
CHECK(FCoreRedirects::AddRedirectList(ImportTableRedirects, TEXT("Asset Header Patcher Tests - FObjectResource Patching")));
|
|
|
|
// Confirm the initial state before patching is what we expect
|
|
check(Patcher.ImportTable.Num() <= ImportTestCases.Num());
|
|
for (int32 i = 0; i < Patcher.ImportTable.Num(); ++i)
|
|
{
|
|
const FObjectImport& Expected = ImportTestCases[i].Src;
|
|
const FObjectImport& Actual = Patcher.ImportTable[i];
|
|
|
|
CheckFNames(Expected.ObjectName, Actual.ObjectName);
|
|
CHECK(Expected.OuterIndex == Actual.OuterIndex);
|
|
CheckFNames(Expected.ClassName, Actual.ClassName);
|
|
CheckFNames(Expected.ClassPackage, Actual.ClassPackage);
|
|
#if WITH_EDITORONLY_DATA
|
|
//CheckFNames(Expected.PackageName, Actual.PackageName);
|
|
#endif
|
|
}
|
|
check(Patcher.ExportTable.Num() <= ExportTestCases.Num());
|
|
for (int32 i = 0; i < Patcher.ExportTable.Num(); ++i)
|
|
{
|
|
const FObjectExport& Expected = ExportTestCases[i].Src;
|
|
const FObjectExport& Actual = Patcher.ExportTable[i];
|
|
|
|
CheckFNames(Expected.ObjectName, Actual.ObjectName);
|
|
CHECK(Expected.OuterIndex == Actual.OuterIndex);
|
|
#if WITH_EDITORONLY_DATA
|
|
CheckFNames(Expected.OldClassName, Actual.OldClassName);
|
|
CheckFNames(Actual.OldClassName, NAME_None);
|
|
#endif
|
|
}
|
|
|
|
// Perform patching
|
|
|
|
TArray<FAssetHeaderPatcherInner::FExportPatch> ExportPatches;
|
|
int32 NewImportCount = 0;
|
|
TArray<FAssetHeaderPatcherInner::FImportPatch> ImportPatches;
|
|
Patcher.GetExportTablePatches(ExportPatches);
|
|
CHECK(!ExportPatches.IsEmpty());
|
|
FAssetHeaderPatcher::EResult Result = Patcher.GetImportTablePatches(ImportPatches, NewImportCount);
|
|
CHECK(Result == FAssetHeaderPatcher::EResult::Success);
|
|
CHECK(!ExportPatches.IsEmpty());
|
|
Patcher.PatchExportAndImportTables(ExportPatches, ImportPatches, NewImportCount);
|
|
Patcher.PatchNameTable();
|
|
|
|
// Confirm the patched state is what is expected
|
|
CHECK(Patcher.ImportTable.Num() == ImportTestCases.Num());
|
|
for (int32 i = 0; i < ImportTestCases.Num(); ++i)
|
|
{
|
|
const FObjectImport& Expected = ImportTestCases[i].Dst;
|
|
const FObjectImport& Actual = Patcher.ImportTable[i];
|
|
|
|
CheckFNames(Expected.ObjectName, Actual.ObjectName);
|
|
CHECK(Expected.OuterIndex == Actual.OuterIndex);
|
|
if (ImportTestCases[i].bExistingImport)
|
|
{
|
|
CheckFNames(Expected.ClassName, Actual.ClassName);
|
|
CheckFNames(Expected.ClassPackage, Actual.ClassPackage);
|
|
#if WITH_EDITORONLY_DATA
|
|
CheckFNames(Expected.PackageName, Actual.PackageName);
|
|
CheckFNames(Expected.OldClassName, Actual.OldClassName);
|
|
CheckFNames(Actual.OldClassName, NAME_None);
|
|
#endif
|
|
}
|
|
else
|
|
{
|
|
// For new imports created by the patcher, we don't yet have a contract for what they
|
|
// should report for Class of the import and external packagename, because it would have
|
|
// to read that out of the target packages.
|
|
}
|
|
}
|
|
|
|
CHECK(Patcher.ExportTable.Num() == ExportTestCases.Num());
|
|
for (int32 i = 0; i < Patcher.ExportTable.Num(); ++i)
|
|
{
|
|
const FObjectExport& Expected = ExportTestCases[i].Dst;
|
|
const FObjectExport& Actual = Patcher.ExportTable[i];
|
|
|
|
CheckFNames(Expected.ObjectName, Actual.ObjectName);
|
|
CHECK(Expected.OuterIndex == Actual.OuterIndex);
|
|
#if WITH_EDITORONLY_DATA
|
|
CheckFNames(Expected.OldClassName, Actual.OldClassName);
|
|
CheckFNames(Actual.OldClassName, NAME_None);
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#endif // WITH_TESTS
|