Browse Source

project manager remote gem version support

- remote gem versions can be viewed, downloaded, uninstalled and updated
- currently only .zip download is supported, there is no "clone" button on the gem inspector yet

Signed-off-by: Alex Peterson <[email protected]>
Alex Peterson 2 năm trước cách đây
mục cha
commit
c8a0111ef3

+ 11 - 7
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp

@@ -435,9 +435,9 @@ namespace O3DE::ProjectManager
         ShowInspector();
     }
 
-    void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex)
+    void GemCatalogScreen::UpdateGem(const QModelIndex& modelIndex, const QString& version, const QString& path)
     {
-        const GemInfo& gemInfo = m_gemModel->GetGemInfo(modelIndex);
+        const GemInfo& gemInfo = m_gemModel->GetGemInfo(modelIndex, version, path);
 
         if (!gemInfo.m_repoUri.isEmpty())
         {
@@ -470,21 +470,25 @@ namespace O3DE::ProjectManager
             }
         }
 
+        QString gemNameWithVersionSpecifier = version.isEmpty() ? gemInfo.m_name : QString("%1==%2").arg(gemInfo.m_name, version);
+
         // Check if there is an update avaliable now that repo is refreshed
-        bool updateAvaliable = PythonBindingsInterface::Get()->IsGemUpdateAvaliable(gemInfo.m_name, gemInfo.m_lastUpdatedDate);
+        bool updateAvaliable = PythonBindingsInterface::Get()->IsGemUpdateAvaliable(gemNameWithVersionSpecifier, gemInfo.m_lastUpdatedDate);
 
         GemUpdateDialog* confirmUpdateDialog = new GemUpdateDialog(gemInfo.m_name, updateAvaliable, this);
         if (confirmUpdateDialog->exec() == QDialog::Accepted)
         {
-            m_downloadController->AddObjectDownload(gemInfo.m_name, "" , DownloadController::DownloadObjectType::Gem);
+            m_downloadController->AddObjectDownload(gemNameWithVersionSpecifier, "" , DownloadController::DownloadObjectType::Gem);
         }
     }
 
     void GemCatalogScreen::DownloadGem(const QModelIndex& modelIndex, const QString& version, const QString& path)
     {
-        const QString gemDisplayName = m_gemModel->GetDisplayName(modelIndex);
         const GemInfo& gemInfo = m_gemModel->GetGemInfo(modelIndex, version, path);
-        m_downloadController->AddObjectDownload(gemInfo.m_name, "" , DownloadController::DownloadObjectType::Gem);
+        QString downloadName = version.isEmpty() ? gemInfo.m_name : QString("%1==%2").arg(gemInfo.m_name, version);
+
+        // download to the default path
+        m_downloadController->AddObjectDownload(downloadName, "" , DownloadController::DownloadObjectType::Gem);
     }
 
     void GemCatalogScreen::UninstallGem(const QModelIndex& modelIndex, const QString& path)
@@ -613,7 +617,7 @@ namespace O3DE::ProjectManager
         {
             m_gemModel->AddGems(allGemInfosResult.GetValue());
 
-            const auto& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos();
+            const auto& allRepoGemInfosResult = PythonBindingsInterface::Get()->GetGemInfosForAllRepos(projectPath);
             if (allRepoGemInfosResult.IsSuccess())
             {
                 m_gemModel->AddGems(allRepoGemInfosResult.GetValue());

+ 1 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h

@@ -59,7 +59,7 @@ namespace O3DE::ProjectManager
         void SelectGem(const QString& gemName);
         void OnGemDownloadResult(const QString& gemName, bool succeeded = true);
         void Refresh();
-        void UpdateGem(const QModelIndex& modelIndex);
+        void UpdateGem(const QModelIndex& modelIndex, const QString& version, const QString& path);
         void UninstallGem(const QModelIndex& modelIndex, const QString& path);
         void DownloadGem(const QModelIndex& modelIndex, const QString& version, const QString& path);
         void HandleGemCreated(const GemInfo& gemInfo);

+ 1 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.cpp

@@ -438,7 +438,7 @@ namespace O3DE::ProjectManager
         m_updateGemButton = new QPushButton(tr("Update Gem"));
         m_updateGemButton->setProperty("secondary", true);
         m_mainLayout->addWidget(m_updateGemButton);
-        connect(m_updateGemButton, &QPushButton::clicked, this , [this]{ emit UpdateGem(m_curModelIndex); });
+        connect(m_updateGemButton, &QPushButton::clicked, this , [this]{ emit UpdateGem(m_curModelIndex, GetVersion(), GetVersionPath()); });
 
         m_mainLayout->addSpacing(10);
 

+ 1 - 1
Code/Tools/ProjectManager/Source/GemCatalog/GemInspector.h

@@ -69,7 +69,7 @@ namespace O3DE::ProjectManager
 
     signals:
         void TagClicked(const Tag& tag);
-        void UpdateGem(const QModelIndex& modelIndex);
+        void UpdateGem(const QModelIndex& modelIndex, const QString& version = "", const QString& path = "");
         void UninstallGem(const QModelIndex& modelIndex, const QString& path = "");
         void EditGem(const QModelIndex& modelIndex, const QString& path = "");
         void DownloadGem(const QModelIndex& modelIndex, const QString& version = "", const QString& path = "");

+ 3 - 0
Code/Tools/ProjectManager/Source/ProjectInfo.h

@@ -56,6 +56,9 @@ namespace O3DE::ProjectManager
         QString m_license;
         QStringList m_userTags;
 
+        QStringList m_requiredGemDependencies;
+        QStringList m_optionalGemDependencies;
+
         // Used as temp variable for replace images
         QString m_newPreviewImagePath;
         QString m_newBackgroundImagePath;

+ 6 - 3
Code/Tools/ProjectManager/Source/ProjectsScreen.cpp

@@ -378,10 +378,13 @@ namespace O3DE::ProjectManager
             }
 
             // Setup building button again
-            auto buildProjectIter = m_projectButtons.find(buildProjectPath);
-            if (buildProjectIter != m_projectButtons.end())
+            if (!buildProjectPath.empty())
             {
-                m_currentBuilder->SetProjectButton(buildProjectIter->second);
+                auto buildProjectIter = m_projectButtons.find(buildProjectPath);
+                if (buildProjectIter != m_projectButtons.end())
+                {
+                    m_currentBuilder->SetProjectButton(buildProjectIter->second);
+                }
             }
 
             for (const ProjectInfo& project : m_buildQueue)

+ 123 - 65
Code/Tools/ProjectManager/Source/PythonBindings.cpp

@@ -1072,6 +1072,28 @@ namespace O3DE::ProjectManager
             }
         }
 
