瀏覽代碼

Update asset processor to handle processing multiple metadata files with the same relative path

Signed-off-by: amzn-mike <[email protected]>
amzn-mike 2 年之前
父節點
當前提交
a8a7641ff5

+ 2 - 0
Code/Tools/AssetProcessor/assetprocessor_static_files.cmake

@@ -115,6 +115,8 @@ set(FILES
     native/utilities/UuidManager.h
     native/utilities/UuidManager.cpp
     native/utilities/IMetadataUpdates.h
+    native/utilities/ProductOutputUtil.h
+    native/utilities/ProductOutputUtil.cpp
 )
 
 set(SKIP_UNITY_BUILD_INCLUSION_FILES

+ 14 - 1
Code/Tools/AssetProcessor/native/AssetManager/assetProcessorManager.cpp

@@ -36,6 +36,7 @@
 #include <native/AssetManager/SourceAssetReference.h>
 #include <AzToolsFramework/Metadata/MetadataManager.h>
 #include <native/utilities/UuidManager.h>
+#include <native/utilities/ProductOutputUtil.h>
 
 namespace AssetProcessor
 {
@@ -1307,6 +1308,13 @@ namespace AssetProcessor
             AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer priorProducts;
             m_stateData->GetProductsByJobID(job.m_jobID, priorProducts);
 
+            ProductOutputUtil::FinalizeProduct(
+                m_stateData,
+                m_platformConfig,
+                processedAsset.m_entry.m_sourceAssetReference,
+                processedAsset.m_response.m_outputProducts,
+                processedAsset.m_entry.m_platformInfo.m_identifier);
+
             //make new product entries from the job response output products
             ProductInfoList newProducts;
             AZStd::vector<AZStd::vector<AZ::u32> > newLegacySubIDs;  // each product has a vector of legacy subids;
@@ -2778,6 +2786,9 @@ namespace AssetProcessor
         auto* fileStateInterface = AZ::Interface<AssetProcessor::IFileStateRequests>::Get();
         AZ_Assert(fileStateInterface, "Programmer Error - IFileStateRequests interface is not available.");
 
+        auto* uuidManagerInterface = AZ::Interface<AssetProcessor::IUuidRequests>::Get();
+        AZ_Assert(uuidManagerInterface, "Programmer Error - IUuidRequests interface is not available.");
+
         // During unit tests, it can be the case that cache folders are actually in a temp folder structure
         // on OSX this is /var/... , but that is a symlink for real path /private/var.  In some circumstances file monitor
         // for deletions may report the canonical path (/private/var/...) when the 'cache root' or watched folder
@@ -3004,6 +3015,8 @@ namespace AssetProcessor
                     }
                 }
 
+                const bool isMetadataEnabledType = uuidManagerInterface->IsGenerationEnabledForFile(normalizedPath.toUtf8().constData());
+
                 // is it being overridden by a higher priority file?
                 QString overrider;
                 if (examineFile.m_isDelete)
@@ -3040,7 +3053,7 @@ namespace AssetProcessor
                         }
                     }
                 }
-                else
+                else if(!isMetadataEnabledType)
                 {
                     overrider = m_platformConfig->GetOverridingFile(sourceAssetReference.RelativePath().c_str(), sourceAssetReference.ScanFolderPath().c_str());
                 }

+ 9 - 1
Code/Tools/AssetProcessor/native/resourcecompiler/rcjob.cpp

@@ -20,6 +20,7 @@
 #include "native/utilities/JobDiagnosticTracker.h"
 
 #include <qstorageinfo.h>
+#include <native/utilities/ProductOutputUtil.h>
 
 namespace
 {
@@ -808,6 +809,13 @@ namespace AssetProcessor
                 return false;
             }
 
