Files
o3de/Code/Tools/AssetProcessor/native/resourcecompiler/RCBuilder.cpp
T
Nicholas Lawson e6146b6608 A quick fix for Asset Processor automated tests (#644)
These tests broke because RC.EXE is no longer a thing.

A more comprehensive fix and cleanup needs to be performed to
properly remove references to it while still keeping the existing
tests and other things.  This just removes as little as is possible
to avoid error.
2021-05-07 11:39:07 -07:00

1161 lines
55 KiB
C++

/*
* All or portions of this file Copyright(c) Amazon.com, Inc.or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
*or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
*WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "RCBuilder.h"
#include <QElapsedTimer>
#include <QCoreApplication>
#include <AzCore/Component/Entity.h>
#include <AzCore/Serialization/SerializeContext.h>
#include <AzCore/IO/SystemFile.h>
#include <AzCore/std/parallel/atomic.h>
#include <AzCore/Utils/Utils.h>
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/Application/Application.h>
#include <AzFramework/Process/ProcessCommunicator.h>
#include <AzFramework/Process/ProcessWatcher.h>
#include <AzToolsFramework/Application/ToolsApplication.h>
#include <AssetBuilderSDK/AssetBuilderSDK.h>
#include <AssetBuilderSDK/AssetBuilderBusses.h>
#include "native/resourcecompiler/rccontroller.h"
#include "native/utilities/assetUtils.h"
#include "native/utilities/AssetBuilderInfo.h"
#include "native/utilities/CommunicatorTracePrinter.h"
#include <AssetProcessor_Traits_Platform.h>
namespace AssetProcessor
{
// Temporary solution to get around the fact that we don't have job dependencies
static AZStd::atomic_bool s_TempSolution_CopyJobsFinished(false);
static AZStd::atomic<int> s_TempSolution_CopyJobActivityCounter(0);
static void TempSolution_TouchCopyJobActivity()
{
s_TempSolution_CopyJobActivityCounter++;
s_TempSolution_CopyJobsFinished = false;
}
//! Special ini configuration keyword to mark a asset pattern for skipping
const QString ASSET_PROCESSOR_CONFIG_KEYWORD_SKIP = "skip";
//! Special ini configuration keyword to mark a asset pattern for copying
const QString ASSET_PROCESSOR_CONFIG_KEYWORD_COPY = "copy";
namespace Internal
{
void PopulateCommonDescriptorParams(AssetBuilderSDK::JobDescriptor& descriptor, const QString& platformIdentifier, const AssetPlatformSpec& platformSpec, const InternalAssetRecognizer* const recognizer)
{
descriptor.m_jobKey = recognizer->m_name.toUtf8().constData();
descriptor.SetPlatformIdentifier(platformIdentifier.toUtf8().constData());
descriptor.m_priority = recognizer->m_priority;
descriptor.m_checkExclusiveLock = recognizer->m_testLockSource;
QString extraInformationForFingerprinting;
extraInformationForFingerprinting.append(platformSpec.m_extraRCParams);
extraInformationForFingerprinting.append(recognizer->m_version);
// if we have specified the product asset type, changing it should cuase
if (!recognizer->m_productAssetType.IsNull())
{
char typeAsString[64] = { 0 };
recognizer->m_productAssetType.ToString(typeAsString, AZ_ARRAY_SIZE(typeAsString));
extraInformationForFingerprinting.append(typeAsString);
}
descriptor.m_priority = recognizer->m_priority;
descriptor.m_additionalFingerprintInfo = AZStd::string(extraInformationForFingerprinting.toUtf8().constData());
bool isCopyJob = (platformSpec.m_extraRCParams.compare(ASSET_PROCESSOR_CONFIG_KEYWORD_COPY) == 0);
// Temporary solution to get around the fact that we don't have job dependencies
if (isCopyJob)
{
TempSolution_TouchCopyJobActivity();
}
// If this is a copy job or critical is set to true in the ini file, then its a critical job
descriptor.m_critical = recognizer->m_isCritical || isCopyJob;
descriptor.m_checkServer = recognizer->m_checkServer;
// If the priority of copy job is default then we update it to 1
// This will ensure that copy jobs will be processed before other critical jobs having default priority
if (isCopyJob && recognizer->m_priority == 0)
{
descriptor.m_priority = 1;
}
}
}
// don't make this too high, its basically how slowly the app responds to a job finishing.
// this basically puts a hard cap on how many RC jobs can execute per second, since at 10ms per job (minimum), with 8 cores, thats a max
// of 800 jobs per second that can possibly run. However, the actual time it takes to launch RC.EXE is far, far longer than 10ms, so this is not a bad number for now...
const int NativeLegacyRCCompiler::s_maxSleepTime = 10;
// You have up to 60 minutes to finish processing an asset.
// This was increased from 10 to account for PVRTC compression
// taking up to an hour for large normal map textures, and should
// be reduced again once we move to the ASTC compression format, or
// find another solution to reduce processing times to be reasonable.
const unsigned int NativeLegacyRCCompiler::s_jobMaximumWaitTime = 1000 * 60 * 60;
NativeLegacyRCCompiler::Result::Result(int exitCode, bool crashed, const QString& outputDir)
: m_exitCode(exitCode)
, m_crashed(crashed)
, m_outputDir(outputDir)
{
}
NativeLegacyRCCompiler::NativeLegacyRCCompiler()
: m_resourceCompilerInitialized(false)
, m_requestedQuit(false)
{
}
bool NativeLegacyRCCompiler::Initialize()
{
this->m_resourceCompilerInitialized = true;
return true;
}
bool NativeLegacyRCCompiler::Execute(
[[maybe_unused]] const QString& inputFile,
[[maybe_unused]] const QString& watchFolder,
[[maybe_unused]] const QString& platformIdentifier,
[[maybe_unused]] const QString& params,
[[maybe_unused]] const QString& dest,
[[maybe_unused]] const AssetBuilderSDK::JobCancelListener* jobCancelListener,
[[maybe_unused]] Result& result) const
{
// running RC.EXE is deprecated.
AZ_Error("RC Builder", false, "running RC.EXE is deprecated");
return false;
}
QString NativeLegacyRCCompiler::BuildCommand(const QString& inputFile, const QString& watchFolder, const QString& platformIdentifier, const QString& params, const QString& dest)
{
QString cmdLine;
if (!dest.isEmpty())
{
QString projectPath = AssetUtilities::ComputeProjectPath();
int portNumber = 0;
ApplicationServerBus::BroadcastResult(portNumber, &ApplicationServerBus::Events::GetServerListeningPort);
AZStd::string appBranchToken;
AzFramework::ApplicationRequests::Bus::Broadcast(&AzFramework::ApplicationRequests::CalculateBranchTokenForEngineRoot, appBranchToken);
cmdLine = QString("\"%1\" --platform=%2 %3 --unattended=true --project-path=\"%4\" --watchfolder=\"%6\" --targetroot=\"%5\" --logprefix=\"%5/\" --port=%7 --branchtoken=\"%8\"");
cmdLine = cmdLine.arg(inputFile, platformIdentifier, params, projectPath, dest, watchFolder).arg(portNumber).arg(appBranchToken.c_str());
}
else
{
cmdLine = QString("\"%1\" --platform=%2 %3").arg(inputFile, platformIdentifier, params);
}
return cmdLine;
}
void NativeLegacyRCCompiler::RequestQuit()
{
this->m_requestedQuit = true;
}
BuilderIdAndName::BuilderIdAndName(QString builderName, QString builderId, Type type, QString rcParam /*=QString("")*/)
: m_builderName(builderName)
, m_builderId(builderId)
, m_type(type)
, m_rcParam(rcParam)
{
}
BuilderIdAndName& BuilderIdAndName::operator=(const AssetProcessor::BuilderIdAndName& src)
{
this->m_builderId = src.m_builderId;
this->m_builderName = src.m_builderName;
this->m_type = src.m_type;
this->m_rcParam = src.m_rcParam;
return *this;
}
const QString& BuilderIdAndName::GetName() const
{
return this->m_builderName;
}
bool BuilderIdAndName::GetUuid(AZ::Uuid& builderUuid) const
{
if (this->m_type == Type::REGISTERED_BUILDER)
{
builderUuid = AZ::Uuid::CreateString(this->m_builderId.toUtf8().data());
return true;
}
else
{
return false;
}
}
const QString& BuilderIdAndName::GetRcParam() const
{
return this->m_rcParam;
}
const QString& BuilderIdAndName::GetId() const
{
return this->m_builderId;
}
const BuilderIdAndName::Type BuilderIdAndName::GetType() const
{
return this->m_type;
}
const char* INTERNAL_BUILDER_UUID_STR = "589BE398-2EBB-4E3C-BE66-C894E34C944D";
const BuilderIdAndName BUILDER_ID_COPY("Internal Copy Builder", "31B74BFD-7046-47AC-A7DA-7D5167E9B2F8", BuilderIdAndName::Type::REGISTERED_BUILDER, ASSET_PROCESSOR_CONFIG_KEYWORD_COPY);
const BuilderIdAndName BUILDER_ID_RC ("Internal RC Builder", "0BBFC8C1-9137-4404-BD94-64C0364EFBFB", BuilderIdAndName::Type::REGISTERED_BUILDER);
const BuilderIdAndName BUILDER_ID_SKIP("Internal Skip Builder", "A033AF24-5041-4E24-ACEC-161A2E522BB6", BuilderIdAndName::Type::UNREGISTERED_BUILDER, ASSET_PROCESSOR_CONFIG_KEYWORD_SKIP);
const QHash<QString, BuilderIdAndName> ALL_INTERNAL_BUILDER_BY_ID =
{
{ BUILDER_ID_COPY.GetId(), BUILDER_ID_COPY },
{ BUILDER_ID_RC.GetId(), BUILDER_ID_RC },
{ BUILDER_ID_SKIP.GetId(), BUILDER_ID_SKIP }
};
InternalAssetRecognizer::InternalAssetRecognizer(const AssetRecognizer& src, const QString& builderId, const QHash<QString, AssetPlatformSpec>& assetPlatformSpecByPlatform)
: AssetRecognizer(src.m_name, src.m_testLockSource, src.m_priority, src.m_isCritical, src.m_supportsCreateJobs, src.m_patternMatcher, src.m_version, src.m_productAssetType, src.m_outputProductDependencies, src.m_checkServer)
, m_builderId(builderId)
{
// assetPlatformSpecByPlatform is a hash table like
// "pc" --> (settings to compile on pc)
// "ios" --> settings to compile on ios)
// and so is m_platformSpecsByPlatform
m_platformSpecsByPlatform = assetPlatformSpecByPlatform;
m_paramID = CalculateCRC();
}
AZ::u32 InternalAssetRecognizer::CalculateCRC() const
{
AZ::Crc32 crc;
crc.Add(m_name.toUtf8().data());
crc.Add(m_builderId.toUtf8().data());
crc.Add(const_cast<void*>(static_cast<const void*>(&m_testLockSource)), sizeof(m_testLockSource));
crc.Add(const_cast<void*>(static_cast<const void*>(&m_priority)), sizeof(m_priority));
crc.Add(m_patternMatcher.GetBuilderPattern().m_pattern.c_str());
crc.Add(const_cast<void*>(static_cast<const void*>(&m_patternMatcher.GetBuilderPattern().m_type)), sizeof(m_patternMatcher.GetBuilderPattern().m_type));
return static_cast<AZ::u32>(crc);
}
//! Constructor to initialize the internal builders and a general internal builder uuid that is used for bus
//! registration. This constructor is helpful for deriving other classes from this builder for purposes like
//! unit testing.
InternalRecognizerBasedBuilder::InternalRecognizerBasedBuilder(QHash<QString, BuilderIdAndName> inputBuilderByIdMap, AZ::Uuid internalBuilderUuid)
: m_isShuttingDown(false)
, m_rcCompiler(new NativeLegacyRCCompiler())
, m_internalRecognizerBuilderUuid(internalBuilderUuid)
{
for (BuilderIdAndName builder : inputBuilderByIdMap)
{
m_builderById[builder.GetId()] = inputBuilderByIdMap[builder.GetId()];
}
AssetBuilderSDK::AssetBuilderCommandBus::Handler::BusConnect(m_internalRecognizerBuilderUuid);
}
//! Constructor to initialize the internal based builder to a present set of internal builders and fixed bus id
InternalRecognizerBasedBuilder::InternalRecognizerBasedBuilder()
: InternalRecognizerBasedBuilder(ALL_INTERNAL_BUILDER_BY_ID, AZ::Uuid::CreateString(INTERNAL_BUILDER_UUID_STR))
{
}
InternalRecognizerBasedBuilder::~InternalRecognizerBasedBuilder()
{
AssetBuilderSDK::AssetBuilderCommandBus::Handler::BusDisconnect(m_internalRecognizerBuilderUuid);
for (auto assetRecognizer : m_assetRecognizerDictionary)
{
delete assetRecognizer;
}
}
AssetBuilderSDK::AssetBuilderDesc InternalRecognizerBasedBuilder::CreateBuilderDesc(const QString& builderId, const AZStd::vector<AssetBuilderSDK::AssetBuilderPattern>& builderPatterns)
{
const BuilderIdAndName& builder = m_builderById[builderId];
AssetBuilderSDK::AssetBuilderDesc builderDesc;
builderDesc.m_name = builder.GetName().toUtf8().data();
builderDesc.m_version = 2;
builderDesc.m_patterns = builderPatterns;
builderDesc.m_builderType = AssetBuilderSDK::AssetBuilderDesc::AssetBuilderType::Internal;
// Only set a bus id on the descriptor if the builder is a registered builder
AZ::Uuid busId;
if (builder.GetUuid(busId))
{
builderDesc.m_busId = AZ::Uuid::CreateString(builderId.toUtf8().data());
}
builderDesc.m_createJobFunction = AZStd::bind(&InternalRecognizerBasedBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
builderDesc.m_processJobFunction = AZStd::bind(&InternalRecognizerBasedBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2);
return builderDesc;
}
void InternalRecognizerBasedBuilder::ShutDown()
{
m_isShuttingDown = true;
this->m_rcCompiler->RequestQuit();
}
bool InternalRecognizerBasedBuilder::FindRC(QString& rcAbsolutePathOut)
{
AZ::IO::FixedMaxPath executableDirectory = AZ::Utils::GetExecutableDirectory();
executableDirectory /= ASSETPROCESSOR_TRAIT_LEGACY_RC_RELATIVE_PATH;
if (AZ::IO::SystemFile::Exists(executableDirectory.c_str()))
{
rcAbsolutePathOut = QString::fromUtf8(executableDirectory.c_str(), executableDirectory.Native().size());
return true;
}
return false;
}
bool InternalRecognizerBasedBuilder::Initialize(const RecognizerConfiguration& recognizerConfig)
{
InitializeAssetRecognizers(recognizerConfig.GetAssetRecognizerContainer());
return m_rcCompiler->Initialize();
}
void InternalRecognizerBasedBuilder::InitializeAssetRecognizers(const RecognizerContainer& assetRecognizers)
{
// Split the asset recognizers that were scanned in into 'buckets' for each of the 3 builder ids based on
// either the custom fixed rc params or the standard rc param ('copy','skip', or others)
QHash<QString, InternalAssetRecognizerList> internalRecognizerListByType;
InternalRecognizerBasedBuilder::BuildInternalAssetRecognizersByType(assetRecognizers, internalRecognizerListByType);
// note that the QString key to this map is actually the builder ID (as in the QString "Internal Copy Builder" for example)
// and the key of the map is actually a AZStd::list for InternalAssetRecognizer* which belong to that builder
// inside of each such recognizer is a map of [platform] --> options for that platform.
// so visualizing this whole struct in summary might look something like
// "Internal RC Builder" :
// {
// { <----- list of recognizers for that RC builder starts here
// regex: "*.tif",
// builderUUID : "12345-12354-123145",
// platformSpecsByPlatform :
// {
// "pc" : "streaming = 1",
// "ios" : "streaming = 0"
// }
// },
// {
// regex: "*.png",
// builderUUID : "12345-12354-123145",
// platformSpecsByPlatform :
// {
// "pc" : "split=1"
// }
// },
// },
// "Internal Copy Builder",
// {
// regex: "*.cfg",
// builderUUID : "12345-12354-123145",
// platformSpecsByPlatform :
// {
// "pc" : "copy",
// "ios" : "copy"
// }
// },
for (auto internalRecognizerList = internalRecognizerListByType.begin();
internalRecognizerList != internalRecognizerListByType.end();
internalRecognizerList++)
{
QString builderId = internalRecognizerList.key();
const BuilderIdAndName& builderInfo = m_builderById[builderId];
QString builderName = builderInfo.GetName();
AZStd::vector<AssetBuilderSDK::AssetBuilderPattern> builderPatterns;
bool supportsCreateJobs = false;
// intentionaly using a set here, as we want it to be the same order each time for hashing.
AZStd::set<AZStd::string> fingerprintRelevantParameters;
for (auto internalAssetRecognizer : *internalRecognizerList)
{
// so referring to the structure explanation above, internalAssetRecognizer is
// one of those objects that has the regex in it, (along with list of commands to apply per platform)
if (internalAssetRecognizer->m_platformSpecsByPlatform.size() == 0)
{
delete internalAssetRecognizer;
AZ_Warning(AssetProcessor::DebugChannel, false, "Skipping recognizer %s, no platforms supported\n", builderName.toUtf8().data());
continue;
}
// Ignore duplicate recognizers
// note that m_paramID is the CRC of a bunch of values inside the recognizer, so different recognizers should have a different paramID.
if (m_assetRecognizerDictionary.contains(internalAssetRecognizer->m_paramID))
{
delete internalAssetRecognizer;
AZ_Warning(AssetProcessor::DebugChannel, false, "Ignoring duplicate asset recognizer in configuration: %s\n", builderName.toUtf8().data());
continue;
}
for (auto iteratorValue = internalAssetRecognizer->m_platformSpecsByPlatform.begin(); iteratorValue != internalAssetRecognizer->m_platformSpecsByPlatform.end(); ++iteratorValue)
{
fingerprintRelevantParameters.insert(AZStd::string::format("%s-%s", iteratorValue.key().toUtf8().constData(), iteratorValue.value().m_extraRCParams.toUtf8().constData()));
}
// note that the version number must be included here, despite the builder dirty-check function taking version into account
// because the RC Builder is just a single builder (with version#0) that defers to these "internal" builders when called upon.
if (!internalAssetRecognizer->m_version.isEmpty())
{
fingerprintRelevantParameters.insert(internalAssetRecognizer->m_version.toUtf8().constData());
}
fingerprintRelevantParameters.insert(internalAssetRecognizer->m_productAssetType.ToString<AZStd::string>());
// Register the recognizer
builderPatterns.push_back(internalAssetRecognizer->m_patternMatcher.GetBuilderPattern());
m_assetRecognizerDictionary[internalAssetRecognizer->m_paramID] = internalAssetRecognizer;
AZ_TracePrintf(AssetProcessor::DebugChannel, "Registering %s as a %s\n", internalAssetRecognizer->m_name.toUtf8().data(),
builderName.toUtf8().data());
supportsCreateJobs = supportsCreateJobs || (internalAssetRecognizer->m_supportsCreateJobs);
}
// Register the builder desc if its registrable
if (builderInfo.GetType() == BuilderIdAndName::Type::REGISTERED_BUILDER)
{
AssetBuilderSDK::AssetBuilderDesc builderDesc = CreateBuilderDesc(builderId, builderPatterns);
// RC Builder also needs to include its platforms and its RC command lines so that if you change this, the jobs
// are re-evaluated.
size_t currentHash = 0;
for (const AZStd::string& element : fingerprintRelevantParameters)
{
AZStd::hash_combine<AZStd::string>(currentHash, element);
}
builderDesc.m_analysisFingerprint = AZStd::string::format("0x%zX", currentHash);
// the "rc" builder can only emit dependencies if it has createjobs in a recognizer.
if (!supportsCreateJobs)
{
// optimization: copy builder emits no dependencies since its just a copy builder.
builderDesc.m_flags |= AssetBuilderSDK::AssetBuilderDesc::BF_EmitsNoDependencies;
}
AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBusTraits::RegisterBuilderInformation, builderDesc);
}
}
}
void InternalRecognizerBasedBuilder::UnInitialize()
{
for (BuilderIdAndName builder: m_builderById.values())
{
AZ::Uuid builderUuid;
// Register the builder desc if its registrable
if ((builder.GetType() == BuilderIdAndName::Type::REGISTERED_BUILDER) && (builder.GetUuid(builderUuid)))
{
EBUS_EVENT(AssetBuilderRegistrationBus, UnRegisterBuilderDescriptor, builderUuid);
}
}
}
bool InternalRecognizerBasedBuilder::GetMatchingRecognizers(const AZStd::vector<AssetBuilderSDK::PlatformInfo>& platformInfos, const QString& fileName, InternalRecognizerPointerContainer& output) const
{
QByteArray fileNameUtf8 = fileName.toUtf8();
AZ_Assert(fileName.contains('\\') == false, "fileName must not contain backslashes: %s", fileNameUtf8.constData());
bool foundAny = false;
// assetRecognizerDictionary is a key value pair dictionary where
// [key] is m_paramID of a recognizer - that is, a unique id of an internal asset recognizer
// and [value] is the actual recognizer.
// inside recognizers are the pattern that they match, as well as the various platforms that they compile for.
for (const InternalAssetRecognizer* recognizer : m_assetRecognizerDictionary)
{
// so this platform is supported. Check if the file matches the regex in MatchesPath.
if (recognizer->m_patternMatcher.MatchesPath(fileNameUtf8.constData()))
{
// this recognizer does match that particular file name.
// do we know how to compile it for any of the platforms?
for (const AssetBuilderSDK::PlatformInfo& platformInfo : platformInfos)
{
// recognizer->m_platformSpecsByplatform is a dictionary like
// ["pc"] -> what to do with the asset on PC.
// ["ios"] -> what to do wiht the asset on ios
if (recognizer->m_platformSpecsByPlatform.find(platformInfo.m_identifier.c_str()) != recognizer->m_platformSpecsByPlatform.end())
{
// yes, we have at least one platform that overlaps with the enabled platform list.
output.push_back(recognizer);
foundAny = true;
break;
}
}
}
}
return foundAny;
}
void InternalRecognizerBasedBuilder::CreateJobs(const AssetBuilderSDK::CreateJobsRequest& request, AssetBuilderSDK::CreateJobsResponse& response)
{
if (m_isShuttingDown)
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::ShuttingDown;
return;
}
// Convert the incoming builder id (AZ::Uuid) to the equivalent GUID from the asset recognizers
AZStd::string azBuilderId;
request.m_builderid.ToString(azBuilderId,false);
QString requestedBuilderID = QString(azBuilderId.c_str());
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
QDir watchFolder(request.m_watchFolder.c_str());
QString normalizedPath = watchFolder.absoluteFilePath(request.m_sourceFile.c_str());
normalizedPath = AssetUtilities::NormalizeFilePath(normalizedPath);
// Locate recognizers that match the file
InternalRecognizerPointerContainer recognizers;
if (!GetMatchingRecognizers(request.m_enabledPlatforms, normalizedPath, recognizers))
{
AssetBuilderSDK::BuilderLog(m_internalRecognizerBuilderUuid, "Cannot find recognizer for %s.", request.m_sourceFile.c_str());
if (request.m_enabledPlatforms.empty())
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
return;
}
// First pass
for (const InternalAssetRecognizer* recognizer : recognizers)
{
if (recognizer->m_supportsCreateJobs)
{
// The recognizer's builder id must match the job requests' builder id
if (recognizer->m_builderId.compare(requestedBuilderID) != 0)
{
continue;
}
AssetBuilderSDK::CreateJobsResponse rcResponse;
CreateLegacyRCJob(request, "", rcResponse);
if (rcResponse.m_result != AssetBuilderSDK::CreateJobsResultCode::Success)
{
// Error is already printed out by CreateLegacyRCJob
continue;
}
for (auto& descriptor : rcResponse.m_createJobOutputs)
{
descriptor.m_jobParameters[recognizer->m_paramID] = descriptor.m_jobKey;
}
// Move-append the response outputs
response.m_createJobOutputs.reserve(response.m_createJobOutputs.size() + rcResponse.m_createJobOutputs.size());
response.m_createJobOutputs.reserve(response.m_sourceFileDependencyList.size() + rcResponse.m_sourceFileDependencyList.size());
AZStd::move(rcResponse.m_createJobOutputs.begin(), rcResponse.m_createJobOutputs.end(), AZStd::back_inserter(response.m_createJobOutputs));
AZStd::move(rcResponse.m_sourceFileDependencyList.begin(), rcResponse.m_sourceFileDependencyList.end(), AZStd::back_inserter(response.m_sourceFileDependencyList));
response.m_result = rcResponse.m_result;
}
else
{
bool skippedByPlatform = false;
// Iterate through the platform specific specs and apply the ones that match the platform flag
for (auto iterPlatformSpec = recognizer->m_platformSpecsByPlatform.cbegin();
iterPlatformSpec != recognizer->m_platformSpecsByPlatform.cend();
iterPlatformSpec++)
{
if (request.HasPlatform(iterPlatformSpec.key().toUtf8().constData()))
{
QString rcParam = iterPlatformSpec.value().m_extraRCParams;
// Check if this is the 'skip' parameter
if (rcParam.compare(ASSET_PROCESSOR_CONFIG_KEYWORD_SKIP) == 0)
{
skippedByPlatform = true;
}
// The recognizer's builder id must match the job requests' builder id
else if (recognizer->m_builderId.compare(requestedBuilderID) == 0)
{
AssetBuilderSDK::JobDescriptor descriptor;
Internal::PopulateCommonDescriptorParams(descriptor, iterPlatformSpec.key(), iterPlatformSpec.value(), recognizer);
// Job Parameter Value can be any arbitrary string since we are relying on the key to lookup
// the parameter in the process job
descriptor.m_jobParameters[recognizer->m_paramID] = descriptor.m_jobKey;
response.m_createJobOutputs.push_back(descriptor);
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
}
}
// Adjust response if we did not get any jobs, but one or more platforms were marked as skipped
if ((response.m_result == AssetBuilderSDK::CreateJobsResultCode::Failed) && (skippedByPlatform))
{
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Success;
}
}
}
}
void InternalRecognizerBasedBuilder::ProcessJob(const AssetBuilderSDK::ProcessJobRequest& request, AssetBuilderSDK::ProcessJobResponse& response)
{
AssetBuilderSDK::JobCancelListener jobCancelListener(request.m_jobId);
if (m_isShuttingDown)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
if (request.m_jobDescription.m_jobParameters.empty())
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel,
"Job request for %s in builder %s missing job parameters.",
request.m_sourceFile.c_str(),
BUILDER_ID_RC.GetId().toUtf8().data());
return;
}
for (auto jobParam = request.m_jobDescription.m_jobParameters.begin();
jobParam != request.m_jobDescription.m_jobParameters.end();
jobParam++)
{
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
if (m_assetRecognizerDictionary.find(jobParam->first) == m_assetRecognizerDictionary.end())
{
AZ_TracePrintf(AssetProcessor::ConsoleChannel,
"Job request for %s in builder %s has invalid job parameter (%ld).",
request.m_sourceFile.c_str(),
BUILDER_ID_RC.GetId().toUtf8().data(),
jobParam->first);
continue;
}
InternalAssetRecognizer* assetRecognizer = m_assetRecognizerDictionary[jobParam->first];
if (!assetRecognizer->m_platformSpecsByPlatform.contains(request.m_jobDescription.GetPlatformIdentifier().c_str()))
{
// Skip due to platform restrictions
continue;
}
QString rcParam = assetRecognizer->m_platformSpecsByPlatform[request.m_jobDescription.GetPlatformIdentifier().c_str()].m_extraRCParams;
if (rcParam.compare(ASSET_PROCESSOR_CONFIG_KEYWORD_COPY) == 0)
{
ProcessCopyJob(request, assetRecognizer->m_productAssetType, assetRecognizer->m_outputProductDependencies, jobCancelListener, response);
}
else if (rcParam.compare(ASSET_PROCESSOR_CONFIG_KEYWORD_SKIP) == 0)
{
// This should not occur because 'skipped' jobs should not be processed
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job ID %lld Failed, encountered an invalid 'skip' parameter during job processing\n", AssetProcessor::GetThreadLocalJobId());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
}
else
{
// If the job fails due to a networking issue, we will attempt to retry RetriesForJobNetworkError times
int retryCount = 0;
do
{
++retryCount;
ProcessLegacyRCJob(request, rcParam, assetRecognizer->m_productAssetType, jobCancelListener, response);
AZ_Warning("RC Builder", response.m_resultCode != AssetBuilderSDK::ProcessJobResult_NetworkIssue, "RC.exe reported a network connection issue. %s",
retryCount <= AssetProcessor::RetriesForJobNetworkError ? "Attempting to retry job." : "Maximum retry attempts exceeded, giving up.");
} while (response.m_resultCode == AssetBuilderSDK::ProcessJobResult_NetworkIssue && retryCount <= AssetProcessor::RetriesForJobNetworkError);
}
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
}
if (response.m_resultCode != AssetBuilderSDK::ProcessJobResult_Success)
{
// If anything other than a success occurred, break out of the loop and report the failed job
return;
}
}
}
void InternalRecognizerBasedBuilder::CreateLegacyRCJob(const AssetBuilderSDK::CreateJobsRequest& request, QString rcParam, AssetBuilderSDK::CreateJobsResponse& response)
{
const char* requestFileName = "createjobsRequest.xml";
const char* responseFileName = "createjobsResponse.xml";
const char* createJobsParam = "/createjobs";
QDir watchDir(request.m_watchFolder.c_str());
auto normalizedPath = watchDir.absoluteFilePath(request.m_sourceFile.c_str());
QString workFolder;
if (!AssetUtilities::CreateTempWorkspace(workFolder))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Failed to create temporary workspace");
return;
}
QString watchFolder = request.m_watchFolder.c_str();
NativeLegacyRCCompiler::Result rcResult;
QDir workDir(workFolder);
QString requestPath = workDir.absoluteFilePath(requestFileName);
QString responsePath = workDir.absoluteFilePath(responseFileName);
if (!AZ::Utils::SaveObjectToFile(requestPath.toStdString().c_str(), AZ::DataStream::ST_XML, &request))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Failed to write CreateJobsRequest to file %s", requestPath.toStdString().c_str());
return;
}
QString params = QString("%1=\"%2\"").arg(createJobsParam).arg(requestPath);
//Platform and platform id are hard coded to PC because it doesn't matter, the actual platform info is in the CreateJobsRequest
if ((!this->m_rcCompiler->Execute(normalizedPath, watchFolder, "pc", rcParam.append(params), workFolder, nullptr, rcResult)) || (rcResult.m_exitCode != 0))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job ID %lld Failed with exit code %d\n", AssetProcessor::GetThreadLocalJobId(), rcResult.m_exitCode);
response.m_result = AssetBuilderSDK::CreateJobsResultCode::Failed;
return;
}
if (AZ::Utils::LoadObjectFromFileInPlace(responsePath.toStdString().c_str(), response))
{
workDir.removeRecursively();
}
}
bool InternalRecognizerBasedBuilder::SaveProcessJobRequestFile(const char* requestFileDir, const char* requestFileName, const AssetBuilderSDK::ProcessJobRequest& request)
{
AZStd::string finalFullPath;
AzFramework::StringFunc::Path::Join(requestFileDir, requestFileName, finalFullPath);
if (!AZ::Utils::SaveObjectToFile(finalFullPath.c_str(), AZ::DataStream::ST_XML, &request))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Failed to write ProcessJobRequest to file %s", finalFullPath.c_str());
return false;
}
return true;
}
bool InternalRecognizerBasedBuilder::LoadProcessJobResponseFile(const char* responseFileDir, const char* responseFileName, AssetBuilderSDK::ProcessJobResponse& response, bool& responseLoaded)
{
responseLoaded = false;
AZStd::string finalFullPath;
AzFramework::StringFunc::Path::Join(responseFileDir, responseFileName, finalFullPath);
if (AZ::IO::FileIOBase::GetInstance()->Exists(finalFullPath.c_str()))
{
// make a new one in case of pollution.
response = AssetBuilderSDK::ProcessJobResponse();
AZ_TracePrintf(AssetProcessor::DebugChannel, "Loading the response from '%s' emitted by RC.EXE.\n", finalFullPath.c_str());
if (!AZ::Utils::LoadObjectFromFileInPlace(finalFullPath.c_str(), response))
{
// rc TRIED to make a response file and failed somehow!
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
AZ_Error(AssetProcessor::DebugChannel, false, "Failed to deserialize '%s' despite RC.EXE having written it.", finalFullPath.c_str());
return false;
}
else
{
// we loaded it.
responseLoaded = true;
}
}
// either way, its not a failure if the file doesn't exist or loaded successfully
return true;
}
void InternalRecognizerBasedBuilder::ProcessLegacyRCJob(const AssetBuilderSDK::ProcessJobRequest& request, QString rcParam,
AZ::Uuid productAssetType, const AssetBuilderSDK::JobCancelListener& jobCancelListener, AssetBuilderSDK::ProcessJobResponse& response)
{
// Process this job
QString inputFile = QString(request.m_fullPath.c_str());
QString platformIdentifier = request.m_jobDescription.GetPlatformIdentifier().c_str();
QString dest = request.m_tempDirPath.c_str();
QString watchFolder = request.m_watchFolder.c_str();
NativeLegacyRCCompiler::Result rcResult;
QDir workDir(dest);
if (!SaveProcessJobRequestFile(dest.toUtf8().constData(), AssetBuilderSDK::s_processJobRequestFileName, request))
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
if ((!this->m_rcCompiler->Execute(inputFile, watchFolder, platformIdentifier, rcParam, dest, &jobCancelListener, rcResult)) || (rcResult.m_exitCode != 0))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "Job ID %lld Failed with exit code %d\n", AssetProcessor::GetThreadLocalJobId(), rcResult.m_exitCode);
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
else if (rcResult.m_crashed)
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Crashed;
return;
}
}
// did the rc Compiler output a response file?
bool responseFromRCCompiler = false;
if (!LoadProcessJobResponseFile(dest.toUtf8().constData(), AssetBuilderSDK::s_processJobResponseFileName, response, responseFromRCCompiler))
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
if (!responseFromRCCompiler)
{
if(rcResult.m_exitCode != 0)
{
// RC didn't crash and didn't write a response, but returned a failure code
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return;
}
else
{
// if the response was NOT loaded from a response file, we assume success (since RC did not crash or anything)
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
}
}
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
if (response.m_requiresSubIdGeneration)
{
ProcessRCResultFolder(dest, productAssetType, responseFromRCCompiler, response);
}
}
void InternalRecognizerBasedBuilder::ProcessRCResultFolder(const QString &dest, const AZ::Uuid& productAssetType, bool responseFromRCCompiler, AssetBuilderSDK::ProcessJobResponse &response)
{
// Get all of the files from the dest folder
QFileInfoList originalFiles = GetFilesInDirectory(dest);
QFileInfoList filteredFiles;
// Filter out the log files and add to the result products
AZStd::unordered_set<AZ::u32> m_alreadyAssignedSubIDs;
bool hasSubIdCollision = false;
// for legacy compatibility, we generate the list of output Products ourselves so that we can
// assign legacy SubIDs to them:
AZStd::vector<AssetBuilderSDK::JobProduct> generatedOutputProducts;
for (const auto& file : originalFiles)
{
if (MatchTempFileToSkip(file.fileName()))
{
AZ_TracePrintf(AssetProcessor::DebugChannel, "RC created temporary file: (%s), ignoring.\n", file.absoluteFilePath().toUtf8().data());
continue;
}
AZStd::string productName = file.fileName().toUtf8().constData();
// convert to abs path:
if (AzFramework::StringFunc::Path::IsRelative(productName.c_str()))
{
// convert to absolute path.
AZStd::string joinedPath;
AzFramework::StringFunc::Path::Join(dest.toUtf8().constData(), productName.c_str(), joinedPath);
productName.swap(joinedPath);
}
else
{
AzFramework::StringFunc::Path::Normalize(productName);
}
// this kind of job can output multiple products.
// we are going to generate SUBIds for them if they collide, here!
// ideally, the builder SDK builder written for this asset type would deal with it.
AZ_TracePrintf(AssetProcessor::DebugChannel, "RC created product file: (%s).\n", productName.c_str());
generatedOutputProducts.push_back(AssetBuilderSDK::JobProduct(productName, productAssetType));
hasSubIdCollision |= (m_alreadyAssignedSubIDs.insert(generatedOutputProducts.back().m_productSubID).second == false); // insert returns pair<iter, bool> where the bool is false if it was already there.
}
// now fix any subid collisions, but only if we have an actual collision.
// previously these would be the real subids, but now they are legacy subids if the response was given.
if (hasSubIdCollision)
{
m_alreadyAssignedSubIDs.clear();
for (AssetBuilderSDK::JobProduct& product : generatedOutputProducts)
{
AZ_TracePrintf("RC Builder", "SubId collision detected for product file: (%s).\n", product.m_productFileName.c_str());
AZ::u32 seedValue = 0;
while (m_alreadyAssignedSubIDs.find(product.m_productSubID) != m_alreadyAssignedSubIDs.end())
{
// its already in! pick another one. For now, lets pick something based on the name so that ordering doesn't mess it up
QFileInfo productFileInfo(product.m_productFileName.c_str());
QString filePart = productFileInfo.fileName(); // the file part only (blah.dds) - not the path.
AZ::u32 fullCRC = AZ::Crc32(filePart.toUtf8().data());
AZ::u32 maskedCRC = (fullCRC + seedValue) & AssetBuilderSDK::SUBID_MASK_ID;
// preserve the LOD and the other flags, but replace the CRC:
product.m_productSubID = AssetBuilderSDK::ConstructSubID(maskedCRC, AssetBuilderSDK::GetSubID_LOD(product.m_productSubID), product.m_productSubID);
++seedValue;
}
m_alreadyAssignedSubIDs.insert(product.m_productSubID);
}
}
// now that we have generated both the legacy product subIDs and have potentially gotten a response from RC.EXE, we reconcile them
if (!responseFromRCCompiler)
{
// If we get here, it means that RC.EXE did no generate a response file for us, so its 100% up to our heuristic to decide on subIds.
response.m_outputProducts = generatedOutputProducts;
// its fine for RC to decide there are no outputs. The only factor is what its exit code is.
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
}
else
{
// otherwise, if we get here it means that RC.EXE output a valid response file for us, including a list of canonical products
// all we need to do thus is match up the generated products with the ones it made to fill out the legacy SubID field.
// this is also our opportunity to clean and check for problems such as duplicate product files and duplicate subIDs.
AZStd::unordered_set<AZStd::string> productsToCheckForDuplicates;
AZStd::unordered_map<AZ::u32, AZStd::string> subIdsToCheckForDuplicates;
for (AssetBuilderSDK::JobProduct& product : response.m_outputProducts)
{
AZStd::string productName = product.m_productFileName;
if (productName.empty())
{
AZ_Error(AssetProcessor::DebugChannel, false, "The RC Builder responded with a processJobResult.xml but that xml contained an empty product name.");
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return; // its a failure to do this!
}
// now clean it, normalize it, and make it absolute so that its unambiguous.
// do not convert its case - this has to work on Operating Systems which have case sensitivy.
// we'll insert a lowercase copy into the set to check.
if (AzFramework::StringFunc::Path::IsRelative(productName.c_str()))
{
// convert to absolute path.
AZStd::string joinedPath;
AzFramework::StringFunc::Path::Join(dest.toUtf8().constData(), productName.c_str(), joinedPath);
productName.swap(joinedPath);
}
// update it in the structure to be absolute normalized path.
product.m_productFileName = productName;
AZStd::string productNameLowerCase = productName;
AZStd::to_lower(productNameLowerCase.begin(), productNameLowerCase.end());
if (subIdsToCheckForDuplicates.find(product.m_productSubID) != subIdsToCheckForDuplicates.end())
{
AZStd::string collidingFileName = subIdsToCheckForDuplicates[product.m_productSubID];
AZ_Error(AssetProcessor::DebugChannel, false, "Duplicate subID emitted by builder: '%s' with subID 0x%08x was in the outputProducts array more than once.", collidingFileName.c_str(), product.m_productSubID);
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return; // its a failure to do this!
}
subIdsToCheckForDuplicates.insert(product.m_productSubID);
if (productsToCheckForDuplicates.find(productNameLowerCase) != productsToCheckForDuplicates.end())
{
AZ_Error(AssetProcessor::DebugChannel, false, "Duplicate product emitted by builder: '%s' was in the outputProducts array more than once.", productName.c_str());
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Failed;
return; // its a failure to do this!
}
productsToCheckForDuplicates.insert(productNameLowerCase);
// we are through the gauntlet. Now reconcile this with the previously generated legacy subIDs.
for (AssetBuilderSDK::JobProduct& generatedProduct : generatedOutputProducts)
{
AZStd::string generatedPath = generatedProduct.m_productFileName;
// (this is already absolute path and already normalized)
// this is icase compare and is being done in utf8 strings.
if (AzFramework::StringFunc::Equal(generatedPath.c_str(), productName.c_str()))
{
// found it.
if (AZStd::find(product.m_legacySubIDs.begin(), product.m_legacySubIDs.end(), generatedProduct.m_productSubID) == product.m_legacySubIDs.end())
{
product.m_legacySubIDs.push_back(generatedProduct.m_productSubID);
}
}
}
}
}
}
void InternalRecognizerBasedBuilder::ProcessCopyJob(
const AssetBuilderSDK::ProcessJobRequest& request,
AZ::Uuid productAssetType,
bool outputProductDependencies,
const AssetBuilderSDK::JobCancelListener& jobCancelListener,
AssetBuilderSDK::ProcessJobResponse& response)
{
AssetBuilderSDK::JobProduct jobProduct(request.m_fullPath, productAssetType);
if(!outputProductDependencies)
{
jobProduct.m_dependenciesHandled = true; // Copy jobs are meant to be used for assets that have no dependencies and just need to be copied.
}
response.m_outputProducts.push_back(jobProduct);
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success;
if (jobCancelListener.IsCancelled())
{
response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Cancelled;
return;
}
// Temporary solution to get around the fact that we don't have job dependencies
TempSolution_TouchCopyJobActivity();
if (outputProductDependencies)
{
// Launch the external process when it requires the old cry code to parse a specific type of asset
QString rcParam = QString("/copyonly /outputproductdependencies /targetroot");
rcParam = QString("%1=\"%2\"").arg(rcParam).arg(request.m_tempDirPath.c_str());
ProcessLegacyRCJob(request, rcParam, productAssetType, jobCancelListener, response);
}
}
QFileInfoList InternalRecognizerBasedBuilder::GetFilesInDirectory(const QString& directoryPath)
{
QDir workingDir(directoryPath);
QFileInfoList filesInDir(workingDir.entryInfoList(QDir::NoDotAndDotDot | QDir::Files));
return filesInDir;
}
bool InternalRecognizerBasedBuilder::MatchTempFileToSkip(const QString& outputFilename)
{
// List of specific files to skip
static const char* s_fileNamesToSkip[] = {
AssetBuilderSDK::s_processJobRequestFileName,
AssetBuilderSDK::s_processJobResponseFileName,
"rc_createdfiles.txt",
"rc_log.log",
"rc_log_warnings.log",
"rc_log_errors.log"
};
for (const char* filenameToSkip : s_fileNamesToSkip)
{
if (QString::compare(outputFilename, filenameToSkip, Qt::CaseInsensitive) == 0)
{
return true;
}
}
// List of specific file name patters to skip
static const QString s_filePatternsToSkip[] = {
QString(".*\\.\\$.*"),
QString("log.*\\.txt")
};
for (const QString& patternsToSkip : s_filePatternsToSkip)
{
QRegExp skipRegex(patternsToSkip, Qt::CaseInsensitive, QRegExp::RegExp);
if (skipRegex.exactMatch(outputFilename))
{
return true;
}
}
return false;
}
void InternalRecognizerBasedBuilder::RegisterInternalAssetRecognizerToMap(const AssetRecognizer& assetRecognizer, const QString& builderId, QHash<QString, AssetPlatformSpec>& sourceAssetPlatformSpecs, QHash<QString, InternalAssetRecognizerList>& internalRecognizerListByType)
{
// this is called to say that the internal builder with builderID is to handle assets recognized by the givne recognizer.
InternalAssetRecognizer* newAssetRecognizer = new InternalAssetRecognizer(assetRecognizer, builderId, sourceAssetPlatformSpecs);
// the list is keyed off the builderID.
internalRecognizerListByType[builderId].push_back(newAssetRecognizer);
}
void InternalRecognizerBasedBuilder::BuildInternalAssetRecognizersByType(const RecognizerContainer& assetRecognizers, QHash<QString, InternalAssetRecognizerList>& internalRecognizerListByType)
{
// Go through each asset recognizer's platform specs to determine which type bucket to create and put the converted internal
// assert recognizer into
for (const AssetRecognizer& assetRecognizer : assetRecognizers)
{
// these hashes are keyed on the same key as the incoming asset recognizers list, which is
// [ name in ini file ] --> [regognizer details]
// so like "rc png" --> [details]. Specifically, the QString key is the name of the entry in the INI file and NOT a platform name.
QHash<QString, AssetPlatformSpec> copyAssetPlatformSpecs;
QHash<QString, AssetPlatformSpec> skipAssetPlatformSpecs;
QHash<QString, AssetPlatformSpec> rcAssetPlatformSpecs;
// Go through the global asset recognizers and split them by operation keywords if they exist or by the main rc param
for (auto iterSrcPlatformSpec = assetRecognizer.m_platformSpecs.begin();
iterSrcPlatformSpec != assetRecognizer.m_platformSpecs.end();
iterSrcPlatformSpec++)
{
if (iterSrcPlatformSpec->m_extraRCParams.compare(BUILDER_ID_COPY.GetRcParam()) == 0)
{
copyAssetPlatformSpecs[iterSrcPlatformSpec.key()] = iterSrcPlatformSpec.value();
}
else if (iterSrcPlatformSpec->m_extraRCParams.compare(BUILDER_ID_SKIP.GetRcParam()) == 0)
{
skipAssetPlatformSpecs[iterSrcPlatformSpec.key()] = iterSrcPlatformSpec.value();
}
else
{
rcAssetPlatformSpecs[iterSrcPlatformSpec.key()] = iterSrcPlatformSpec.value();
}
}
// Create separate internal asset recognizers based on whether or not they were detected
if (copyAssetPlatformSpecs.size() > 0)
{
RegisterInternalAssetRecognizerToMap(assetRecognizer, BUILDER_ID_COPY.GetId(), copyAssetPlatformSpecs, internalRecognizerListByType);
}
if (skipAssetPlatformSpecs.size() > 0)
{
RegisterInternalAssetRecognizerToMap(assetRecognizer, BUILDER_ID_SKIP.GetId(), skipAssetPlatformSpecs, internalRecognizerListByType);
}
if (rcAssetPlatformSpecs.size() > 0)
{
RegisterInternalAssetRecognizerToMap(assetRecognizer, BUILDER_ID_RC.GetId(), rcAssetPlatformSpecs, internalRecognizerListByType);
}
}
}
}