+        if (projectData.contains("gem_names"))
+        {
+            for (auto gem : projectData["gem_names"])
+            {
+                if (pybind11::isinstance<pybind11::dict>(gem))
+                {
+                    if (gem["optional"].cast<bool>())
+                    {
+                        projectInfo.m_optionalGemDependencies.append(Py_To_String(gem["name"]));
+                    }
+                    else
+                    {
+                        projectInfo.m_requiredGemDependencies.append(Py_To_String(gem["name"]));
+                    }
+                }
+                else
+                {
+                    projectInfo.m_requiredGemDependencies.append(Py_To_String(gem));
+                }
+            }
+        }
+
         if (projectData.contains("engine_path"))
         {
             // Python looked for an engine path so we don't need to, but be careful
@@ -1081,7 +1103,7 @@ namespace O3DE::ProjectManager
                 projectInfo.m_enginePath = Py_To_String(projectData["engine_path"]);
             }
         }
-        else
+        else if (!projectInfo.m_path.isEmpty())
         {
             auto enginePathResult = m_manifest.attr("get_project_engine_path")(QString_To_Py_Path(projectInfo.m_path));
             if (!pybind11::isinstance<pybind11::none>(enginePathResult))
@@ -1149,13 +1171,12 @@ namespace O3DE::ProjectManager
             [&]
             {
                 auto pyUri = QString_To_Py_String(repoUri);
-                auto projectPaths = m_repo.attr("get_project_json_paths_from_cached_repo")(pyUri);
-
-                if (pybind11::isinstance<pybind11::set>(projectPaths))
+                auto pyProjects = m_repo.attr("get_project_json_data_from_cached_repo")(pyUri);
+                if (pybind11::isinstance<pybind11::list>(pyProjects))
                 {
-                    for (auto path : projectPaths)
+                    for (auto pyProjectJsonData : pyProjects)
                     {
-                        ProjectInfo projectInfo = ProjectInfoFromPath(path);
+                        ProjectInfo projectInfo = ProjectInfoFromDict(pyProjectJsonData);
                         projectInfo.m_remote = true;
                         projects.push_back(projectInfo);
                     }
@@ -1172,19 +1193,18 @@ namespace O3DE::ProjectManager
 
     AZ::Outcome<QVector<ProjectInfo>, AZStd::string> PythonBindings::GetProjectsForAllRepos()
     {
-        QVector<ProjectInfo> projectInfos;
+        QVector<ProjectInfo> projects;
         AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
             [&]
             {
-                auto projectPaths = m_repo.attr("get_project_json_paths_from_all_cached_repos")();
-
-                if (pybind11::isinstance<pybind11::set>(projectPaths))
+                auto pyProjects = m_repo.attr("get_project_json_data_from_all_cached_repos")();
+                if (pybind11::isinstance<pybind11::list>(pyProjects))
                 {
-                    for (auto path : projectPaths)
+                    for (auto pyProjectJsonData : pyProjects)
                     {
-                        ProjectInfo projectInfo = ProjectInfoFromPath(path);
+                        ProjectInfo projectInfo = ProjectInfoFromDict(pyProjectJsonData);
                         projectInfo.m_remote = true;
-                        projectInfos.push_back(projectInfo);
+                        projects.push_back(projectInfo);
                     }
                 }
             });
@@ -1194,7 +1214,7 @@ namespace O3DE::ProjectManager
             return AZ::Failure(result.GetError());
         }
 
-        return AZ::Success(AZStd::move(projectInfos));
+        return AZ::Success(AZStd::move(projects));
     }
 
     IPythonBindings::DetailedOutcome PythonBindings::AddGemsToProject(const QStringList& gemPaths, const QStringList& gemNames, const QString& projectPath, bool force)
@@ -1305,6 +1325,31 @@ namespace O3DE::ProjectManager
         return AZ::Success();
     }
 
+    ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromDict(pybind11::handle templateData, const QString& path) const
+    {
+        ProjectTemplateInfo templateInfo(TemplateInfoFromDict(templateData, path));
+        if (templateInfo.IsValid())
+        {
+            QString templateProjectPath = QDir(templateInfo.m_path).filePath("Template");
+            constexpr bool includeDependencies = false;
+            auto enabledGems = GetEnabledGems(templateProjectPath, includeDependencies);
+            if (enabledGems)
+            {
+                for (auto gemName : enabledGems.GetValue().keys())
+                {
+                    // Exclude the template ${Name} placeholder for the list of included gems
+                    // That Gem gets created with the project
+                    if (!gemName.contains("${Name}"))
+                    {
+                        templateInfo.m_includedGems.push_back(gemName);
+                    }
+                }
+            }
+        }
+
+        return templateInfo;
+    }
+
     ProjectTemplateInfo PythonBindings::ProjectTemplateInfoFromPath(pybind11::handle path) const
     {
         ProjectTemplateInfo templateInfo(TemplateInfoFromPath(path));
@@ -1330,6 +1375,45 @@ namespace O3DE::ProjectManager
         return templateInfo;
     }
 