+            const bool isSourceMetadataEnabled = !params.m_sourceUuid.IsNull();
+
+            if (isSourceMetadataEnabled)
+            {
+                ProductOutputUtil::ModifyProductPath(outputFilename, params.m_rcJob->GetJobEntry().m_sourceAssetReference.ScanFolderId());
+            }
+
             if(outputToCache)
             {
                 needCacheDirectory = true;
@@ -843,7 +851,7 @@ namespace AssetProcessor
                     // The assumption for the UUID generated below is that the source UUID will not change.  A type which has no metadata
                     // file currently may be updated later to have a metadata file, which would break that assumption.  In that case, stick
                     // with the default path-based UUID.
-                    if (!params.m_sourceUuid.IsNull())
+                    if (isSourceMetadataEnabled)
                     {
                         // Generate a UUID for the intermediate as:
                         // SourceUuid:BuilderUuid:SubId

+ 4 - 0
Code/Tools/AssetProcessor/native/resourcecompiler/rcjob.h

@@ -76,6 +76,10 @@ namespace AssetProcessor
         AZ::IO::Path m_cacheOutputDir;
         AZ::IO::Path m_intermediateOutputDir;
         AZ::IO::Path m_relativePath;
+
+        // UUID of the original source asset.
+        // If this job is for an intermediate asset, the UUID is for the direct source which produced the intermediate.
+        // If the original source asset is not using metadata files, this value will be empty.
         AZ::Uuid m_sourceUuid;
 
         Params(const Params&) = default;

+ 14 - 8
Code/Tools/AssetProcessor/native/tests/assetmanager/AssetManagerTestingBase.cpp

@@ -81,8 +81,7 @@ namespace UnitTests
         m_platformConfig->PopulatePlatformsForScanFolder(platforms);
         m_platformConfig->ReadMetaDataFromSettingsRegistry();
 
-        m_platformConfig->AddScanFolder(
-            AssetProcessor::ScanFolderInfo{ (assetRootDir / "folder").c_str(), "folder", "folder", false, true, platforms });
+        SetupScanfolders(assetRootDir, platforms);
 
         m_platformConfig->AddIntermediateScanFolder();
 
@@ -202,6 +201,12 @@ namespace UnitTests
         LeakDetectionFixture::TearDown();
     }
 
+    void AssetManagerTestingBase::SetupScanfolders(AZ::IO::Path assetRootDir, const AZStd::vector<AssetBuilderSDK::PlatformInfo>& platforms)
+    {
+        m_platformConfig->AddScanFolder(
+            AssetProcessor::ScanFolderInfo{ (assetRootDir / "folder").c_str(), "folder", "folder", false, true, platforms });
+    }
+
     void AssetManagerTestingBase::RunFile(int expectedJobCount, int expectedFileCount, int dependencyFileCount)
     {
         m_jobDetailsList.clear();
@@ -468,16 +473,16 @@ namespace UnitTests
     }
 
     void AssetManagerTestingBase::ProcessFileMultiStage(
-        int endStage, bool doProductOutputCheck, const char* file, int startStage, bool expectAutofail, bool hasExtraFile)
+        int endStage, bool doProductOutputCheck, AssetProcessor::SourceAssetReference sourceAsset, int startStage, bool expectAutofail, bool hasExtraFile)
     {
         auto intermediatesDir = GetIntermediateAssetsDir();
 
-        if (file == nullptr)
+        if (!sourceAsset)
         {
-            file = m_testFilePath.c_str();
+            sourceAsset = AssetProcessor::SourceAssetReference(m_testFilePath.c_str());
         }
 
-        QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssessAddedFile", Qt::QueuedConnection, Q_ARG(QString, file));
+        QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssessAddedFile", Qt::QueuedConnection, Q_ARG(QString, sourceAsset.AbsolutePath().c_str()));
         QCoreApplication::processEvents();
 
         for (int i = startStage; i <= endStage; ++i)
@@ -508,7 +513,8 @@ namespace UnitTests
 
             if (i < endStage)
             {
-                auto expectedIntermediatePath = intermediatesDir / AZStd::string::format("test.stage%d", i + 1);
+                auto expectedIntermediatePath =
+                    MakePath(sourceAsset.RelativePath().ReplaceExtension(AZStd::string::format("stage%d", i + 1).c_str()).c_str(), true);
                 EXPECT_TRUE(AZ::IO::SystemFile::Exists(expectedIntermediatePath.c_str())) << expectedIntermediatePath.c_str();
             }
 
@@ -522,7 +528,7 @@ namespace UnitTests
 
         if (doProductOutputCheck)
         {
-            CheckProduct(AZStd::string::format("test.stage%d", endStage + 1).c_str());
+            CheckProduct(sourceAsset.RelativePath().ReplaceExtension(AZStd::string::format("stage%d", endStage + 1).c_str()).c_str());
         }
     }
 } // namespace UnitTests

