You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
o3de/Code/Framework/AzToolsFramework/AzToolsFramework/AssetBundle/AssetBundleComponent.cpp

761 lines
33 KiB
C++

/*
* Copyright (c) Contributors to the Open 3D Engine Project.
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
*
* SPDX-License-Identifier: Apache-2.0 OR MIT
*
*/
#include <AzCore/Asset/AssetManagerBus.h>
#include <AzCore/Component/ComponentApplicationBus.h>
#include <AzCore/Debug/Trace.h>
#include <AzCore/IO/FileIO.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/Serialization/Utils.h>
#include <AzCore/std/parallel/thread.h>
#include <AzCore/Utils/Utils.h>
#include <AzFramework/Asset/AssetBundleManifest.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/API/ApplicationAPI.h>
#include <AzToolsFramework/AssetBundle/AssetBundleComponent.h>
#include <AzToolsFramework/Archive/ArchiveAPI.h>
#include <AzToolsFramework/Asset/AssetSeedManager.h>
#include <AzToolsFramework/AssetCatalog/PlatformAddressedAssetCatalog.h>
#include <AzToolsFramework/AssetCatalog/PlatformAddressedAssetCatalogBus.h>
namespace AzToolsFramework
{
const char* logWindowName = "AssetBundle";
const char tempBundleFileSuffix[] = "_temp";
const int NumOfBytesInMB = 1024 * 1024;
const int ManifestFileSizeBufferInBytes = 10 * 1024; // 10 KB
const float AssetCatalogFileSizeBufferPercentage = 1.0f;
using AssetCatalogRequestBus = AZ::Data::AssetCatalogRequestBus;
const char AssetBundleComponent::DeltaCatalogName[] = "DeltaCatalog.xml";
constexpr int InjectFileRetryCount = 4;
bool MaxSizeExceeded(AZ::u64 totalFileSize, AZ::u64 bundleSize, AZ::u64 assetCatalogFileSizeBuffer, AZ::u64 maxSizeInBytes)
{
return ((totalFileSize + bundleSize + assetCatalogFileSizeBuffer + ManifestFileSizeBufferInBytes) > maxSizeInBytes);
}
bool MakePath(const AZStd::string& directory)
{
if (!AZ::IO::FileIOBase::GetInstance()->Exists(directory.c_str()))
{
auto result = AZ::IO::FileIOBase::GetInstance()->CreatePath(directory.c_str());
if (!result)
{
AZ_Error(logWindowName, false, "Path creation failed. Input path: %s \n", directory.c_str());
return false;
}
}
return true;
}
//! This helper class can be used to create a temp folder from a filename.
//! It strips the extension and than adds _temp token to the name and tries to create that directory on disk.
struct TemporaryDir
{
explicit TemporaryDir(AZStd::string filePath)
{
AzFramework::StringFunc::Path::StripExtension(filePath);
filePath += "_temp";
if (MakePath(filePath))
{
m_tempFolderPath = filePath;
m_result = true;
}
}
AZ_DISABLE_COPY_MOVE(TemporaryDir)
~TemporaryDir()
{
if (m_result)
{
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
fileIO->DestroyPath(m_tempFolderPath.c_str());
}
}
AZStd::string m_tempFolderPath;
bool m_result = false;
};
void AssetBundleComponent::Reflect(AZ::ReflectContext* context)
{
if (AZ::SerializeContext* serializeContext = azrtti_cast<AZ::SerializeContext*>(context))
{
serializeContext->Class<AssetBundleComponent, AZ::Component>();
}
}
void AssetBundleComponent::Activate()
{
AssetBundleCommands::Bus::Handler::BusConnect();
}
void AssetBundleComponent::Deactivate()
{
AssetBundleCommands::Bus::Handler::BusDisconnect();
}
bool AssetBundleComponent::CreateDeltaCatalog(const AZStd::string& sourcePak, bool regenerate)
{
AZStd::string normalizedSourcePakPath = sourcePak;
AzFramework::StringFunc::Path::Normalize(normalizedSourcePakPath);
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
if (!fileIO)
{
AZ_Error("AssetBundle", false, "No FileIOBase Instance");
return false;
}
if (!fileIO->Exists(normalizedSourcePakPath.c_str()))
{
AZ_Error("AssetBundle", false, "Invalid Arg: Source Pak does not exist at \"%s\".", normalizedSourcePakPath.c_str());
return false;
}
AZStd::string outCatalogPath;
// Scoping the fixed string to the if block to prevent the 1024 byte stack object from materializing longer than needed in this function
if(char resolvedPath[AZ_MAX_PATH_LEN]; fileIO->ResolvePath(DeltaCatalogName, resolvedPath, AZ_ARRAY_SIZE(resolvedPath)))
{
outCatalogPath = resolvedPath;
}
AZ_TracePrintf(logWindowName, "Gathering file entries in source pak file \"%s\".\n", sourcePak.c_str());
bool result = false;
AZStd::vector<AZStd::string> fileEntries;
ArchiveCommandsBus::BroadcastResult(result, &AzToolsFramework::ArchiveCommandsBus::Events::ListFilesInArchive, normalizedSourcePakPath, fileEntries);
// This ebus currently always returns false as the result, as it is believed that the 7z process is
// being terminated by the user instead of ending gracefully. Check against an empty fileList instead
// as a result.
if (fileEntries.empty())
{
AZ_Error(logWindowName, false, "Failed to read archive \"%s\" for file entries.", sourcePak.c_str());
return false;
}
// if this bundle already contains a manifest and should not regenerate it, then this bundle is good to go.
bool manifestExists = HasManifest(fileEntries);
if (manifestExists && !regenerate)
{
AZ_TracePrintf(logWindowName, "Skipping delta asset catalog creation for \"%s\", as it already contains a delta catalog.\n", normalizedSourcePakPath.c_str());
return true;
}
// Load the manifest if it exists, and set the output path to the catalog in the manifest
AzFramework::AssetBundleManifest* manifest = nullptr;
if (manifestExists)
{
manifest = GetManifestFromBundle(normalizedSourcePakPath);
if (!manifest)
{
return false;
}
outCatalogPath = manifest->GetCatalogName();
}
// remove non-asset entries from this file list (including the source pak itself)
AZ_TracePrintf(logWindowName, "Removing known non-assets from the gathered list of file entries.\n");
if (!RemoveNonAssetFileEntries(fileEntries, normalizedSourcePakPath, manifest))
{
return false;
}
// create the new asset registry containing these files and save the catalog to a file here.
AZ_TracePrintf(logWindowName, "Creating new asset catalog file \"%s\" containing information for assets in source pak.\n", outCatalogPath.c_str());
bool catalogSaved = false;
AssetCatalogRequestBus::BroadcastResult(catalogSaved, &AssetCatalogRequestBus::Events::CreateDeltaCatalog, fileEntries, outCatalogPath);
if (!catalogSaved)
{
AZ_Error(logWindowName, false, "Failed to create new asset catalog \"%s\".", outCatalogPath.c_str());
return false;
}
if (!InjectFile(outCatalogPath, normalizedSourcePakPath))
{
return false;
}
// clean up the file that was created.
if (!fileIO->Remove(outCatalogPath.c_str()))
{
AZ_Warning(logWindowName, false, "Failed to clean up catalog artifact at %s.", outCatalogPath.c_str());
}
// if the manifest already exists, we don't need to modify it, so this bundle is set.
if (manifest)
{
return true;
}
// create the new bundle manifest and save it to a file here.
AZ_TracePrintf(logWindowName, "Creating new asset bundle manifest file \"%s\" for source pak \"%s\".\n", AzFramework::AssetBundleManifest::s_manifestFileName, sourcePak.c_str());
bool manifestSaved = false;
AZStd::string manifestDirectory;
AzFramework::StringFunc::Path::GetFullPath(sourcePak.c_str(), manifestDirectory);
AssetCatalogRequestBus::BroadcastResult(manifestSaved, &AssetCatalogRequestBus::Events::CreateBundleManifest, outCatalogPath,
AZStd::vector<AZStd::string>(), manifestDirectory, AzFramework::AssetBundleManifest::CurrentBundleVersion, AZStd::vector<AZ::IO::Path>{});
AZStd::string manifestPath;
AzFramework::StringFunc::Path::Join(manifestDirectory.c_str(), AzFramework::AssetBundleManifest::s_manifestFileName, manifestPath);
if (!manifestSaved)
{
AZ_Error(logWindowName, false, "Failed to create new manifest file \"%s\" for source pak \"%s\".\n", manifestPath.c_str(), sourcePak.c_str());
return false;
}
if (!InjectFile(manifestPath, sourcePak))
{
return false;
}
// clean up the file that was created.
if (!fileIO->Remove(manifestPath.c_str()))
{
AZ_Warning(logWindowName, false, "Failed to clean up manifest artifact.");
}
return true;
}
AZStd::string AssetBundleComponent::CreateAssetBundleFileName(const AZStd::string& assetBundleFilePath, int bundleIndex)
{
AZStd::string fileName;
AZStd::string extension;
AzFramework::StringFunc::AssetDatabasePath::Split(assetBundleFilePath.c_str(), nullptr, nullptr, nullptr, &fileName, &extension);
if (!bundleIndex)
{
return AZStd::string::format("%s%s", fileName.c_str(), extension.c_str());
}
return AZStd::string::format("%s__%d%s", fileName.c_str(), bundleIndex, extension.c_str());
}
bool AssetBundleComponent::CreateAssetBundleFromList(const AssetBundleSettings& assetBundleSettings, const AssetFileInfoList& assetFileInfoList)
{
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
AZ_Assert(fileIO != nullptr, "AZ::IO::FileIOBase must be ready for use.\n");
AZ::IO::Path bundleFilePath = AZ::IO::Path(AZStd::string_view{ AZ::Utils::GetEnginePath() }) / assetBundleSettings.m_bundleFilePath;
AzFramework::PlatformId platformId = static_cast<AzFramework::PlatformId>(AzFramework::PlatformHelper::GetPlatformIndexFromName(assetBundleSettings.m_platform.c_str()));
if (platformId == AzFramework::PlatformId::Invalid)
{
return false;
}
AZ::u64 maxSizeInBytes = static_cast<AZ::u64>(assetBundleSettings.m_maxBundleSizeInMB * NumOfBytesInMB);
AZ::u64 assetCatalogFileSizeBuffer = static_cast<AZ::u64>(AssetCatalogFileSizeBufferPercentage * assetBundleSettings.m_maxBundleSizeInMB * NumOfBytesInMB) / 100;
AZ::u64 bundleSize = 0;
AZ::u64 totalFileSize = 0;
int bundleIndex = 0;
AZStd::string tempBundleFilePath = bundleFilePath.Native() + "_temp";
AZStd::vector<AZStd::string> dependentBundleNames;
AZStd::vector<AZ::IO::Path> levelDirs;
AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>> bundlePathDeltaCatalogPair;
bundlePathDeltaCatalogPair.emplace_back(AZStd::make_pair(tempBundleFilePath, DeltaCatalogName));
AZStd::vector<AZStd::string> fileEntries; // this is used to add files to the archive
AZStd::vector<AZStd::string> deltaCatalogEntries; // this is used to create the delta catalog
AZStd::string bundleFolder;
AzFramework::StringFunc::Path::GetFullPath(bundleFilePath.c_str(), bundleFolder);
// Create the archive directory if it does not exist
if (!MakePath(bundleFolder))
{
return false;
}
AZStd::string deltaCatalogFilePath;
AzFramework::StringFunc::Path::ConstructFull(bundleFolder.c_str(), bundlePathDeltaCatalogPair.back().second.c_str(), deltaCatalogFilePath, true);
AZStd::string assetAlias = PlatformAddressedAssetCatalog::GetAssetRootForPlatform(platformId);
if (fileIO->Exists(bundleFilePath.c_str()))
{
// This will delete both the parent bundle as well as all the dependent bundles mentioned in the manifest file of the parent bundle.
if (!DeleteBundleFiles(bundleFilePath.Native()))
{
return false;
}
}
bool usePrefabSystemForLevels = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(
usePrefabSystemForLevels, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled);
for (const AzToolsFramework::AssetFileInfo& assetFileInfo : assetFileInfoList.m_fileInfoList)
{
AZ::u64 fileSize = 0;
AZStd::string fullAssetFilePath;
AzFramework::StringFunc::Path::Join(assetAlias.c_str(), assetFileInfo.m_assetRelativePath.c_str(), fullAssetFilePath);
if (!fileIO->Size(fullAssetFilePath.c_str(), fileSize))
{
AZ_Error(logWindowName, false, "Unable to find size of file (%s).\n", fullAssetFilePath.c_str());
return false;
}
if (fileSize > maxSizeInBytes)
{
AZ_Warning(logWindowName, false, "File (%s) size (%d) is bigger than the max bundle size (%d).\n", assetFileInfo.m_assetRelativePath.c_str(), fileSize, maxSizeInBytes);
}
if (!usePrefabSystemForLevels && (AzFramework::StringFunc::EndsWith(assetFileInfo.m_assetRelativePath, "level.pak")))
{
AZStd::string levelFolder;
AzFramework::StringFunc::Path::GetFolderPath(assetFileInfo.m_assetRelativePath.c_str(), levelFolder);
AzFramework::StringFunc::RelativePath::Normalize(levelFolder);
if (AzFramework::StringFunc::LastCharacter(levelFolder.c_str()) == AZ_CORRECT_FILESYSTEM_SEPARATOR)
{
AzFramework::StringFunc::RChop(levelFolder, 1);
}
levelDirs.emplace_back(levelFolder);
}
totalFileSize += fileSize;
if (!MaxSizeExceeded(totalFileSize, bundleSize, assetCatalogFileSizeBuffer, maxSizeInBytes))
{
// if we are here it implies that the total file size on disk is less than the max bundle size
// and therefore these files can be added together into the bundle.
fileEntries.emplace_back(assetFileInfo.m_assetRelativePath);
deltaCatalogEntries.emplace_back(assetFileInfo.m_assetRelativePath);
continue;
}
else
{
// add all files to the archive as a batch and update the bundle size
if (!InjectFiles(fileEntries, tempBundleFilePath, assetAlias.c_str()))
{
return false;
}
fileEntries.clear();
if (fileIO->Exists(tempBundleFilePath.c_str()))
{
if (!fileIO->Size(tempBundleFilePath.c_str(), bundleSize))
{
AZ_Error(logWindowName, false, "Unable to find size of archive file (%s).\n", bundleFilePath.c_str());
return false;
}
}
}
totalFileSize = fileSize;
if (MaxSizeExceeded(totalFileSize, bundleSize, assetCatalogFileSizeBuffer, maxSizeInBytes))
{
// if we are here it implies that adding file size to the remaining increases the size over the max size
// and therefore we can add the pending files and the delta catalog to the bundle
if (!AddCatalogAndFilesToBundle(deltaCatalogEntries, fileEntries, tempBundleFilePath, assetAlias.c_str(), platformId))
{
return false;
}
fileEntries.clear();
deltaCatalogEntries.clear();
bundleSize = 0;
int numOfTries = 20;
AZStd::string dependentBundleFileName;
do
{
// we need to find a bundle which does not exist on disk;
bundleIndex++;
numOfTries--;
dependentBundleFileName = CreateAssetBundleFileName(bundleFilePath.Native(), bundleIndex);
AzFramework::StringFunc::Path::ReplaceFullName(tempBundleFilePath, (dependentBundleFileName + tempBundleFileSuffix).c_str());
} while (numOfTries && fileIO->Exists(tempBundleFilePath.c_str()));
if (!numOfTries)
{
AZ_Error(logWindowName, false, "Unable to find a unique name for the archive in the directory (%s).\n", bundleFolder.c_str());
return false;
}
dependentBundleNames.emplace_back(dependentBundleFileName);
AZStd::string currentDeltaCatalogName = DeltaCatalogName;
AzFramework::StringFunc::Path::ConstructFull(bundleFolder.c_str(), currentDeltaCatalogName.c_str(), deltaCatalogFilePath, true);
bundlePathDeltaCatalogPair.emplace_back(AZStd::make_pair(tempBundleFilePath, currentDeltaCatalogName));
}
fileEntries.emplace_back(assetFileInfo.m_assetRelativePath);
deltaCatalogEntries.emplace_back(assetFileInfo.m_assetRelativePath);
}
if (!AddCatalogAndFilesToBundle(deltaCatalogEntries, fileEntries, tempBundleFilePath, assetAlias.c_str(), platformId))
{
return false;
}
// Create and add manifest files for all the bundles
if (!AddManifestFileToBundles(bundlePathDeltaCatalogPair, dependentBundleNames, bundleFolder, assetBundleSettings, levelDirs))
{
return false;
}
// Rename all the temp files to the actual bundle names
for (int idx = 0; idx < bundlePathDeltaCatalogPair.size(); ++idx)
{
AZStd::string destinationBundleFullPath = bundlePathDeltaCatalogPair[idx].first.c_str();
if (destinationBundleFullPath.ends_with(tempBundleFileSuffix))
{
destinationBundleFullPath = destinationBundleFullPath.substr(0, destinationBundleFullPath.length() - strlen(tempBundleFileSuffix));
int numRetries = 3;
while(!fileIO->Rename(bundlePathDeltaCatalogPair[idx].first.c_str(), destinationBundleFullPath.c_str()))
{
--numRetries;
AZ_Error(logWindowName, false, "Failed to rename temporary bundle file (%s) to (%s)%s", bundlePathDeltaCatalogPair[idx].first.c_str(), destinationBundleFullPath.c_str(), numRetries ? " Retrying.." : "");
if (!numRetries)
{
return false;
}
constexpr auto SleepDuration = AZStd::chrono::seconds(1);
AZStd::this_thread::sleep_for(SleepDuration);
}
}
}
return true;
}
bool AssetBundleComponent::CreateAssetBundle(const AssetBundleSettings& assetBundleSettings)
{
if (assetBundleSettings.m_assetFileInfoListPath.empty())
{
AZ_Error(logWindowName, false, "AssetFileInfo List path is empty. \n");
return false;
}
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
AZ::IO::Path assetFileInfoListPath = AZ::IO::Path{ AZStd::string_view{AZ::Utils::GetEnginePath()} } / assetBundleSettings.m_assetFileInfoListPath;
if (!fileIO->Exists(assetFileInfoListPath.c_str()))
{
AZ_Error(logWindowName, false, "AssetFileInfoList file (%s) does not exist. \n", assetFileInfoListPath.c_str());
return false;
}
AssetFileInfoList assetFileInfoList;
if (!AZ::Utils::LoadObjectFromFileInPlace(assetFileInfoListPath.c_str(), assetFileInfoList))
{
AZ_Error(logWindowName, false, "Failed to deserialize file (%s).\n", assetFileInfoListPath.c_str());
return false;
}
return CreateAssetBundleFromList(assetBundleSettings, assetFileInfoList);
}
bool AssetBundleComponent::AddCatalogAndFilesToBundle(const AZStd::vector<AZStd::string>& deltaCatalogEntries, const AZStd::vector<AZStd::string>& fileEntries, const AZStd::string& bundleFilePath, const char* assetAlias, const AzFramework::PlatformId& platformId)
{
AZStd::string bundleFolder;
AzFramework::StringFunc::Path::GetFullPath(bundleFilePath.c_str(), bundleFolder);
if (!MakePath(bundleFolder))
{
return false;
}
if (fileEntries.size())
{
if (!InjectFiles(fileEntries, bundleFilePath, assetAlias))
{
return false;
}
}
if(deltaCatalogEntries.size())
{
TemporaryDir tempDir(bundleFilePath);
if (!tempDir.m_result)
{
return false;
}
AZStd::string tempDeltaCatalogFile;
AzFramework::StringFunc::Path::Join(tempDir.m_tempFolderPath.c_str(), DeltaCatalogName, tempDeltaCatalogFile);
// add the delta catalog to the bundle
bool success = false;
AssetCatalog::PlatformAddressedAssetCatalogRequestBus::EventResult(success, platformId, &AssetCatalog::PlatformAddressedAssetCatalogRequestBus::Events::CreateDeltaCatalog, deltaCatalogEntries, tempDeltaCatalogFile.c_str());
if (!success)
{
AZ_Error(logWindowName, false, "Failed to create the delta catalog file (%s).\n", tempDeltaCatalogFile.c_str());
return false;
}
if (!InjectFile(DeltaCatalogName, bundleFilePath, tempDir.m_tempFolderPath.c_str()))
{
AZ_Error(logWindowName, false, "Failed to add the delta catalog file (%s) in the bundle (%s).\n", tempDeltaCatalogFile.c_str(), bundleFilePath.c_str());
return false;
}
}
return true;
}
bool AssetBundleComponent::AddManifestFileToBundles(const AZStd::vector<AZStd::pair<AZStd::string, AZStd::string>>& bundlePathDeltaCatalogPair, const AZStd::vector<AZStd::string>& dependentBundleNames, const AZStd::string& bundleFolder, const AzToolsFramework::AssetBundleSettings& assetBundleSettings, const AZStd::vector<AZ::IO::Path>& levelDirs)
{
if (!MakePath(bundleFolder))
{
return false;
}
if (bundlePathDeltaCatalogPair.empty())
{
AZ_Warning(logWindowName, false, "AddManifestFilesToBundle called with no bundle paths provided. Cannot add manifest file.");
return false;
}
TemporaryDir tempDir(bundlePathDeltaCatalogPair[0].first);
if (!tempDir.m_result)
{
return false;
}
AZStd::string bundleManifestPath;
AzFramework::StringFunc::Path::ConstructFull(tempDir.m_tempFolderPath.c_str(), AzFramework::AssetBundleManifest::s_manifestFileName, bundleManifestPath, true);
for (int idx = 0; idx < bundlePathDeltaCatalogPair.size(); idx++)
{
AZStd::vector<AZStd::string> bundleNameList;
if (!idx)
{
// The manifest file of the base bundle is special and contains the names of all the rollover bundle in it.
bundleNameList = dependentBundleNames;
}
bool manifestSaved = false;
AssetCatalogRequestBus::BroadcastResult(manifestSaved, &AssetCatalogRequestBus::Events::CreateBundleManifest, bundlePathDeltaCatalogPair[idx].second, bundleNameList, tempDir.m_tempFolderPath, assetBundleSettings.m_bundleVersion, levelDirs);
if (!manifestSaved)
{
AZ_Error(logWindowName, false, "Failed to create manifest file (%s) for the bundle (%s).\n", AzFramework::AssetBundleManifest::s_manifestFileName, bundlePathDeltaCatalogPair[idx].first.c_str());
return false;
}
AZStd::string bundleManifestFileName;
AzFramework::StringFunc::Path::GetFullFileName(bundleManifestPath.c_str(), bundleManifestFileName);
if (!InjectFile(bundleManifestFileName, bundlePathDeltaCatalogPair[idx].first, tempDir.m_tempFolderPath.c_str()))
{
AZ_Error(logWindowName, false, "Failed to add manifest file (%s) in the bundle (%s).\n", bundleManifestPath.c_str(), bundlePathDeltaCatalogPair[idx].first.c_str());
return false;
}
}
return true;
}
bool AssetBundleComponent::DeleteBundleFiles(const AZStd::string& assetBundleFilePath)
{
AZ::IO::FileIOBase* fileIO = AZ::IO::FileIOBase::GetInstance();
AZStd::string bundleFolder;
AzFramework::StringFunc::Path::GetFullPath(assetBundleFilePath.c_str(), bundleFolder);
AZStd::unique_ptr<AzFramework::AssetBundleManifest> manifest(GetManifestFromBundle(assetBundleFilePath));
if (manifest)
{
for (const AZStd::string& filename : manifest->GetDependentBundleNames())
{
AZStd::string dependentBundlesFilePath;
AzFramework::StringFunc::Path::ConstructFull(bundleFolder.c_str(), filename.c_str(), dependentBundlesFilePath, true);
if (!fileIO->Remove(dependentBundlesFilePath.c_str()))
{
AZ_Warning(logWindowName, false, "Failed to delete dependent bundle file (%s)", dependentBundlesFilePath.c_str());
}
}
}
// remove the parent bundle
if (!fileIO->Remove(assetBundleFilePath.c_str()))
{
AZ_Error(logWindowName, false, "Failed to delete bundle file (%s)", assetBundleFilePath.c_str());
return false;
}
return true;
}
bool AssetBundleComponent::InjectFile(const AZStd::string& filePath, const AZStd::string& archiveFilePath, const char* workingDirectory)
{
AZ_TracePrintf(logWindowName, "Injecting file (%s) into bundle (%s).\n", filePath.c_str(), archiveFilePath.c_str());
bool fileAddedToArchive = false;
std::future<bool> fileAdded;
int retryCount = InjectFileRetryCount;
while (!fileAddedToArchive && retryCount)
{
ArchiveCommandsBus::BroadcastResult(fileAdded, &AzToolsFramework::ArchiveCommandsBus::Events::AddFileToArchive, archiveFilePath, workingDirectory, filePath);
--retryCount;
fileAddedToArchive = fileAdded.get();
if (!fileAddedToArchive && retryCount)
{
AZ_Error(logWindowName, false, "Failed to insert file (%s) into bundle (%s). Retrying.", filePath.c_str(), archiveFilePath.c_str());
}
}
if (!fileAddedToArchive)
{
AZ_Error(logWindowName, false, "Failed to insert file (%s) into bundle (%s) after %d retries.", filePath.c_str(), archiveFilePath.c_str(), InjectFileRetryCount);
}
return fileAddedToArchive;
}
bool AssetBundleComponent::InjectFile(const AZStd::string& filePath, const AZStd::string& sourcePak)
{
// When no working directory is specified, assume that the file being injected goes into the root of the archive.
// The filePath should be an absolute path, making the workingDirectory be the path leading up to the file.
AZ::IO::PathView fullFilePath{ filePath, AZ::IO::PosixPathSeparator };
AZ::IO::Path workingDir{ fullFilePath.ParentPath() };
return InjectFile(filePath, sourcePak, workingDir.c_str());
}
bool AssetBundleComponent::InjectFiles(const AZStd::vector<AZStd::string>& fileEntries, const AZStd::string& sourcePak, const char* workingDirectory)
{
if (!fileEntries.size())
{
return true;
}
AZStd::string filesStr;
for (const AZStd::string& file : fileEntries)
{
filesStr.append(AZStd::string::format("%s\n", file.c_str()));
}
TemporaryDir tempDir(sourcePak);
if (!tempDir.m_result)
{
return false;
}
// Creating a list file
AZStd::string listFilePath;
const char listFileName[] = "ListFile.txt";
AzFramework::StringFunc::Path::ConstructFull(tempDir.m_tempFolderPath.c_str(), listFileName, listFilePath, true);
{
AZ::IO::FileIOStream fileStream(listFilePath.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeText);
if (fileStream.IsOpen())
{
fileStream.Write(filesStr.size(), filesStr.data());
}
else
{
AZ_Error(logWindowName, false, "Failed to create list file (%s) for adding files in the bundle (%s).\n", listFilePath.c_str(), sourcePak.c_str());
return false;
}
}
std::future<bool> filesAdded;
AzToolsFramework::ArchiveCommandsBus::BroadcastResult(filesAdded, &AzToolsFramework::ArchiveCommands::AddFilesToArchive, sourcePak, workingDirectory, listFilePath);
bool filesAddedToArchive = filesAdded.get();
if (!filesAddedToArchive)
{
AZ_Error(logWindowName, false, "Failed to insert files into bundle (%s).\n", sourcePak.c_str());
}
return filesAddedToArchive;
}
bool AssetBundleComponent::HasManifest(const AZStd::vector<AZStd::string>& fileEntries)
{
auto itr = AZStd::find(fileEntries.begin(), fileEntries.end(), AZStd::string(AzFramework::AssetBundleManifest::s_manifestFileName));
return itr != fileEntries.end();
}
AzFramework::AssetBundleManifest* AssetBundleComponent::GetManifestFromBundle(const AZStd::string& sourcePak)
{
// open the manifest and deserialize it
bool manifestExtracted = false;
TemporaryDir tempDir(sourcePak);
if (!tempDir.m_result)
{
return nullptr;
}
AZStd::string manifestFilePath;
AzFramework::StringFunc::Path::ConstructFull(tempDir.m_tempFolderPath.c_str(), AzFramework::AssetBundleManifest::s_manifestFileName, manifestFilePath, true);
std::future<bool> extractResult;
ArchiveCommandsBus::BroadcastResult(extractResult, &ArchiveCommandsBus::Events::ExtractFile, sourcePak, AzFramework::AssetBundleManifest::s_manifestFileName, tempDir.m_tempFolderPath);
manifestExtracted = extractResult.get();
if (!manifestExtracted)
{
AZ_Error(logWindowName, false, "Failed to extract existing manifest from archive \"%s\".", sourcePak.c_str());
return nullptr;
}
// deserialize the manifest
AZ::SerializeContext* serializeContext = nullptr;
EBUS_EVENT_RESULT(serializeContext, AZ::ComponentApplicationBus, GetSerializeContext);
AZ_Assert(serializeContext, "Unable to retrieve serialize context.");
if (nullptr == serializeContext->FindClassData(AZ::AzTypeInfo<AzFramework::AssetBundleManifest>::Uuid()))
{
AzFramework::AssetBundleManifest::ReflectSerialize(serializeContext);
}
AzFramework::AssetBundleManifest* manifest = AZ::Utils::LoadObjectFromFile<AzFramework::AssetBundleManifest>(manifestFilePath, serializeContext);
if (manifest == nullptr)
{
AZ_Error(logWindowName, false, "Failed to deserialize existing manifest from archive \"%s\".", sourcePak.c_str());
}
return manifest;
}
bool AssetBundleComponent::RemoveNonAssetFileEntries(AZStd::vector<AZStd::string>& fileEntries, const AZStd::string& normalizedSourcePakPath, const AzFramework::AssetBundleManifest* manifest)
{
auto sourcePakItr = AZStd::find(fileEntries.begin(), fileEntries.end(), normalizedSourcePakPath);
if (sourcePakItr != fileEntries.end())
{
fileEntries.erase(sourcePakItr);
}
if (manifest)
{
sourcePakItr = AZStd::find(fileEntries.begin(), fileEntries.end(), AZStd::string(AzFramework::AssetBundleManifest::s_manifestFileName));
if (sourcePakItr != fileEntries.end())
{
fileEntries.erase(sourcePakItr);
}
// use the catalog name stored in the manifest to determine what to exclude and overwrite
sourcePakItr = AZStd::find(fileEntries.begin(), fileEntries.end(), manifest->GetCatalogName());
if (sourcePakItr != fileEntries.end())
{
fileEntries.erase(sourcePakItr);
}
else
{
AZ_Error(logWindowName, false, "Asset catalog \"%s\" referenced in manifest doesn't exist in bundle \"%s\".", manifest->GetCatalogName().c_str(), normalizedSourcePakPath.c_str());
return false;
}
}
return true;
}
}