+    TemplateInfo PythonBindings::TemplateInfoFromDict(pybind11::handle data, const QString& path) const
+    {
+        TemplateInfo templateInfo;
+        if (!path.isEmpty())
+        {
+            templateInfo.m_path = path;
+        }
+        else
+        {
+            templateInfo.m_path = Py_To_String_Optional(data, "path", templateInfo.m_path);
+        }
+
+        templateInfo.m_displayName = Py_To_String(data["display_name"]);
+        templateInfo.m_name = Py_To_String(data["template_name"]);
+        templateInfo.m_summary = Py_To_String(data["summary"]);
+
+        if (data.contains("canonical_tags"))
+        {
+            for (auto tag : data["canonical_tags"])
+            {
+                templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
+            }
+        }
+
+        if (data.contains("user_tags"))
+        {
+            for (auto tag : data["user_tags"])
+            {
+                templateInfo.m_userTags.push_back(Py_To_String(tag));
+            }
+        }
+
+
+        templateInfo.m_requirements = Py_To_String_Optional(data, "requirements", "");
+        templateInfo.m_license = Py_To_String_Optional(data, "license", "");
+
+        return templateInfo;
+    }
+
     TemplateInfo PythonBindings::TemplateInfoFromPath(pybind11::handle path) const
     {
         TemplateInfo templateInfo;
@@ -1343,29 +1427,7 @@ namespace O3DE::ProjectManager
         {
             try
             {
-                templateInfo.m_displayName = Py_To_String(data["display_name"]);
-                templateInfo.m_name = Py_To_String(data["template_name"]);
-                templateInfo.m_summary = Py_To_String(data["summary"]);
-
-                if (data.contains("canonical_tags"))
-                {
-                    for (auto tag : data["canonical_tags"])
-                    {
-                        templateInfo.m_canonicalTags.push_back(Py_To_String(tag));
-                    }
-                }
-
-                if (data.contains("user_tags"))
-                {
-                    for (auto tag : data["user_tags"])
-                    {
-                        templateInfo.m_userTags.push_back(Py_To_String(tag));
-                    }
-                }
-
-
-                templateInfo.m_requirements = Py_To_String_Optional(data, "requirements", "");
-                templateInfo.m_license = Py_To_String_Optional(data, "license", "");
+                templateInfo = TemplateInfoFromDict(data, Py_To_String(path));
             }
             catch ([[maybe_unused]] const std::exception& e)
             {
@@ -1428,16 +1490,15 @@ namespace O3DE::ProjectManager
             [&]
             {
                 using namespace pybind11::literals;
-
-                auto templatePaths = m_repo.attr("get_template_json_paths_from_cached_repo")(
+                auto pyTemplates = m_repo.attr("get_template_json_data_from_cached_repo")(
                     "repo_uri"_a = QString_To_Py_String(repoUri)
                     );
 
-                if (pybind11::isinstance<pybind11::set>(templatePaths))
+                if (pybind11::isinstance<pybind11::set>(pyTemplates))
                 {
-                    for (auto path : templatePaths)
+                    for (auto pyTemplateJsonData : pyTemplates)
                     {
-                        ProjectTemplateInfo remoteTemplate = ProjectTemplateInfoFromPath(path);
+                        ProjectTemplateInfo remoteTemplate = TemplateInfoFromDict(pyTemplateJsonData);
                         remoteTemplate.m_isRemote = true;
                         templates.push_back(remoteTemplate);
                     }
@@ -1461,13 +1522,12 @@ namespace O3DE::ProjectManager
         bool result = ExecuteWithLock(
             [&]
             {
-                auto templatePaths = m_repo.attr("get_template_json_paths_from_all_cached_repos")();
-
-                if (pybind11::isinstance<pybind11::set>(templatePaths))
+                auto pyTemplates = m_repo.attr("get_template_json_data_from_all_cached_repos")();
+                if (pybind11::isinstance<pybind11::list>(pyTemplates))
                 {
-                    for (auto path : templatePaths)
+                    for (auto pyTemplateJsonData : pyTemplates)
                     {
-                        ProjectTemplateInfo remoteTemplate = ProjectTemplateInfoFromPath(path);
+                        ProjectTemplateInfo remoteTemplate = ProjectTemplateInfoFromDict(pyTemplateJsonData);
                         remoteTemplate.m_isRemote = true;
                         templates.push_back(remoteTemplate);
                     }
@@ -1692,13 +1752,13 @@ namespace O3DE::ProjectManager
             [&]
             {
                 auto pyUri = QString_To_Py_String(repoUri);
-                auto gemPaths = m_repo.attr("get_gem_json_paths_from_cached_repo")(pyUri);
-
-                if (pybind11::isinstance<pybind11::set>(gemPaths))
+                auto pyGems = m_repo.attr("get_gem_json_data_from_cached_repo")(pyUri);
+                if (pybind11::isinstance<pybind11::list>(pyGems))
                 {
-                    for (auto path : gemPaths)
+                    for (auto pyGemJsonData : pyGems)
                     {
-                        GemInfo gemInfo = GemInfoFromPath(path, pybind11::none());
+                        GemInfo gemInfo;
+                        GetGemInfoFromPyDict(gemInfo, pyGemJsonData.cast<pybind11::dict>());
                         gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
                         gemInfo.m_gemOrigin = GemInfo::Remote;
                         gemInfos.push_back(AZStd::move(gemInfo));
@@ -1714,23 +1774,21 @@ namespace O3DE::ProjectManager
         return AZ::Success(AZStd::move(gemInfos));
     }
 
-    AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemInfosForAllRepos()
+    AZ::Outcome<QVector<GemInfo>, AZStd::string> PythonBindings::GetGemInfosForAllRepos(const QString& projectPath)
     {
-        QVector<GemInfo> gemInfos;
+        QVector<GemInfo> gems;
         AZ::Outcome<void, AZStd::string> result = ExecuteWithLockErrorHandling(
             [&]
             {
-                auto gemPaths = m_repo.attr("get_gem_json_paths_from_all_cached_repos")();
-
-                if (pybind11::isinstance<pybind11::set>(gemPaths))
+                const auto pyProjectPath = QString_To_Py_Path(projectPath);
+                const auto gemInfos = m_projectManagerInterface.attr("get_gem_infos_from_all_repos")(pyProjectPath);
+                for (pybind11::handle pyGemJsonData : gemInfos)
                 {
-                    for (auto path : gemPaths)
-                    {
-                        GemInfo gemInfo = GemInfoFromPath(path, pybind11::none());
-                        gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
-                        gemInfo.m_gemOrigin = GemInfo::Remote;
-                        gemInfos.push_back(AZStd::move(gemInfo));
-                    }
+                    GemInfo gemInfo;
+                    GetGemInfoFromPyDict(gemInfo, pyGemJsonData.cast<pybind11::dict>());
+                    gemInfo.m_downloadStatus = GemInfo::DownloadStatus::NotDownloaded;
+                    gemInfo.m_gemOrigin = GemInfo::Remote;
+                    gems.push_back(AZStd::move(gemInfo));
                 }
             });
 
@@ -1739,7 +1797,7 @@ namespace O3DE::ProjectManager
             return AZ::Failure(result.GetError());
         }
 
-        return AZ::Success(AZStd::move(gemInfos));
+        return AZ::Success(AZStd::move(gems));
     }
 
     IPythonBindings::DetailedOutcome  PythonBindings::DownloadGem(

+ 3 - 1
Code/Tools/ProjectManager/Source/PythonBindings.h

@@ -76,7 +76,7 @@ namespace O3DE::ProjectManager
         bool RemoveGemRepo(const QString& repoUri) override;
         AZ::Outcome<QVector<GemRepoInfo>, AZStd::string> GetAllGemRepoInfos() override;
         AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForRepo(const QString& repoUri) override;
-        AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos() override;
+        AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos(const QString& projectPath) override;
         DetailedOutcome DownloadGem(
             const QString& gemName, const QString& path, std::function<void(int, int)> gemProgressCallback, bool force = false) override;
         DetailedOutcome DownloadProject(
@@ -109,7 +109,9 @@ namespace O3DE::ProjectManager
         ProjectInfo ProjectInfoFromPath(pybind11::handle path);
         ProjectInfo ProjectInfoFromDict(pybind11::handle projectData, const QString& path = {});
         ProjectTemplateInfo ProjectTemplateInfoFromPath(pybind11::handle path) const;
+        ProjectTemplateInfo ProjectTemplateInfoFromDict(pybind11::handle templateData, const QString& path = {}) const;
         TemplateInfo TemplateInfoFromPath(pybind11::handle path) const;
+        TemplateInfo TemplateInfoFromDict(pybind11::handle templateData, const QString& path = {}) const;
         AZ::Outcome<void, AZStd::string> GemRegistration(const QString& gemPath, const QString& projectPath, bool remove = false);
         bool StopPython();
         IPythonBindings::ErrorPair GetErrorPair();

+ 2 - 1
Code/Tools/ProjectManager/Source/PythonBindingsInterface.h

@@ -328,9 +328,10 @@ namespace O3DE::ProjectManager
 
         /**
          * Gathers all gem infos for all gems registered from repos.
+         * @param projectPath an optional project path to use for compatibility information
          * @return A list of gem infos.
          */
-        virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos() = 0;
+        virtual AZ::Outcome<QVector<GemInfo>, AZStd::string> GetGemInfosForAllRepos(const QString& projectPath = "") = 0;
 
         /**
          * Downloads and registers a Gem.

+ 1 - 1
scripts/o3de/o3de/download.py

@@ -127,7 +127,7 @@ def download_o3de_object(object_name: str, default_folder_name: str, dest_path:
     if use_source_control:
         origin_uri = downloadable_object_data.get('source_control_uri')
         if not origin_uri:
-            logger.error(f"Tried to use source control to aquire {object_name} but the 'source_control_uri' is empty or missing.")
+            logger.error(f"Tried to use source control to acquire {object_name} but the 'source_control_uri' is empty or missing.")
             return 1
     else:
         origin_uri = downloadable_object_data.get('download_source_uri')

+ 61 - 9
scripts/o3de/o3de/project_manager_interface.py

@@ -12,7 +12,7 @@ Contains functions for the project manager to call that gather data from o3de sc
 import logging
 import pathlib
 
-from o3de import manifest, utils, compatibility, enable_gem
+from o3de import manifest, utils, compatibility, enable_gem, repo
 
 logger = logging.getLogger('o3de.project_manager_interface')
 logging.basicConfig(format=utils.LOG_FORMAT)
@@ -291,6 +291,14 @@ def get_all_gem_infos(project_path: pathlib.Path or None) -> list:
     # because the project might be using a different engine than the one Project Manager is
     # running out of
     engine_path = manifest.get_project_engine_path(project_path=project_path) if project_path else manifest.get_this_engine_path() 
+    if not engine_path:
+        logger.error("Failed to get engine path for gem info retrieval")
+        return [] 
+
+    engine_json_data = manifest.get_engine_json_data(engine_path=engine_path)
+    if not engine_json_data:
+        logger.error("Failed to get engine json data for gem info retrieval")
+        return [] 
 
     # get all gem json data by path so we can use it for detecting engine and project gems
     # without re-opening and parsing gem.json files again
@@ -308,17 +316,27 @@ def get_all_gem_infos(project_path: pathlib.Path or None) -> list:
     utils.replace_dict_keys_with_value_key(all_gem_json_data, value_key='gem_name', replaced_key_name='path', place_values_in_list=True)
 
     # flatten into a single list
-    all_gem_json_data = [gem_json_data for gem_versions in all_gem_json_data.values() for gem_json_data in gem_versions]
+    all_gem_json_data_list = [gem_json_data for gem_versions in all_gem_json_data.values() for gem_json_data in gem_versions]
 
-    for i, gem_json_data in enumerate(all_gem_json_data):
+    for i, gem_json_data in enumerate(all_gem_json_data_list):
         if project_path:
             if gem_json_data['path'] in project_gem_paths:
-                all_gem_json_data[i]['project_gem'] = True
+                all_gem_json_data_list[i]['project_gem'] = True
         
         if gem_json_data['path'] in engine_gem_paths:
-            all_gem_json_data[i]['engine_gem'] = True
+            all_gem_json_data_list[i]['engine_gem'] = True
+        else:
+            # gather general incompatibility information
+            incompatible_engine =  compatibility.get_incompatible_objects_for_engine(gem_json_data, engine_json_data)
+            if incompatible_engine:
+                all_gem_json_data_list[i]['incompatible_engine'] = incompatible_engine
+
+            incompatible_gem_dependencies = compatibility.get_incompatible_gem_dependencies(gem_json_data, all_gem_json_data)
+            if incompatible_gem_dependencies:
+                all_gem_json_data_list[i]['incompatible_gem_dependencies'] = incompatible_gem_dependencies
+        
 
-    return all_gem_json_data
+    return all_gem_json_data_list
 
 def download_gem(gem_name: str, force_overwrite: bool = False, progress_callback = None):
     """
@@ -387,8 +405,7 @@ def get_gem_infos_from_repo(repo_uri: str) -> list:
     """
     return list()
 
-
-def get_gem_infos_from_all_repos() -> list:
+def get_gem_infos_from_all_repos(project_path:pathlib.Path = None) -> list:
     """
         Call get_gem_json_paths_from_all_cached_repos
 
@@ -396,7 +413,42 @@ def get_gem_infos_from_all_repos() -> list:
 
         :return list of dicts containing gem infos.
     """
-    return list()
+    remote_gem_json_data_list = repo.get_gem_json_data_from_all_cached_repos()
+    if not remote_gem_json_data_list:
+        return list()
+
+    engine_path = manifest.get_project_engine_path(project_path=project_path) if project_path else manifest.get_this_engine_path() 
+    if not engine_path:
+        logger.error("Failed to get engine path for remote gem compatibility checks")
+        return list() 
+
+    engine_json_data = manifest.get_engine_json_data(engine_path=engine_path)
+    if not engine_json_data:
+        logger.error("Failed to get engine json data remote gem compatibility checks")
+        return list() 
+
+    # get all gem json data by path so we can use it for detecting engine and project gems
+    # without re-opening and parsing gem.json files again
+    all_gem_json_data = manifest.get_gems_json_data_by_path(engine_path=engine_path,
+                                                            project_path=project_path,
+                                                            include_engine_gems=True,
+                                                            include_manifest_gems=True)
+
+    # first add all the known gems together before checking compatibility and dependencies
+    for i, gem_json_data in enumerate(remote_gem_json_data_list):
+        all_gem_json_data[i] = gem_json_data
+    
+    # do general compatibility checks - dependency resolution is too slow for now
+    for i, gem_json_data in enumerate(remote_gem_json_data_list):
+        incompatible_engine =  compatibility.get_incompatible_objects_for_engine(gem_json_data, engine_json_data)
+        if incompatible_engine:
+            remote_gem_json_data_list[i]['incompatible_engine'] = incompatible_engine
+
+        incompatible_gem_dependencies = compatibility.get_incompatible_gem_dependencies(gem_json_data, all_gem_json_data)
+        if incompatible_gem_dependencies:
+            remote_gem_json_data_list[i]['incompatible_gem_dependencies'] = incompatible_gem_dependencies
+
+    return remote_gem_json_data_list
 
 
 def refresh_gem_repo(repo_uri: str):

+ 85 - 68
scripts/o3de/o3de/repo.py

@@ -121,21 +121,23 @@ def validate_remote_repo(repo_uri: str, validate_contained_objects: bool = False
 
         if repo_schema_version == REPO_IMPLICIT_SCHEMA_VERSION:
             if download_object_manifests(repo_data) != 0:
+                # we don't issue an error message here because better error messaging is provided
+                # in the download functions themselves
                 return False
-            gem_set = get_gem_json_paths_from_cached_repo(repo_uri)
-            for gem_json in gem_set:
-                if not validation.valid_o3de_gem_json(gem_json):
-                    logger.error(f'Invalid gem JSON - {gem_json} could not be loaded or is missing required values')
+            cached_gems_json_data = get_gem_json_data_from_cached_repo(repo_uri)
+            for gem_json_data in cached_gems_json_data:
+                if not validation.valid_o3de_gem_json_data(gem_json_data):
+                    logger.error(f'Invalid gem JSON - {gem_json_data} is missing required values')
                     return False
-            project_set = get_project_json_paths_from_cached_repo(repo_uri)
-            for project_json in project_set:
-                if not validation.valid_o3de_project_json(project_json):
-                    logger.error(f'Invalid project JSON - {project_json} could not be loaded or is missing required values')
+            cached_project_json_data = get_project_json_data_from_cached_repo(repo_uri)
+            for project_json_data in cached_project_json_data:
+                if not validation.valid_o3de_project_json_data(project_json_data):
+                    logger.error(f'Invalid project JSON - {project_json_data} is missing required values')
                     return False
-            template_set = get_template_json_paths_from_cached_repo(repo_uri)
-            for template_json in template_set:
-                if not validation.valid_o3de_template_json(template_json):
-                    logger.error(f'Invalid template JSON - {template_json} could not be loaded or is missing required values')
+            cached_template_json_data = get_template_json_data_from_cached_repo(repo_uri)
+            for template_json_data in cached_template_json_data:
+                if not validation.valid_o3de_template_json_data(template_json_data):
+                    logger.error(f'Invalid template JSON - {template_json_data} is missing required values')
                     return False
                 
         elif repo_schema_version == REPO_SCHEMA_VERSION_1_0_0:
@@ -229,79 +231,108 @@ def process_add_o3de_repo(file_name: str or pathlib.Path,
             return process_add_o3de_repo(cache_file, repo_set)
     return 0
 
+def get_object_versions_json_data(remote_object_list:list, required_json_key:str = None, required_json_value:str = None) -> list:
+    """
+    Convert a list of remote objects that may have 'versions_data', into a list
+    of object json data with a separate entry for every entry in 'versions_data'
+    or a single entry for every remote object that has no 'versions_data' entries
+    :param remote_object_list The list of remote object json data
+    :param required_json_key Optional required json key to look for in each object
+    :param required_json_value Optional required value if required json key is specified
+    """
+    object_json_data_list = []
+    for remote_object_json_data in remote_object_list:
+        if required_json_key and remote_object_json_data.get(required_json_key, '') != required_json_value:
+            continue
+
+        versions_data = remote_object_json_data.pop('versions_data', None)
+        if versions_data:
+            for version_json_data in versions_data:
+                object_json_data_list.append(remote_object_json_data | version_json_data)
+        else:
+            object_json_data_list.append(remote_object_json_data)
 
-def get_object_json_paths_from_cached_repo(repo_uri: str, repo_key: str, object_manifest_filename: str) -> set:
+    return object_json_data_list
+
+def get_object_json_data_from_cached_repo(repo_uri: str, repo_key: str, object_typename: str, object_validator) -> list or None:
     url = f'{repo_uri}/repo.json'
     cache_file, _ = get_cache_file_uri(url)
 
-    o3de_object_set = set()
+    o3de_object_json_data = list()
 
     file_name = pathlib.Path(cache_file).resolve()
     if not file_name.is_file():
         logger.error(f'Could not find cached repository json file for {repo_uri}. Try refreshing the repository.')
-        return o3de_object_set
+        return None
 
     with file_name.open('r') as f:
         try:
             repo_data = json.load(f)
         except json.JSONDecodeError as e:
             logger.error(f'{file_name} failed to load: {str(e)}')
-            return o3de_object_set
+            return None
 
         # Get list of objects, then add all json paths to the list if they exist in the cache
         repo_objects = []
         try:
-            repo_objects.append((repo_data[repo_key], object_manifest_filename + '.json'))
+            repo_objects.append((repo_data[repo_key], object_typename + '.json'))
         except KeyError:
             pass
 
-        for o3de_object_uris, manifest_json in repo_objects:
-            for o3de_object_uri in o3de_object_uris:
-                manifest_json_uri = f'{o3de_object_uri}/{manifest_json}'
-                cache_object_json_filepath, _ = get_cache_file_uri(manifest_json_uri)
-                
-                if cache_object_json_filepath.is_file():
-                    o3de_object_set.add(cache_object_json_filepath)
-                else:
-                    logger.warning(f'Could not find cached {repo_key} json file {cache_object_json_filepath} for {o3de_object_uri} in repo {repo_uri}')
+        repo_schema_version = get_repo_schema_version(repo_data)
+        if repo_schema_version == REPO_IMPLICIT_SCHEMA_VERSION:        
+            for o3de_object_uris, manifest_json in repo_objects:
+                for o3de_object_uri in o3de_object_uris:
+                    manifest_json_uri = f'{o3de_object_uri}/{manifest_json}'
+                    cache_object_json_filepath, _ = get_cache_file_uri(manifest_json_uri)
+                    
+                    if cache_object_json_filepath.is_file():
+                        json_data = manifest.get_json_data_file(cache_object_json_filepath, object_typename, object_validator)
+                        # validation errors will be logged via the function above
+                        if json_data:
+                            o3de_object_json_data.append(json_data)
+                    else:
+                        logger.warning(f'Could not find cached {repo_key} json file {cache_object_json_filepath} for {o3de_object_uri} in repo {repo_uri}')
+        elif repo_schema_version == REPO_SCHEMA_VERSION_1_0_0:
+            o3de_object_json_data.extend(get_object_versions_json_data(repo_data[repo_key]))
 
-    return o3de_object_set
+    return o3de_object_json_data
 
-def get_gem_json_paths_from_cached_repo(repo_uri: str) -> set:
-    return get_object_json_paths_from_cached_repo(repo_uri, 'gems', 'gem')
+def get_gem_json_data_from_cached_repo(repo_uri: str) -> list:
+    return get_object_json_data_from_cached_repo(repo_uri, 'gems', 'gem', validation.valid_o3de_gem_json)
 
-def get_gem_json_paths_from_all_cached_repos() -> set:
-    json_data = manifest.load_o3de_manifest()
-    gem_set = set()
+def get_gem_json_data_from_all_cached_repos() -> list:
+    manifest_json_data = manifest.load_o3de_manifest()
+    gems_json_data = list()
 
-    for repo_uri in json_data.get('repos', []):
-        gem_set.update(get_gem_json_paths_from_cached_repo(repo_uri))
+    for repo_uri in manifest_json_data.get('repos', []):
+        gems_json_data.extend(get_gem_json_data_from_cached_repo(repo_uri))
 
-    return gem_set
+    return gems_json_data
 
-def get_project_json_paths_from_cached_repo(repo_uri: str) -> set:
-    return get_object_json_paths_from_cached_repo(repo_uri, 'projects', 'project')
+def get_project_json_data_from_cached_repo(repo_uri: str) -> list:
+    return get_object_json_data_from_cached_repo(repo_uri, 'projects', 'project', validation.valid_o3de_project_json)
 
-def get_project_json_paths_from_all_cached_repos() -> set:
-    json_data = manifest.load_o3de_manifest()
-    project_set = set()
+def get_project_json_data_from_all_cached_repos() -> list:
+    manifest_json_data = manifest.load_o3de_manifest()
+    projects_json_data = list()
 
-    for repo_uri in json_data.get('repos', []):
-        project_set.update(get_project_json_paths_from_cached_repo(repo_uri))
+    for repo_uri in manifest_json_data.get('repos', []):
+        projects_json_data.extend(get_project_json_data_from_cached_repo(repo_uri))
 
-    return project_set
+    return projects_json_data
 
-def get_template_json_paths_from_cached_repo(repo_uri: str) -> set:
-    return get_object_json_paths_from_cached_repo(repo_uri, 'templates', 'template')
+def get_template_json_data_from_cached_repo(repo_uri: str) -> list:
+    return get_object_json_data_from_cached_repo(repo_uri, 'templates', 'template', validation.valid_o3de_template_json)
 
-def get_template_json_paths_from_all_cached_repos() -> set:
-    json_data = manifest.load_o3de_manifest()
-    template_set = set()
+def get_template_json_data_from_all_cached_repos() -> list:
+    manifest_json_data = manifest.load_o3de_manifest()
+    templates_json_data = list()
 
-    for repo_uri in json_data.get('repos', []):
-        template_set.update(get_template_json_paths_from_cached_repo(repo_uri))
+    for repo_uri in manifest_json_data.get('repos', []):
+        templates_json_data.extend(get_template_json_data_from_cached_repo(repo_uri))
 
-    return template_set
+    return templates_json_data
 
 def refresh_repo(repo_uri: str,
                  repo_set: set = None) -> int:
@@ -399,26 +430,12 @@ def search_o3de_repo_for_object(repo_json_data: dict, manifest_attribute:str, ta
 
     target_name_without_version_specifier, _ = utils.get_object_name_and_optional_version_specifier(target_name)
 
-    # merge all versioned data into candidates
-    versioned_candidates = []
-    for candidate in remote_candidates:
-        # ignore candidates without the matching target name
-        if candidate.get(target_json_key, '') != target_name_without_version_specifier:
-            continue
+    # merge all versioned data into a list of candidates
+    versioned_candidates = get_object_versions_json_data(remote_candidates, target_json_key, target_name_without_version_specifier)
 
-        # remove the versions_data from the candidate so it isn't
-        # included in the result if it exists
-        remote_versioned_data = candidate.pop('versions_data', None)
-        if remote_versioned_data:
-            for versioned_data in remote_versioned_data:
-                versioned_candidates.append(candidate | versioned_data)
-        else:
-            versioned_candidates.append(candidate)
+    return manifest.get_most_compatible_object(object_name=target_name, name_key=target_json_key, objects=versioned_candidates)
 
-    if not versioned_candidates:
-        return None
 
-    return manifest.get_most_compatible_object(object_name=target_name, name_key=target_json_key, objects=versioned_candidates)
 def search_o3de_manifest_for_object(manifest_json_data: dict, manifest_attribute: str, target_manifest_json: str, target_json_key: str, target_name: str):
     o3de_object_uris = manifest_json_data.get(manifest_attribute, [])
     search_func = lambda manifest_json_data: manifest_json_data if manifest_json_data.get(target_json_key, '') == target_name else None

+ 5 - 5
scripts/o3de/tests/test_project_manager_interface.py

@@ -365,20 +365,20 @@ def test_refresh_repos():
 
     assert sig.return_annotation == int
 
-def test_get_gem_json_paths_from_cached_repo():
-    sig = signature(repo.get_gem_json_paths_from_cached_repo)
+def test_get_gem_json_data_from_cached_repo():
+    sig = signature(repo.get_gem_json_data_from_cached_repo)
     assert len(sig.parameters) >= 1
 
     repo_uri = list(sig.parameters.values())[0]
     assert repo_uri.name == 'repo_uri'
     assert repo_uri.annotation == str
 
-    assert sig.return_annotation == set
+    assert sig.return_annotation == list 
 
 def test_get_gem_json_paths_from_all_cached_repos():
-    sig = signature(repo.get_gem_json_paths_from_all_cached_repos)
+    sig = signature(repo.get_gem_json_data_from_all_cached_repos)
 
-    assert sig.return_annotation == set
+    assert sig.return_annotation == list
 
 # download interface
 def test_download_gem():

+ 205 - 44
scripts/o3de/tests/test_repo.py

@@ -65,7 +65,7 @@ TEST_O3DE_REPOB_JSON_PAYLOAD = '''
 }
 '''
 
-# This holds the repo 2.0.0 schema, which houses all relevant objects in file
+# This holds the repo 1.0.0 schema, which houses all relevant objects in file
 # A possible future field for the version data is download_prebuilt_uri, 
 # which indicates where to download dedicated binaries of a version of the O3DE object. 
 TEST_O3DE_REPO_JSON_VERSION_2_FILENAME = '3b14717bafd5a3bd768d3d0791a44998c3bd0fb2bfa1a7e2ee8bb1a39b04d631.json'
@@ -75,7 +75,7 @@ TEST_O3DE_REPO_JSON_VERSION_2_PAYLOAD = '''
 
     "origin": "Studios",
 
-    "$schemaVersion":"2.0.0",
+    "$schemaVersion":"1.0.0",
     
     "repo_uri": "https://downloads.testgem3.com/o3de-repo",
 
@@ -107,26 +107,14 @@ TEST_O3DE_REPO_JSON_VERSION_2_PAYLOAD = '''
         "dependencies": [],
         "versions_data": [{
                 "origin_uri": "https://downloads.testgem3.com/o3de-repo/testgem3-2.15/O3DE_testgem3Gem_v2.0.0_Win64_Linux64_Mac64.zip",
-                "version": "2.0.0",
+                "version": "1.0.0",
                 "last_updated": "2021-03-09"
             },
             {
-                "display_name": "testgem3 2.15.4",
+                "display_name": "testgem3 2.0.0",
                 "download_source_uri": "https://downloads.testgem3.com/o3de-repo/testgem3-2.15/O3DE_testgem3Gem_v2.15.4_Win64_Linux64_Mac64.zip",
-                "version": "2.15.4",
+                "version": "2.0.0",
                 "last_updated": "2023-03-09"
-            },
-            {
-                "display_name": "testgem3 3.1.2",
-                "source_control_uri": "https://github.com/testgem3/O3DEtestgem3Plugin.git",
-                "version": "3.1.2",
-                "last_updated": "2025-02-19"
-            },
-            {
-                "display_name": "testgem3 3.2.0",
-                "origin_uri": "https://github.com/testgem3/O3DEtestgem3Plugin",
-                "version": "3.2.0",
-                "last_updated": "2025-02-19"
             }
         ]
     }],
@@ -181,6 +169,60 @@ TEST_O3DE_REPO_JSON_VERSION_2_PAYLOAD = '''
 }
 '''
 
+TEST_O3DE_REPO_GEM3_JSON_VERSION_1_PAYLOAD = '''
+{
+    "gem_name": "testgem3",
+    "display_name": "testgem3 2",
+    "download_api": "HTTP",
+    "license": "Community",
+    "license_url": "https://www.testgem3.com/testgem3-community-license",
+    "origin": "Persistant Studios - testgem3.com",
+    "requirements": "Users will need to download testgem3 Editor from the <a href='https://www.testgem3.com/download/'>testgem3 Web Site</a> to edit/author effects.",
+    "documentation_url": "https://www.testgem3.com/docs/testgem3-v2/plugins/o3de-gem/",
+    "type": "Code",
+    "summary": "The testgem3 Gem provides real-time FX solution for particle effects.",
+    "canonical_tags": [
+        "Gem"
+    ],
+    "user_tags": [
+        "Particles",
+        "Simulation",
+        "SDK"
+    ],
+    "dependencies": [],
+    "origin_uri": "https://downloads.testgem3.com/o3de-repo/testgem3-2.15/O3DE_testgem3Gem_v2.0.0_Win64_Linux64_Mac64.zip",
+    "version": "1.0.0",
+    "last_updated": "2021-03-09"
+}
+'''
+
+TEST_O3DE_REPO_GEM3_JSON_VERSION_2_PAYLOAD = '''
+{
+    "gem_name": "testgem3",
+    "download_api": "HTTP",
+    "license": "Community",
+    "license_url": "https://www.testgem3.com/testgem3-community-license",
+    "origin": "Persistant Studios - testgem3.com",
+    "requirements": "Users will need to download testgem3 Editor from the <a href='https://www.testgem3.com/download/'>testgem3 Web Site</a> to edit/author effects.",
+    "documentation_url": "https://www.testgem3.com/docs/testgem3-v2/plugins/o3de-gem/",
+    "type": "Code",
+    "summary": "The testgem3 Gem provides real-time FX solution for particle effects.",
+    "canonical_tags": [
+        "Gem"
+    ],
+    "user_tags": [
+        "Particles",
+        "Simulation",
+        "SDK"
+    ],
+    "dependencies": [],
+    "display_name": "testgem3 2.0.0",
+    "download_source_uri": "https://downloads.testgem3.com/o3de-repo/testgem3-2.15/O3DE_testgem3Gem_v2.15.4_Win64_Linux64_Mac64.zip",
+    "version": "2.0.0",
+    "last_updated": "2023-03-09"
+}
+'''
+
 TEST_O3DE_REPO_BROKEN_JSON_PAYLOAD = '''
 {
     "repo_name": "Test Repo",
@@ -227,6 +269,25 @@ TEST_O3DE_REPO_GEM_JSON_PAYLOAD = '''
     "dependencies": []
 }
 '''
+TEST_O3DE_REPO_GEM2_JSON_PAYLOAD = '''
+{
+    "gem_name": "TestGem2",
+    "license": "Apache-2.0 Or MIT",
+    "origin": "Test Creator",
+    "origin_uri": "http://o3derepo.org/TestGem2/gem.zip",
+    "repo_uri": "http://o3derepo.org",
+    "type": "Tool",
+    "summary": "A test downloadable gem.",
+    "canonical_tags": [
+        "Gem"
+    ],
+    "user_tags": [],
+    "icon_path": "preview.png",
+    "requirements": "",
+    "documentation_url": "",
+    "dependencies": []
+}
+'''
 
 TEST_O3DE_REPO_PROJECT_FILE_NAME = '233c6e449888b4dc1355b2bf668b91b53715888e6777a2791df0e7aec9d08989.json'
 TEST_O3DE_REPO_PROJECT_JSON_PAYLOAD = '''
@@ -258,6 +319,35 @@ TEST_O3DE_REPO_PROJECT_JSON_PAYLOAD = '''
     ]
 }
 '''
+TEST_O3DE_REPO_PROJECT2_JSON_PAYLOAD = '''
+{
+    "project_name": "TestProject2",
+    "project_id": "{04112c69-306d-4de6-b3b4-4cb1a3eca58e}",
+    "version" : "0.0.0",
+    "compatible_engines" : [
+        "o3de==1.2.3"
+    ],
+    "engine_api_dependencies" : [
+        "framework==1.2.3"
+    ],
+    "origin": "The primary repo for TestProject2 goes here: i.e. http://www.mydomain.com",
+    "license": "What license TestProject2 uses goes here: i.e. https://opensource.org/licenses/MIT",
+    "display_name": "TestProject",
+    "summary": "A short description of TestProject.",
+    "canonical_tags": [
+        "Project"
+    ],
+    "user_tags": [
+        "TestProject2"
+    ],
+    "icon_path": "preview.png",
+    "engine": "o3de==1.2.3",
+    "restricted_name": "projects",
+    "external_subdirectories": [
+        "TestGem"
+    ]
+}
+'''
 
 TEST_O3DE_REPO_TEMPLATE_FILE_NAME = '7802eae005ca1c023e14611ed63182299bf87e760708b4dba8086a134e309f3a.json'
 TEST_O3DE_REPO_TEMPLATE_JSON_PAYLOAD = '''
@@ -279,6 +369,25 @@ TEST_O3DE_REPO_TEMPLATE_JSON_PAYLOAD = '''
     "dependencies": []
 }
 '''
+TEST_O3DE_REPO_TEMPLATE2_JSON_PAYLOAD = '''
+{
+    "template_name": "TestTemplate2",
+    "license": "Apache-2.0 Or MIT",
+    "origin": "Test Creator",
+    "origin_uri": "http://o3derepo.org/TestTemplate/template2.zip",
+    "repo_uri": "http://o3derepo.org",
+    "type": "Tool",
+    "summary": "A test downloadable gem.",
+    "canonical_tags": [
+        "Template"
+    ],
+    "user_tags": [],
+    "icon_path": "preview.png",
+    "requirements": "",
+    "documentation_url": "",
+    "dependencies": []
+}
+'''
 
 @pytest.fixture(scope='class')
 def init_register_repo_data(request):
@@ -290,8 +399,11 @@ class TestRepos:
     valid_urls = [
         'http://o3derepo.org/repo.json',
         'http://o3derepo.org/TestGem/gem.json',
+        'http://o3derepo.org/TestGem2/gem.json',
         'http://o3derepo.org/TestProject/project.json',
+        'http://o3derepo.org/TestProject2/project.json',
         'http://o3derepo.org/TestTemplate/template.json',
+        'http://o3derepo.org/TestTemplate2/template.json',
         'http://o3derecursiverepo.org/repo.json'
     ]
 
@@ -399,53 +511,102 @@ class TestRepos:
             assert result == expected_result
             assert repo_path not in manifest.get_manifest_repos()
 
-    def test_get_object_list(self):
+    @pytest.mark.parametrize("test_name, repo_paths, expected_gems_json_data, expected_projects_json_data, expected_templates_json_data", [
+            pytest.param("repoA loads repoA objects", ['http://o3de.org/repoA'],  
+                         [TEST_O3DE_REPO_GEM_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_PROJECT_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_TEMPLATE_JSON_PAYLOAD]),
+            pytest.param("repoB loads repoB objects", ['http://o3de.org/repoB'],  
+                         [TEST_O3DE_REPO_GEM_JSON_PAYLOAD, TEST_O3DE_REPO_GEM2_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_PROJECT2_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_TEMPLATE2_JSON_PAYLOAD]),
+            # TestGem is included twice because it is in both repositories
+            pytest.param("repoA and repoB loads all objects", ['http://o3de.org/repoA','http://o3de.org/repoB'],  
+                         [TEST_O3DE_REPO_GEM_JSON_PAYLOAD, TEST_O3DE_REPO_GEM_JSON_PAYLOAD, TEST_O3DE_REPO_GEM2_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_PROJECT_JSON_PAYLOAD, TEST_O3DE_REPO_PROJECT2_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_TEMPLATE_JSON_PAYLOAD, TEST_O3DE_REPO_TEMPLATE2_JSON_PAYLOAD]),
+            # RepoC contains a mix of objects with versions_data and objects without
+            pytest.param("repoC loads repoC objects", ['http://o3de.org/repoC'],  
+                         [TEST_O3DE_REPO_GEM3_JSON_VERSION_1_PAYLOAD, TEST_O3DE_REPO_GEM3_JSON_VERSION_2_PAYLOAD],
+                         [TEST_O3DE_REPO_PROJECT_JSON_PAYLOAD],
+                         [TEST_O3DE_REPO_TEMPLATE_JSON_PAYLOAD])
+        ])
+    def test_get_object_json_data(self, test_name, repo_paths, expected_gems_json_data, 
+                                  expected_projects_json_data, expected_templates_json_data):
         self.o3de_manifest_data = json.loads(TEST_O3DE_MANIFEST_JSON_PAYLOAD)
-        self.o3de_manifest_data["repos"] = ["http://o3de.org/repoA", "http://o3de.org/repoB"]
-        self.created_files.clear()
+        self.o3de_manifest_data["repos"] = repo_paths
 
         def load_o3de_manifest(manifest_path: pathlib.Path = None) -> dict:
             return copy.deepcopy(self.o3de_manifest_data)
 
-        def save_o3de_manifest(manifest_data: dict, manifest_path: pathlib.Path = None) -> bool:
-            self.o3de_manifest_data = manifest_data
-            return True
-
         def mocked_open(path, mode, *args, **kwargs):
             file_data = bytes(0)
             if pathlib.Path(path).name == TEST_O3DE_REPOA_FILENAME:
                 file_data = TEST_O3DE_REPOA_JSON_PAYLOAD
             elif pathlib.Path(path).name == TEST_O3DE_REPOB_FILENAME:
                 file_data = TEST_O3DE_REPOB_JSON_PAYLOAD
+            elif pathlib.Path(path).name == TEST_O3DE_REPO_JSON_VERSION_2_FILENAME:
+                file_data = TEST_O3DE_REPO_JSON_VERSION_2_PAYLOAD
+
             mockedopen = mock_open(mock=MagicMock(), read_data=file_data)
             return mockedopen(self, *args, **kwargs)
 
+        def get_json_data_file(object_json: pathlib.Path,
+                            object_typename: str,
+                            object_validator: callable) -> dict or None:
+            if object_typename == 'gem':
+                if object_json == self.test_gem_cache_filename:
+                    return json.loads(TEST_O3DE_REPO_GEM_JSON_PAYLOAD)
+                if object_json == self.test_gem2_cache_filename:
+                    return json.loads(TEST_O3DE_REPO_GEM2_JSON_PAYLOAD)
+                else:
+                    return None
+            elif object_typename == 'project':
+                if object_json == self.test_project_cache_filename:
+                    return json.loads(TEST_O3DE_REPO_PROJECT_JSON_PAYLOAD)
+                elif object_json == self.test_project2_cache_filename:
+                    return json.loads(TEST_O3DE_REPO_PROJECT2_JSON_PAYLOAD)
+                else:
+                    return None
+            elif object_typename == 'template':
+                if object_json == self.test_template_cache_filename:
+                    return json.loads(TEST_O3DE_REPO_TEMPLATE_JSON_PAYLOAD)
+                elif object_json == self.test_template2_cache_filename:
+                    return json.loads(TEST_O3DE_REPO_TEMPLATE2_JSON_PAYLOAD)
+                else:
+                    return None
+            else:
+                None
+
         with patch('o3de.manifest.load_o3de_manifest', side_effect=load_o3de_manifest) as _1,\
-                patch('o3de.manifest.save_o3de_manifest', side_effect=save_o3de_manifest) as _2, \
-                patch('pathlib.Path.open', mocked_open) as _3, \
-                patch('pathlib.Path.is_file', return_value=True) as _4:
-                    # Gems
-                    object_set = repo.get_gem_json_paths_from_cached_repo('http://o3de.org/repoA')
-                    assert len(object_set) == 1
-                    object_set = repo.get_gem_json_paths_from_all_cached_repos()
-                    assert len(object_set) == 2
-                    # Projects
-                    object_set = repo.get_project_json_paths_from_cached_repo('http://o3de.org/repoA')
-                    assert len(object_set) == 1
-                    object_set = repo.get_project_json_paths_from_all_cached_repos()
-                    assert len(object_set) == 2
-                    # Templates
-                    object_set = repo.get_template_json_paths_from_cached_repo('http://o3de.org/repoA')
-                    assert len(object_set) == 1
-                    object_set = repo.get_template_json_paths_from_all_cached_repos()
-                    assert len(object_set) == 2
-        assert True
+            patch('o3de.manifest.get_o3de_cache_folder', return_value=pathlib.Path('Cache')) as _2, \
+            patch('o3de.manifest.get_json_data_file', side_effect=get_json_data_file) as get_json_data_file_patch, \
+            patch('pathlib.Path.open', mocked_open) as _3, \
+            patch('pathlib.Path.is_file', return_value=True) as _4:
+
+            self.test_gem_cache_filename, _ = repo.get_cache_file_uri("http://o3derepo.org/TestGem/gem.json")
+            self.test_gem2_cache_filename, _ = repo.get_cache_file_uri("http://o3derepo.org/TestGem2/gem.json")
+            self.test_project_cache_filename, _ = repo.get_cache_file_uri("http://o3derepo.org/TestProject/project.json")
+            self.test_project2_cache_filename, _ = repo.get_cache_file_uri("http://o3derepo.org/TestProject2/project.json")
+            self.test_template_cache_filename, _ = repo.get_cache_file_uri("http://o3derepo.org/TestTemplate/template.json")
+            self.test_template2_cache_filename, _ = repo.get_cache_file_uri("http://o3derepo.org/TestTemplate2/template.json")
+
+            # Gems
+            gems_json_data = repo.get_gem_json_data_from_all_cached_repos()
+            assert gems_json_data == [json.loads(data) for data in expected_gems_json_data]
+            # Projects
+            projects_json_data = repo.get_project_json_data_from_all_cached_repos()
+            assert projects_json_data == [json.loads(data) for data in expected_projects_json_data]
+            # Templates
+            templates_json_data = repo.get_template_json_data_from_all_cached_repos()
+            assert templates_json_data == [json.loads(data) for data in expected_templates_json_data]
 
 
     @pytest.mark.parametrize("repo_uri, validate_objects", [
+        # tests with version schema 0.0.0
         pytest.param('http://o3de.org/repoA', False),
         pytest.param('http://o3de.org/repoA', True),
-        #tests with version schema 2.0.0
+        # tests with version schema 1.0.0
         pytest.param('http://o3de.org/repoC', False),
         pytest.param('http://o3de.org/repoC', True)
     ])