+ 3 - 1
Code/Tools/AssetProcessor/native/tests/assetmanager/AssetManagerTestingBase.h

@@ -100,6 +100,8 @@ namespace UnitTests
         void SetUp() override;
         void TearDown() override;
 
+        virtual void SetupScanfolders(AZ::IO::Path assetRootDir, const AZStd::vector<AssetBuilderSDK::PlatformInfo>& platforms);
+
         static constexpr int AssetSubId = 1;
         static constexpr int ExtraAssetSubId = 2;
         static constexpr int MetadataProcessingDelayMs = 1;
@@ -118,7 +120,7 @@ namespace UnitTests
         void ProcessFileMultiStage(
             int endStage,
             bool doProductOutputCheck,
-            const char* file = nullptr,
+            AssetProcessor::SourceAssetReference = {},
             int startStage = 1,
             bool expectAutofail = false,
             bool hasExtraFile = false);

+ 105 - 2
Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp

@@ -24,6 +24,7 @@
 #include <AzCore/Jobs/JobManagerDesc.h>
 #include <AzCore/Utils/Utils.h>
 #include <tests/assetmanager/AssetManagerTestingBase.h>
+#include <utilities/ProductOutputUtil.h>
 
 using namespace AssetProcessor;
 
@@ -312,10 +313,11 @@ TEST_F(AssetProcessorManagerUuid, UuidUpdated_SendsAssetRemovedMessage)
     AZ::IO::Path scanFolderDir(m_scanfolder.m_scanFolder);
     AZStd::string testFilename = "test.in";
     AZ::IO::Path filePath = (scanFolderDir / testFilename).AsPosix();
+    AssetProcessor::SourceAssetReference sourceAsset{ filePath.c_str() };
 
     UnitTestUtils::CreateDummyFileAZ(filePath, "unit test file");
 
-    ProcessFileMultiStage(1, true, filePath.c_str());
+    ProcessFileMultiStage(1, true, sourceAsset);
 
     auto metadataInterface = AZ::Interface<AzToolsFramework::IMetadataRequests>::Get();
 
@@ -348,7 +350,7 @@ TEST_F(AssetProcessorManagerUuid, UuidUpdated_SendsAssetRemovedMessage)
         });
 
     // Run the file again
-    ProcessFileMultiStage(1, true, filePath.c_str());
+    ProcessFileMultiStage(1, true, sourceAsset);
 
     // Verify asset removed and asset changed messages were sent
     std::sort(
@@ -369,6 +371,107 @@ TEST_F(AssetProcessorManagerUuid, UuidUpdated_SendsAssetRemovedMessage)
     EXPECT_EQ(notifications[1].m_type, AzFramework::AssetSystem::AssetNotificationMessage::AssetChanged);
 }
 
