Files
UnrealEngine/Engine/Plugins/Mutable/Source/CustomizableObjectEditor/Private/MuCOE/CustomizableObjectCookPackageSplitter.cpp
2025-05-18 13:04:45 +08:00

441 lines
16 KiB
C++

// 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<FString, MutablePrivate::FMutableCachedPlatformData>& CachePlatforms = Object.GetPrivate()->CachedPlatformsData;
for (const TPair<FString, MutablePrivate::FMutableCachedPlatformData>& 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<FCustomizableObjectStreamedResourceData>& 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<UObject>(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<ICookPackageSplitter::FGeneratedPackage>& 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<UCustomizableObject>(SplitData);
if (!Object->IsChildObject())
{
if(const UModelResources* ModelResources = FindModelResources(*Object))
{
return ModelResources->StreamedResourceData.Num() || ModelResources->StreamedExtensionData.Num();
}
}
return false;
}
TArray<ICookPackageSplitter::FGeneratedPackage> FCustomizableObjectCookPackageSplitter::GetGenerateList(
const UPackage* OwnerPackage,
const UObject* OwnerObject)
{
// Keep a strong reference to the CO.
StrongObject.Reset(OwnerObject);
UCustomizableObject* Object = const_cast<UCustomizableObject*>(CastChecked<UCustomizableObject>(OwnerObject));
// All platforms should have the same resources
const UModelResources* ModelResources = FindModelResources(*Object);
check(ModelResources);
TArray<ICookPackageSplitter::FGeneratedPackage> 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<ICookPackageSplitter::FGeneratedPackageForPopulate>& PlaceholderPackages = PopulateContext.GetGeneratedPackages();
const auto& PreSavePackage = [] (const ICookPackageSplitter::FGeneratedPackageForPopulate& GeneratedPackage,
TArray<FCustomizableObjectStreamedResourceData>& 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<UCustomizableObject>(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<FString, MutablePrivate::FMutableCachedPlatformData>& CachePlatforms = Object->GetPrivate()->CachedPlatformsData;
for (const TPair<FString, MutablePrivate::FMutableCachedPlatformData>& 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<UCustomizableObject>(PopulateContext.GetOwnerObject());
UModelResources* ModelResources = FindModelResources(*Object);
if (!ModelResources)
{
UE_LOG(LogMutable, Warning, TEXT("Couldn't find ModelResources. CO %s"), *GetNameSafe(Object));
return;
}
TArray<FCustomizableObjectStreamedResourceData> 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<UCustomizableObject>(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<UCustomizableObject>(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();
}