// Copyright Epic Games, Inc. All Rights Reserved. #include "MuCOE/CustomizableObjectCookPackageSplitter.h" #include "Algo/Find.h" #include "MuCO/CustomizableObject.h" #include "MuCO/CustomizableObjectPrivate.h" #include "MuCO/LoadUtils.h" #include "UObject/NameTypes.h" #include "UObject/Package.h" REGISTER_COOKPACKAGE_SPLITTER(FCustomizableObjectCookPackageSplitter, UCustomizableObject); namespace { UModelResources* FindModelResources(UCustomizableObject& Object) { // All platforms should have the same resources const TMap& CachePlatforms = Object.GetPrivate()->CachedPlatformsData; for (const TPair& PlatformData : CachePlatforms) { if (PlatformData.Value.ModelResources) { return PlatformData.Value.ModelResources.Get(); } } return nullptr; } // Look up a streamed Resource Data constant by name on a Customizable Object. // // Returns nullptr if not found. FCustomizableObjectStreamedResourceData* FindStreamedResourceData( TArray& StreamedResources, const FString& ContainerName ) { return Algo::FindByPredicate( StreamedResources, [&ContainerName](const FCustomizableObjectStreamedResourceData& StreamedData) { const FSoftObjectPath& Path = StreamedData.GetPath().ToSoftObjectPath(); // ContainerName should match the last element of the path, which could be the // sub-path string or the asset name. if (Path.GetSubPathString().Len() > 0) { return Path.GetSubPathString() == ContainerName; } return Path.GetAssetName() == ContainerName; }); } enum class EMoveContainerError { None, FailedToLoadContainer, NameCollision, // Object with that name already exists in the new outer RenameFailed, }; const TCHAR* LexToString(EMoveContainerError Error) { switch (Error) { case EMoveContainerError::None: return TEXT("None"); case EMoveContainerError::FailedToLoadContainer: return TEXT("FailedToLoadContainer"); case EMoveContainerError::NameCollision: return TEXT("NameCollision"); case EMoveContainerError::RenameFailed: return TEXT("RenameFailed"); default: return TEXT("Unknown"); } } // Moves the StreamedResourceData's data container to the given Outer. EMoveContainerError MoveContainerToNewOuter( UObject* NewOuter, const FCustomizableObjectStreamedResourceData* StreamedResourceData, UCustomizableObjectResourceDataContainer*& OutContainer ) { check(StreamedResourceData); OutContainer = nullptr; UCustomizableObjectResourceDataContainer* Container = MutablePrivate::LoadObject(StreamedResourceData->GetPath()); if (!Container) { return EMoveContainerError::FailedToLoadContainer; } if (Container->GetOuter() != NewOuter) { // Ensure the target object doesn't exist if (FindObject(NewOuter, *Container->GetName())) { return EMoveContainerError::NameCollision; } // The Rename function moves the object into the given package if (!Container->Rename(nullptr, NewOuter, REN_DontCreateRedirectors)) { return EMoveContainerError::RenameFailed; } } OutContainer = Container; return EMoveContainerError::None; } void GenerateNewPackage(const FCustomizableObjectStreamedResourceData& StreamedData, const UPackage* OwnerPackage, const UObject* OwnerObject, TArray& Result) { // The StreamedData container path should be of the form // OwnerPackageName.OwnerObjectName:ContainerName const FSoftObjectPath& StreamedDataPath = StreamedData.GetPath().ToSoftObjectPath(); // Check that the StreamedData container has the OwnerObject as its Outer check(StreamedDataPath.GetWithoutSubPath() == FSoftObjectPath(OwnerObject)); // Check that the ContainerName is valid and that there isn't another Outer level between // the OwnerObject and the container. check(StreamedDataPath.GetSubPathString().Len() > 0); check(!StreamedDataPath.GetSubPathString().Contains(SUBOBJECT_DELIMITER)); ICookPackageSplitter::FGeneratedPackage& Package = Result.AddDefaulted_GetRef(); // Because of the checks above, the container name must be unique within this Customizable // Object, so it's safe to use as a package path. Package.RelativePath = StreamedDataPath.GetSubPathString(); Package.SetCreateAsMap(false); // To support iterative cooking, GenerationHash should only change when OwnerPackage // changes. // // The simplest and fastest way to do this is to set it to OwnerPackage's PackageSavedHash. { // Zero the hash, as we won't be writing all bytes of it below Package.GenerationHash.Reset(); FIoHash OwnerSavedHash = OwnerPackage->GetSavedHash(); static_assert(sizeof(Package.GenerationHash.GetBytes()) >= sizeof(OwnerSavedHash.GetBytes())); // -V568 static_assert(sizeof(Package.GenerationHash.GetBytes()) > 8); // It should be a byte array, not a pointer // -V568 static_assert(sizeof(OwnerSavedHash.GetBytes()) > 8); // It should be a byte array, not a pointer // -V568 FMemory::Memcpy(Package.GenerationHash.GetBytes(), OwnerSavedHash.GetBytes(), sizeof(OwnerSavedHash.GetBytes())); // -V568 } } } bool FCustomizableObjectCookPackageSplitter::ShouldSplit(UObject* SplitData) { UCustomizableObject* Object = CastChecked(SplitData); if (!Object->IsChildObject()) { if(const UModelResources* ModelResources = FindModelResources(*Object)) { return ModelResources->StreamedResourceData.Num() || ModelResources->StreamedExtensionData.Num(); } } return false; } TArray FCustomizableObjectCookPackageSplitter::GetGenerateList( const UPackage* OwnerPackage, const UObject* OwnerObject) { // Keep a strong reference to the CO. StrongObject.Reset(OwnerObject); UCustomizableObject* Object = const_cast(CastChecked(OwnerObject)); // All platforms should have the same resources const UModelResources* ModelResources = FindModelResources(*Object); check(ModelResources); TArray Result; // Generate a new package for each streamed Resource Data for (const FCustomizableObjectStreamedResourceData& StreamedData : ModelResources->StreamedResourceData) { GenerateNewPackage(StreamedData, OwnerPackage, OwnerObject, Result); } // Generate a new package for each streamed Extension Data for (const FCustomizableObjectStreamedResourceData& StreamedData : ModelResources->StreamedExtensionData) { GenerateNewPackage(StreamedData, OwnerPackage, OwnerObject, Result); } return Result; } bool FCustomizableObjectCookPackageSplitter::PreSaveGeneratorPackage(FPopulateContext& PopulateContext) { // The CO is just about to be saved (i.e. produce the cooked version of the asset), so this // function needs to: // // 1. Move the streamed Data out of the CO's package, so that it doesn't get saved // into the cooked package. // // 2. Remove hard references to the streamed data, so that it doesn't get loaded as soon as // the CO is loaded TConstArrayView& PlaceholderPackages = PopulateContext.GetGeneratedPackages(); const auto& PreSavePackage = [] (const ICookPackageSplitter::FGeneratedPackageForPopulate& GeneratedPackage, TArray& StreamedResources ) -> bool { FCustomizableObjectStreamedResourceData* FoundData = FindStreamedResourceData(StreamedResources, GeneratedPackage.RelativePath); if (!FoundData) { UE_LOG(LogMutable, Error, TEXT("Couldn't find streamed Resource Data container with name %s in array of %d entries"), *GeneratedPackage.RelativePath, StreamedResources.Num()); return false; } // Move the streamed data to the generated package UCustomizableObjectResourceDataContainer* Container = nullptr; EMoveContainerError Error = MoveContainerToNewOuter(GeneratedPackage.Package, FoundData, Container); if (Error != EMoveContainerError::None) { UE_LOG(LogMutable, Error, TEXT("Failed to move container %s to new outer %s - %s"), *FoundData->GetPath().ToSoftObjectPath().ToString(), *GetPathNameSafe(GeneratedPackage.Package), LexToString(Error)); return false; } return true; }; UCustomizableObject* Object = CastChecked(PopulateContext.GetOwnerObject()); UModelResources* ModelResources = FindModelResources(*Object); if (!ModelResources) { UE_LOG(LogMutable, Warning, TEXT("Couldn't find ModelResources. CO %s"), *GetNameSafe(Object)); return false; } // There should be one generated package per streamed Resource Data const int32 NumStreamedData = ModelResources->StreamedResourceData.Num(); const int32 NumStreamedExtensionData = ModelResources->StreamedExtensionData.Num(); check(NumStreamedData + NumStreamedExtensionData == PlaceholderPackages.Num()); // After the CO has been saved, the contract for ICookPackageSplitter states that we need to // restore the CO back to how it was before, so we need to save some information to help with // this. SavedContainerNames.Reset(); SavedExtensionContainerNames.Reset(); for (int32 Index = 0; Index < NumStreamedData; ++Index) { const ICookPackageSplitter::FGeneratedPackageForPopulate& GeneratedPackage = PlaceholderPackages[Index]; if (!PreSavePackage(GeneratedPackage, ModelResources->StreamedResourceData)) { return false; } SavedContainerNames.Add(GeneratedPackage.RelativePath); } for (int32 Index = 0; Index < NumStreamedExtensionData; ++Index) { const ICookPackageSplitter::FGeneratedPackageForPopulate& GeneratedPackage = PlaceholderPackages[NumStreamedData + Index]; if (!PreSavePackage(GeneratedPackage, ModelResources->StreamedExtensionData)) { return false; } SavedExtensionContainerNames.Add(GeneratedPackage.RelativePath); } // All platforms should have the same resources const TMap& CachePlatforms = Object->GetPrivate()->CachedPlatformsData; for (const TPair& PlatformData : CachePlatforms) { if (PlatformData.Value.ModelResources) { for (FCustomizableObjectStreamedResourceData& StreamedResourceData : PlatformData.Value.ModelResources->StreamedResourceData) { // Remove the hard reference and set the soft reference to the streamed data's new location StreamedResourceData.ConvertToSoftReferenceForCooking(); } for (FCustomizableObjectStreamedResourceData& StreamedExtensionData : PlatformData.Value.ModelResources->StreamedExtensionData) { // Remove the hard reference and set the soft reference to the streamed data's new location StreamedExtensionData.ConvertToSoftReferenceForCooking(); } } } return true; } void FCustomizableObjectCookPackageSplitter::PostSaveGeneratorPackage(FPopulateContext& PopulateContext) { // Move the streamed data back into the CO's package and restore the StreamedResourceData and StreamedExtensionData // array on the CO to how it was before PreSaveGeneratorPackage. UCustomizableObject* Object = CastChecked(PopulateContext.GetOwnerObject()); UModelResources* ModelResources = FindModelResources(*Object); if (!ModelResources) { UE_LOG(LogMutable, Warning, TEXT("Couldn't find ModelResources. CO %s"), *GetNameSafe(Object)); return; } TArray NewArray; NewArray.Reset(SavedContainerNames.Num()); for (const FString& ContainerName : SavedContainerNames) { FCustomizableObjectStreamedResourceData* ResourceData = FindStreamedResourceData(ModelResources->StreamedResourceData, ContainerName); if (!ResourceData) { UE_LOG(LogMutable, Error, TEXT("Couldn't find streamed Resource Data container with name %s in array of %d entries"), *ContainerName, ModelResources->StreamedResourceData.Num()); continue; } UCustomizableObjectResourceDataContainer* Container = nullptr; EMoveContainerError Error = MoveContainerToNewOuter(Object, ResourceData, Container); UE_CLOG(Error != EMoveContainerError::None, LogMutable, Warning, TEXT("Failed to move container %s back to %s - %s"), *ContainerName, *GetPathNameSafe(Object), LexToString(Error)); NewArray.Emplace(Container); } ModelResources->StreamedResourceData = NewArray; NewArray.Reset(SavedExtensionContainerNames.Num()); for (const FString& ContainerName : SavedExtensionContainerNames) { FCustomizableObjectStreamedResourceData* ResourceData = FindStreamedResourceData(ModelResources->StreamedExtensionData, ContainerName); if (!ResourceData) { UE_LOG(LogMutable, Error, TEXT("Couldn't find streamed Extension Data container with name %s in array of %d entries"), *ContainerName, ModelResources->StreamedExtensionData.Num()); continue; } UCustomizableObjectResourceDataContainer* Container = nullptr; EMoveContainerError Error = MoveContainerToNewOuter(Object, ResourceData, Container); UE_CLOG(Error != EMoveContainerError::None, LogMutable, Warning, TEXT("Failed to move container %s back to %s - %s"), *ContainerName, *GetPathNameSafe(Object), LexToString(Error)); NewArray.Emplace(Container); } ModelResources->StreamedExtensionData = NewArray; } bool FCustomizableObjectCookPackageSplitter::PopulateGeneratedPackage(FPopulateContext& PopulateContext) { // Move the container into its newly generated package const FGeneratedPackageForPopulate& GeneratedPackage = *PopulateContext.GetTargetGeneratedPackage(); UCustomizableObject* Object = CastChecked(PopulateContext.GetOwnerObject()); UModelResources* ModelResources = FindModelResources(*Object); FCustomizableObjectStreamedResourceData* ResourceData = FindStreamedResourceData(ModelResources->StreamedResourceData, GeneratedPackage.RelativePath); if (!ResourceData) { ResourceData = FindStreamedResourceData(ModelResources->StreamedExtensionData, GeneratedPackage.RelativePath); } if (!ResourceData) { UE_LOG(LogMutable, Error, TEXT("Couldn't find streamed resource Data container with name %s in arrays of %d and %d entries"), *GeneratedPackage.RelativePath, ModelResources->StreamedResourceData.Num(), ModelResources->StreamedExtensionData.Num()); return false; } // [TEMP] Loading a package referencing the CO before PostSaveGeneratedPackage is called causes a name collision. // Duplicate the object with the new outer instead of moving it until it is fixed. UObject* Container = MutablePrivate::LoadObject(ResourceData->GetPath()); EMoveContainerError Error = Container ? EMoveContainerError::None : EMoveContainerError::FailedToLoadContainer; if (Container) { Container = StaticDuplicateObject(Container, GeneratedPackage.Package); } //UCustomizableObjectResourceDataContainer* Container = nullptr; //EMoveContainerError Error = MoveContainerToNewOuter(GeneratedPackage.Package, ResourceData, Container); if (Error != EMoveContainerError::None) { UE_LOG(LogMutable, Error, TEXT("Failed to move container %s to new outer %s - %s"), *ResourceData->GetPath().ToSoftObjectPath().ToString(), *GetPathNameSafe(GeneratedPackage.Package), LexToString(Error)); return false; } PopulateContext.ReportObjectToMove(Container); return true; } void FCustomizableObjectCookPackageSplitter::PostSaveGeneratedPackage(FPopulateContext& PopulateContext) { // Now that the generated package has been saved/cooked, move the container back to the CO, so // that everything is the same as it was before cooking. const FGeneratedPackageForPopulate& GeneratedPackage = *PopulateContext.GetTargetGeneratedPackage(); UCustomizableObject* Object = CastChecked(PopulateContext.GetOwnerObject()); UModelResources* ModelResources = FindModelResources(*Object); FCustomizableObjectStreamedResourceData* ResourceData = FindStreamedResourceData(ModelResources->StreamedResourceData, GeneratedPackage.RelativePath); if (!ResourceData) { ResourceData = FindStreamedResourceData(ModelResources->StreamedExtensionData, GeneratedPackage.RelativePath); } if (!ResourceData) { UE_LOG(LogMutable, Error, TEXT("Couldn't find streamed resource Data container with name %s in arrays of %d and %d entries"), *GeneratedPackage.RelativePath, ModelResources->StreamedResourceData.Num(), ModelResources->StreamedExtensionData.Num()); return; } UCustomizableObjectResourceDataContainer* Container = nullptr; EMoveContainerError Error = MoveContainerToNewOuter(Object, ResourceData, Container); UE_CLOG(Error != EMoveContainerError::None, LogMutable, Warning, TEXT("Failed to move container %s back to %s - %s"), *ResourceData->GetPath().ToSoftObjectPath().ToString(), *GetPathNameSafe(Object), LexToString(Error)); } void FCustomizableObjectCookPackageSplitter::Teardown(ETeardown Status) { StrongObject.Reset(); }