+class MetadataOverrides : public UnitTests::AssetManagerTestingBase
+{
+public:
+    void SetUp() override
+    {
+        UnitTests::AssetManagerTestingBase::SetUp();
+
+        using namespace AssetBuilderSDK;
+
+        CreateBuilder("stage1", "*.stage1", "stage2", false, ProductOutputFlags::ProductAsset);
+
+        // Enable metadata for our file type
+        AZ::Interface<IUuidRequests>::Get()->EnableGenerationForTypes({ ".stage1" });
+
+        AZ::IO::Path assetRootDir = m_databaseLocationListener.GetAssetRootDir();
+        m_sourceA = SourceAssetReference{ assetRootDir / "folder" / "file.stage1" };
+        m_sourceB = SourceAssetReference{ assetRootDir / "folder2" / "file.stage1" };
+    }
+
+    void SetupScanfolders(AZ::IO::Path assetRootDir, const AZStd::vector<AssetBuilderSDK::PlatformInfo>& platforms)
+    {
+        UnitTests::AssetManagerTestingBase::SetupScanfolders(assetRootDir, platforms);
+
+        m_platformConfig->AddScanFolder(
+            AssetProcessor::ScanFolderInfo{ (assetRootDir / "folder2").c_str(), "folder2", "folder2", false, true, platforms });
+    }
+
+    void VerifyProducts()
+    {
+        AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer products;
+        EXPECT_TRUE(this->m_stateData->GetProductsByProductName("pc/file.stage2", products));
+        EXPECT_EQ(products.size(), 1);
+
+        products = {};
+        EXPECT_TRUE(m_stateData->GetProductsByProductName(
+            AZStd::string::format("pc/%sfile.stage2", ProductOutputUtil::GetPrefix(m_sourceB.ScanFolderId()).c_str()).c_str(), products));
+        EXPECT_EQ(products.size(), 1);
+
+        auto io = AZ::IO::FileIOBase::GetInstance();
+        EXPECT_TRUE(io->Exists(MakePath("file.stage2", false).c_str()));
+        EXPECT_FALSE(io->Exists(MakePath("(2)file.stage2", false).c_str()));
+        EXPECT_TRUE(io->Exists(MakePath("(3)file.stage2", false).c_str()));
+
+        auto fileContentsResult = AZ::Utils::ReadFile(MakePath("file.stage2", false).c_str());
+
+        ASSERT_TRUE(fileContentsResult);
+        EXPECT_STREQ(fileContentsResult.GetValue().c_str(), "unit test file A");
+    }
+
+    AssetProcessor::SourceAssetReference m_sourceA;
+    AssetProcessor::SourceAssetReference m_sourceB;
+};
+
+TEST_F(MetadataOverrides, MetadataOverrides_HighestPriorityProcessedFirst_OutputsCorrectly)
+{
+    // Create 2 source files with the same relative name, one in each scanfolder
+    AZ::Utils::WriteFile("unit test file A", m_sourceA.AbsolutePath().c_str());
+    AZ::Utils::WriteFile("unit test file B", m_sourceB.AbsolutePath().c_str());
+
+    // Process both files
+    ProcessFileMultiStage(1, true, m_sourceA);
+    ProcessFileMultiStage(1, true, m_sourceB);
+
+    VerifyProducts();
+}
+
+TEST_F(MetadataOverrides, MetadataOverrides_LowestPriorityProcessedFirst_OutputsCorrectly)
+{
+    // Create 2 source files with the same relative name, one in each scanfolder
+    AZ::Utils::WriteFile("unit test file A", m_sourceA.AbsolutePath().c_str());
+    AZ::Utils::WriteFile("unit test file B", m_sourceB.AbsolutePath().c_str());
+
+    // Process both files
+    ProcessFileMultiStage(1, false, m_sourceB);
+
+    auto io = AZ::IO::FileIOBase::GetInstance();
+    EXPECT_FALSE(io->Exists(MakePath("file.stage2", false).c_str()));
+    EXPECT_TRUE(io->Exists(MakePath("(3)file.stage2", false).c_str()));
+
+    ProcessFileMultiStage(1, false, m_sourceA);
+
+    VerifyProducts();
+}
+
+TEST_F(MetadataOverrides, MetadataOverrides_LowestPriorityCreatedFirst_OutputsCorrectly)
+{
+    // Create and process the low priority file first
+    AZ::Utils::WriteFile("unit test file B", m_sourceB.AbsolutePath().c_str());
+    ProcessFileMultiStage(1, false, m_sourceB);
+
+    auto io = AZ::IO::FileIOBase::GetInstance();
+    EXPECT_TRUE(io->Exists(MakePath("file.stage2", false).c_str()));
+    EXPECT_FALSE(io->Exists(MakePath("(3)file.stage2", false).c_str()));
+
+    // Create and process the high priority file second
+    AZ::Utils::WriteFile("unit test file A", m_sourceA.AbsolutePath().c_str());
+    ProcessFileMultiStage(1, false, m_sourceA);
+
+    VerifyProducts();
+}
+
 using AssetProcessorManagerFinishTests = UnitTests::AssetManagerTestingBase;
 
 TEST_F(AssetProcessorManagerFinishTests, IntermediateAsset_AnalysisCountHitsZero)

