// 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& Table, FStringView Needle) { uint32 NeedleHash = TMap::KeyFuncsType::GetKeyHash(Needle); const FString* MaybeNewItem = Table.FindByHash(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& 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& InSrcAndDstFilePaths, const TMap& 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& 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& 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> PluginExternalMappings; for (const TPair& 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& 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 Result; IAssetRegistry& Registry = *IAssetRegistry::Get(); TArray< TTuple > ToProcess; Algo::Copy(PackagePathRenameMap, ToProcess); TStringBuilder SrcDependencyBuilder; while (ToProcess.Num()) { TTuple 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 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 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* 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 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 SrcNameBuilder; TStringBuilder DstNameBuilder; for (const TTuple& 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 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& 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& 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& 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& 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(&Summary) + Offset; if (bIs64Bit) { return *reinterpret_cast(Ptr); } else { return *reinterpret_cast(Ptr); } } void PatchOffsetValue(FPackageFileSummary& Summary, int64 Value) const { intptr_t Ptr = reinterpret_cast(&Summary) + Offset; if (bIs64Bit) { int64& Dst = *reinterpret_cast(Ptr); Dst += Value; } else { int32& Dst = *reinterpret_cast(Ptr); *reinterpret_cast(Ptr) = IntCastChecked((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_vNAME), 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& 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& InStringReplacements, const TMap& 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& OutExportPatches); FAssetHeaderPatcher::EResult GetImportTablePatches(TArray& OutImportPatches, int32& OutNewImportCount); void PatchExportAndImportTables(const TArray& InExportPatches, const TArray& 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 IgnoredTags; const FString& SrcAsset; const FString& DstAsset; const TMap& StringReplacements; const TMap& StringMountPointReplacements; FArchive* DstArchive = nullptr; TUniquePtr DstArchiveOwner; TArray64 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 NameTable; TMap NameToIndexMap; TSet UnchangedNames; TMap RenameMap; TSet AddedNames; // Export/Import table TArray> ImportTablePatchedNames; TMap ImportNameToImportTableIndexLookup; TArray SoftObjectPathTable; TArray GatherableTextDataTable; TArray ImportTable; TArray ExportTable; TArray SoftPackageReferencesTable; TMap> SearchableNamesMap; TArray ThumbnailTable; // Asset registry data information struct FAssetRegistryObjectData { UE::AssetRegistry::FDeserializeObjectPackageData ObjectData; TArray TagData; }; struct FAssetRegistryData { int64 SectionSize = -1; UE::AssetRegistry::FDeserializePackageData PkgData; TArray ObjectData; int64 DependencyDataSectionSize = -1; TMap ImportIndexUsedInGame; TMap SoftPackageReferenceUsedInGame; TArray> 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 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& 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 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& 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& 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 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> 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& InExportPatches, const TArray& 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> 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 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 ExportPatches; int32 NewImportCount = 0; TArray 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& 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 Dependencies; Dependencies.Reserve(AssetRegistryData.ExtraPackageDependencies.Num()); for (const TPair& Pair : AssetRegistryData.ExtraPackageDependencies) { Dependencies.Add(Pair.Key, Pair.Value); } TArray> AddedKeys; TSet RemovedKeys; for (const TPair& 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& 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& A, const TPair& 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 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> ExtraPackageDependenciesInts; ExtraPackageDependenciesInts.Reserve(AssetRegistryData.ExtraPackageDependencies.Num()); for (const TPair& DependencyPair : AssetRegistryData.ExtraPackageDependencies) { ExtraPackageDependenciesInts.Add({ DependencyPair.Key, static_cast(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(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& 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 PackageRenameMap, bool bGatherDependentPackages = true) : FAssetHeaderPatcher::FContext(PackageRenameMap, bGatherDependentPackages) {} const TMap& GetStringReplacements() { return StringReplacements; } void GenerateRemappings() { GenerateAdditionalRemappings(); } const TArray& GetRedirects() { return Redirects; } const TArray& 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 MountPointReplacementMap = { { SrcMountName, DstMountName }, }; TMap 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 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, }, // -> /Script/DstEngineModule { .Src = FObjectImport(), .Dst = MakeImport(DstEngineModuleImportObjectName, FPackageIndex(), DstImportClassPackage, DstImportClassName, DstPackagePathFName), .bExistingImport = false, }, // -> /Script/SrcEngineModule.NewOuter { .Src = FObjectImport(), .Dst = MakeImport(NewOuterImportObjectName, FPackageIndex::FromImport(0), DstImportClassPackage, DstImportClassName, DstPackagePathFName), .bExistingImport = false, }, }; struct FExportTestCase { FObjectExport Src; FObjectExport Dst; }; TArray 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& 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 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 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 ExportPatches; int32 NewImportCount = 0; TArray 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