+ 9 - 9
Code/Tools/AssetProcessor/native/tests/assetmanager/IntermediateAssetTests.cpp

@@ -97,7 +97,7 @@ namespace UnitTests
         auto builderUuid = builders[0].m_busId;
         auto sourceUuid = AssetUtilities::GetSourceUuid(AssetProcessor::SourceAssetReference(m_testFilePath.c_str()));
         auto actualIntermediateUuid =
-            AssetUtilities::GetSourceUuid(AssetProcessor::SourceAssetReference(MakePath("test.stage2", true).c_str()));
+            AssetUtilities::GetSourceUuid(AssetProcessor::SourceAssetReference(MakePath("(2)test.stage2", true).c_str()));
 
         ASSERT_TRUE(sourceUuid);
         ASSERT_TRUE(actualIntermediateUuid);
@@ -292,7 +292,7 @@ namespace UnitTests
 
         UnitTestUtils::CreateDummyFile(testFilePath.c_str(), "unit test file");
 
-        ProcessFileMultiStage(NumberOfStages, true, testFilePath.c_str(), 2);
+        ProcessFileMultiStage(NumberOfStages, true, AssetProcessor::SourceAssetReference(testFilePath.c_str()), 2);
 
         // Now process another file which produces intermediates that conflict with the existing source file above
         // Only go to stage 1 since we're expecting a failure at that point
@@ -327,7 +327,7 @@ namespace UnitTests
 
         UnitTestUtils::CreateDummyFile(testFilePath.c_str(), "unit test file");
 
-        ProcessFileMultiStage(NumberOfStages, true, testFilePath.c_str(), 3);
+        ProcessFileMultiStage(NumberOfStages, true, AssetProcessor::SourceAssetReference(testFilePath.c_str()), 3);
 
         // Now process another file which produces intermediates that conflict with the existing source file above
         // Only go to stage 2 since we're expecting a failure at that point
@@ -364,7 +364,7 @@ namespace UnitTests
 
         UnitTestUtils::CreateDummyFile(testFilePath.c_str(), "unit test file");
 
-        ProcessFileMultiStage(NumberOfStages, true, testFilePath.c_str(), 2);
+        ProcessFileMultiStage(NumberOfStages, true, AssetProcessor::SourceAssetReference(testFilePath.c_str()), 2);
 
         // Now process another file which produces intermediates that conflict with the existing source file above
         // Only go to stage 1 since we're expecting a failure at that point
@@ -486,7 +486,7 @@ namespace UnitTests
 
         UnitTestUtils::CreateDummyFile(testFilePath.c_str(), "unit test file");
 
-        ProcessFileMultiStage(NumberOfStages, true, testFilePath.c_str(), 2, true);
+        ProcessFileMultiStage(NumberOfStages, true, AssetProcessor::SourceAssetReference(testFilePath.c_str()), 2, true);
 
         ASSERT_EQ(m_jobDetailsList.size(), 1);
 
@@ -512,7 +512,7 @@ namespace UnitTests
 
         UnitTestUtils::CreateDummyFile(testFilePath.c_str(), "unit test file");
 
-        ProcessFileMultiStage(NumberOfStages, true, testFilePath.c_str(), 2, true);
+        ProcessFileMultiStage(NumberOfStages, true, AssetProcessor::SourceAssetReference(testFilePath.c_str()), 2, true);
 
         ASSERT_EQ(m_jobDetailsList.size(), 2);
 
@@ -530,7 +530,7 @@ namespace UnitTests
         CreateBuilder("stage1", "*.stage1", "stage2", true, ProductOutputFlags::IntermediateAsset, true);
         CreateBuilder("stage2", "*.stage2", "stage3", false, ProductOutputFlags::ProductAsset);
 
-        ProcessFileMultiStage(2, true, nullptr, 1, false, true);
+        ProcessFileMultiStage(2, true, {}, 1, false, true);
 
         AZ::IO::Path scanFolderDir(m_scanfolder.m_scanFolder);
         AZStd::string testFilename = "test2.stage1";
@@ -575,7 +575,7 @@ namespace UnitTests
         // Process the intermediate-style file first
         ProcessFileMultiStage(2, true);
         // Process the regular source second
-        ProcessFileMultiStage(1, false, (scanFolderDir / testFilename).c_str());
+        ProcessFileMultiStage(1, false, AssetProcessor::SourceAssetReference((scanFolderDir / testFilename).c_str()));
 
         // Modify the intermediate-style file so it will be processed again
         QFile writer(m_testFilePath.c_str());
@@ -617,7 +617,7 @@ namespace UnitTests
         UnitTestUtils::CreateDummyFile((scanFolderDir / testFilename).c_str(), "unit test file");
 
         // Process the normal source first
-        ProcessFileMultiStage(1, false, (scanFolderDir / testFilename).c_str());
+        ProcessFileMultiStage(1, false, AssetProcessor::SourceAssetReference((scanFolderDir / testFilename).c_str()));
         // Process the intermediate-style source second
         ProcessFileMultiStage(2, true);
 

+ 147 - 0
Code/Tools/AssetProcessor/native/utilities/ProductOutputUtil.cpp

@@ -0,0 +1,147 @@
+/*
+ * 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 <native/utilities/ProductOutputUtil.h>
+#include <QString>
+#include <AzCore/std/containers/vector.h>
+#include <AzFramework/IO/FileOperations.h>
+#include <native/utilities/UuidManager.h>
+
+namespace AssetProcessor
+{
+    AZStd::string ProductOutputUtil::GetPrefix(AZ::s64 scanfolderId)
+    {
+        return AZStd::string::format("(%" PRIu64 ")", scanfolderId);
+    }
+
+    void ProductOutputUtil::ModifyProductPath(QString& outputFilename, AZ::s64 sourceScanfolderId)
+    {
+        auto prefix = GetPrefix(sourceScanfolderId);
+        outputFilename = QStringLiteral("%1%2").arg(prefix.c_str()).arg(outputFilename);
+    }
+
+    void ProductOutputUtil::FinalizeProduct(AZStd::shared_ptr<AssetProcessor::AssetDatabaseConnection> db, const PlatformConfiguration* platformConfig, const SourceAssetReference& sourceAsset, AZStd::vector<AssetBuilderSDK::JobProduct>& products, AZStd::string_view platformIdentifier)
+    {
+        auto* uuidInterface = AZ::Interface<IUuidRequests>::Get();
+        AZ_Assert(uuidInterface, "Programmer Error - IUuidRequests interface is not available.");
+
+        if (uuidInterface->IsGenerationEnabledForFile(sourceAsset.AbsolutePath()))
+        {
+            QString overrider =
+                platformConfig->GetOverridingFile(sourceAsset.RelativePath().c_str(), sourceAsset.ScanFolderPath().c_str());
+
+            if (overrider.isEmpty())
+            {
+                // There is no other file or this is the highest priority
+                for (auto& product : products)
+                {
+                    QString scanfolder, relativePath;
+
+                    if (!AZ::IO::PathView(product.m_productFileName).IsRelative())
+                    {
+                        platformConfig->ConvertToRelativePath(product.m_productFileName.c_str(), scanfolder, relativePath);
+                    }
+                    else
+                    {
+                        relativePath = product.m_productFileName.c_str();
+                    }
+
+                    AZStd::string prefix = GetPrefix(sourceAsset.ScanFolderId());
+                    int prefixPos = relativePath.indexOf(prefix.c_str());
+
+                    if (prefixPos < 0)
+                    {
+                        AZ_Error(
+                            "OutputManager",
+                            false,
+                            "Product " AZ_STRING_FORMAT " is expected to be prefixed but was not",
+                            AZ_STRING_ARG(product.m_productFileName));
+                        continue;
+                    }
+
+                    // Remove the prefix and update
+                    QStringRef unprefixedString = relativePath.midRef(prefixPos + prefix.size());
+                    AZStd::string newName =
+                        (AZ::IO::FixedMaxPath(scanfolder.toUtf8().constData()) / unprefixedString.toUtf8().constData()).c_str();
+
+                    AssetUtilities::ProductPath oldProductPath(product.m_productFileName, platformIdentifier);
+                    AssetUtilities::ProductPath newProductPath(newName, platformIdentifier);
+                    AssetProcessor::ProductAssetWrapper wrapper{ product, oldProductPath };
+
+                    auto oldAbsolutePath =
+                        wrapper.HasCacheProduct() ? oldProductPath.GetCachePath() : oldProductPath.GetIntermediatePath();
+                    auto newAbsolutePath =
+                        wrapper.HasCacheProduct() ? newProductPath.GetCachePath() : newProductPath.GetIntermediatePath();
+
+                    product.m_productFileName = newName;
+
+                    // Find any other sources which output the non-prefixed product
+                    AzToolsFramework::AssetDatabase::ProductDatabaseEntryContainer existingProducts;
+                    if (db->GetProductsByProductName(newProductPath.GetDatabasePath().c_str(), existingProducts))
+                    {
+                        for (const auto& existingProduct : existingProducts)
+                        {
+                            AzToolsFramework::AssetDatabase::SourceDatabaseEntry existingSource;
+                            if (db->GetSourceByProductID(existingProduct.m_productID, existingSource))
+                            {
+                                if (existingSource.m_scanFolderPK != sourceAsset.ScanFolderId() ||
+                                    AZ::IO::PathView(existingSource.m_sourceName) != sourceAsset.RelativePath())
+                                {
+                                    // Found a different source already using this product name
+                                    // This should be a previously-higher priority override
+                                    // Rename the existing file/product entries from non-prefixed to prefixed version
+                                    RenameProduct(db, existingProduct, existingSource, platformIdentifier);
+                                }
+                            }
+                        }
+                    }
+
+                    auto result = AZ::IO::SmartMove(oldAbsolutePath.c_str(), newAbsolutePath.c_str());
+
+                    if (!result)
+                    {
+                        AZ_Error(
+                            "OutputManager",
+                            false,
+                            "Failed to move product from " AZ_STRING_FORMAT " to " AZ_STRING_FORMAT,
+                            AZ_STRING_ARG(oldAbsolutePath),
+                            AZ_STRING_ARG(newAbsolutePath));
+                    }
+                }
+            }
+        }
+    }
+
+    void ProductOutputUtil::RenameProduct(AZStd::shared_ptr<AssetProcessor::AssetDatabaseConnection> db, AzToolsFramework::AssetDatabase::ProductDatabaseEntry existingProduct, const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry, AZStd::string_view platformIdentifier)
+    {
+        auto oldProductPath = AssetUtilities::ProductPath::FromDatabasePath(existingProduct.m_productName);
+        QString newName = oldProductPath.GetRelativePath().c_str();
+        ModifyProductPath(newName, sourceEntry.m_scanFolderPK);
+
+        AssetUtilities::ProductPath newProductPath(newName.toUtf8().constData(), platformIdentifier);
+        AssetProcessor::ProductAssetWrapper wrapper{ existingProduct, oldProductPath };
+
+        auto oldAbsolutePath = wrapper.HasCacheProduct() ? oldProductPath.GetCachePath() : oldProductPath.GetIntermediatePath();
+        auto newAbsolutePath = wrapper.HasCacheProduct() ? newProductPath.GetCachePath() : newProductPath.GetIntermediatePath();
+
+        auto result = AZ::IO::SmartMove(oldAbsolutePath.c_str(), newAbsolutePath.c_str());
+
+        if (!result)
+        {
+            AZ_Error(
+                "OutputManager",
+                false,
+                "Failed to move product from " AZ_STRING_FORMAT " to " AZ_STRING_FORMAT,
+                AZ_STRING_ARG(oldAbsolutePath),
+                AZ_STRING_ARG(newAbsolutePath));
+        }
+
+        existingProduct.m_productName = newProductPath.GetDatabasePath();
+        db->SetProduct(existingProduct);
+    }
+} // namespace AssetProcessor

+ 43 - 0
Code/Tools/AssetProcessor/native/utilities/ProductOutputUtil.h

@@ -0,0 +1,43 @@
+/*
+ * 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
+ *
+ */
+
+#pragma once
+
+#include <AssetBuilderSDK/AssetBuilderSDK.h>
+#include <native/AssetDatabase/AssetDatabase.h>
+#include <native/AssetManager/ProductAsset.h>
+#include <native/utilities/PlatformConfiguration.h>
+
+namespace AssetProcessor
+{
+    struct ProductOutputUtil
+    {
+        static AZStd::string GetPrefix(AZ::s64 scanfolderId);
+
+        // Modifies product paths to have the scanfolder prepended to the filename
+        // This needs to be done before the files are copied into the cache to avoid overwriting the legacy non-prepended version
+        static void ModifyProductPath(QString& outputFilename, AZ::s64 sourceScanfolderId);
+
+        // For meta types, does an override check to see if the provided source is the highest priority
+        // If so, the products are renamed back to the non-prefixed version and any existing un-prefixed products
+        // are prefixed
+        static void FinalizeProduct(
+            AZStd::shared_ptr<AssetDatabaseConnection> db,
+            const PlatformConfiguration* platformConfig,
+            const SourceAssetReference& sourceAsset,
+            AZStd::vector<AssetBuilderSDK::JobProduct>& products,
+            AZStd::string_view platformIdentifier);
+
+    protected:
+        static void RenameProduct(
+            AZStd::shared_ptr<AssetDatabaseConnection> db,
+            AzToolsFramework::AssetDatabase::ProductDatabaseEntry existingProduct,
+            const AzToolsFramework::AssetDatabase::SourceDatabaseEntry& sourceEntry,
+            AZStd::string_view platformIdentifier);
+    };
+} // namespace AssetProcessor

+ 1 - 0
Code/Tools/AssetProcessor/native/utilities/UuidManager.cpp

@@ -382,6 +382,7 @@ namespace AssetProcessor
 
         return newUuid;
     }
+
     AZ::Outcome<void, AZStd::string> UuidManager::CacheUuidEntry(AZ::IO::PathView normalizedPath, AzToolsFramework::MetaUuidEntry entry, bool enabledType)
     {
         if (enabledType)