From 0959756f738598ef42ff87ac3d311c181ef8b513 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Tue, 18 May 2021 15:05:12 -0500 Subject: [PATCH 01/15] [LYN-2255] Implemented duplication of entities with prefabs. --- .../AzToolsFramework/API/EditorEntityAPI.h | 15 ++ .../Application/EditorEntityManager.cpp | 22 ++- .../Application/EditorEntityManager.h | 3 + .../AzToolsFramework/Prefab/PrefabDomUtils.h | 1 + .../Prefab/PrefabPublicHandler.cpp | 173 +++++++++++++++++- .../Prefab/PrefabPublicHandler.h | 3 +- .../Prefab/PrefabPublicInterface.h | 7 + .../SandboxIntegration.cpp | 21 ++- .../SandboxIntegration.h | 2 + 9 files changed, 238 insertions(+), 9 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h index f51af58c51..6c230ff5c7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h @@ -52,6 +52,21 @@ namespace AzToolsFramework * Deletes all entities in the provided list, as well as their transform descendants. */ virtual void DeleteEntitiesAndAllDescendants(const EntityIdList& entities) = 0; + + /** + * Duplicate all currently-selected entities. + */ + virtual void DuplicateSelected() = 0; + + /** + * Duplicates the specified entity. + */ + virtual void DuplicateEntityById(AZ::EntityId entityId) = 0; + + /** + * Duplicates all specified entities. + */ + virtual void DuplicateEntities(const EntityIdList& entities) = 0; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp index 80d7fc7c5a..880217e807 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp @@ -43,7 +43,7 @@ namespace AzToolsFramework void EditorEntityManager::DeleteEntityById(AZ::EntityId entityId) { - DeleteEntities({entityId}); + DeleteEntities(EntityIdList{ entityId }); } void EditorEntityManager::DeleteEntities(const EntityIdList& entities) @@ -53,12 +53,30 @@ namespace AzToolsFramework void EditorEntityManager::DeleteEntityAndAllDescendants(AZ::EntityId entityId) { - DeleteEntitiesAndAllDescendants({entityId}); + DeleteEntitiesAndAllDescendants(EntityIdList{ entityId }); } void EditorEntityManager::DeleteEntitiesAndAllDescendants(const EntityIdList& entities) { m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entities); } + + void EditorEntityManager::DuplicateSelected() + { + EntityIdList selectedEntities; + ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities); + + m_prefabPublicInterface->DuplicateEntitiesInInstance(selectedEntities); + } + + void EditorEntityManager::DuplicateEntityById(AZ::EntityId entityId) + { + DuplicateEntities(EntityIdList{ entityId }); + } + + void EditorEntityManager::DuplicateEntities(const EntityIdList& entities) + { + m_prefabPublicInterface->DuplicateEntitiesInInstance(entities); + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h index 580ad22bda..939f73729e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h @@ -31,6 +31,9 @@ namespace AzToolsFramework void DeleteEntities(const EntityIdList& entities) override; void DeleteEntityAndAllDescendants(AZ::EntityId entityId) override; void DeleteEntitiesAndAllDescendants(const EntityIdList& entities) override; + void DuplicateSelected() override; + void DuplicateEntityById(AZ::EntityId entityId) override; + void DuplicateEntities(const EntityIdList& entities) override; private: Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h index 5ee91c85ae..4feecb9da3 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h @@ -28,6 +28,7 @@ namespace AzToolsFramework inline static const char* PatchesName = "Patches"; inline static const char* SourceName = "Source"; inline static const char* LinkIdName = "LinkId"; + inline static const char* EntityIdName = "Id"; inline static const char* EntitiesName = "Entities"; inline static const char* ContainerEntityName = "ContainerEntity"; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 200a5c96fb..549830d24e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include #include @@ -31,6 +33,8 @@ #include #include +#include + namespace AzToolsFramework { namespace Prefab @@ -631,6 +635,166 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } + PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + if (entityIds.empty()) + { + return AZ::Success(); + } + + if (!EntitiesBelongToSameInstance(entityIds)) + { + return AZ::Failure(AZStd::string("DuplicateEntitiesInInstance - Duplication Error. Cannot duplicate multiple " + "entities belonging to different instances with one operation.")); + } + + // We've already verified the entities are all owned by the same instance, + // so we can just retrieve our instance from the first entity in the list. + InstanceOptionalReference instance = GetOwnerInstanceByEntityId(entityIds[0]); + + // This will cull out any entities that have ancestors in the list, since we will end up duplicating + // the full nested hierarchy with what is returned from RetrieveAndSortPrefabEntitiesAndInstances + AzToolsFramework::EntityIdSet duplicationSet = AzToolsFramework::GetCulledEntityHierarchy(entityIds); + + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); + + UndoSystem::URSequencePoint* currentUndoBatch = nullptr; + ToolsApplicationRequests::Bus::BroadcastResult(currentUndoBatch, &ToolsApplicationRequests::Bus::Events::GetCurrentUndoBatch); + + bool createdUndo = false; + if (!currentUndoBatch) + { + createdUndo = true; + ToolsApplicationRequests::Bus::BroadcastResult( + currentUndoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Duplicate Entities"); + AZ_Assert(currentUndoBatch, "Failed to create new undo batch."); + } + + // In order to undo DuplicateEntitiesInInstance, we have to create a selection command which selects the current selection + // and then add the duplication as children. + // Commands always execute themselves first and then their children (when going forwards) + // and do the opposite when going backwards. + EntityIdList selectedEntities; + ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities); + SelectionCommand* selCommand = aznew SelectionCommand(selectedEntities, "Duplicate Entities"); + + // We insert a "deselect all" command before we duplicate the entities. This ensures the duplicate operations aren't changing + // selection state, which triggers expensive UI updates. By deselecting up front, we are able to do those expensive + // UI updates once at the start instead of once for each entity. + { + EntityIdList deselection; + SelectionCommand* deselectAllCommand = aznew SelectionCommand(deselection, "Deselect Entities"); + deselectAllCommand->SetParent(selCommand); + } + + { + AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); + + // Take a snapshot of the instance DOM before we manipulate it + Prefab::PrefabDom instanceDomBefore; + m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, instance->get()); + + AZStd::vector entities; + AZStd::vector> instances; + + // Gather all entities/instances in the hierarchy, but don't detach them because we are duplicating not deleting. + EntityList inputEntityList = EntityIdSetToEntityList(duplicationSet); + bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, instance->get(), entities, instances, false); + + if (!success) + { + return AZ::Failure(AZStd::string("DuplicateEntitiesInInstance")); + } + + // Make a copy of our before instance DOM where we will add our duplicated entities + Prefab::PrefabDom instanceDomAfter; + instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); + + AZStd::unordered_map oldAliasToNewAliasMap; + AZStd::unordered_map aliasToEntityDomMap; + + for (AZ::Entity* entity : entities) + { + EntityAliasOptionalReference oldAliasRef = instance->get().GetEntityAlias(entity->GetId()); + AZ_Assert(oldAliasRef.has_value(), "No alias found for Entity in the DOM"); + EntityAlias oldAlias = oldAliasRef.value(); + + // Give this the outer allocator so that the memory reference will be valid when + // it gets used for AddMember + Prefab::PrefabDom entityDomBefore(&instanceDomAfter.GetAllocator()); + m_instanceToTemplateInterface->GenerateDomForEntity(entityDomBefore, *entity); + + // Keep track of the old alias <-> new alias mapping for this duplicated entity + // so we can fixup references later + EntityAlias newEntityAlias = Instance::GenerateEntityAlias(); + oldAliasToNewAliasMap.insert(AZStd::make_pair(oldAlias, newEntityAlias)); + + // Update the Entity Id in the Entity DOM for the duplicated Entity + auto entityIdIter = entityDomBefore.FindMember(PrefabDomUtils::EntityIdName); + if (entityIdIter != entityDomBefore.MemberEnd()) + { + entityIdIter->value.SetString(newEntityAlias.c_str(), newEntityAlias.length(), entityDomBefore.GetAllocator()); + } + + rapidjson::StringBuffer buffer; + buffer.Clear(); + + rapidjson::Writer writer(buffer); + entityDomBefore.Accept(writer); + + // Store our duplicated Entity DOM with its new alias as a string + // so that we can fixup entity alias references before adding it + // to the Entities member of our instance DOM + QString entityDomString(buffer.GetString()); + aliasToEntityDomMap.insert(AZStd::make_pair(newEntityAlias, entityDomString)); + } + + auto entitiesIter = instanceDomAfter.FindMember(PrefabDomUtils::EntitiesName); + AZ_Assert(entitiesIter != instanceDomAfter.MemberEnd(), "Instance DOM missing the Entities member."); + + // Now that all the duplicated Entity DOMs have been created, we need to iterate + // through them and replace any previous EntityAlias references with the new ones. + // These are more than just parent entity references for nested entities, this will + // also cover any EntityId references that were made in the components between them. + for (auto aliasEntityPair : aliasToEntityDomMap) + { + EntityAlias newEntityAlias = aliasEntityPair.first; + QString newEntityDomString = aliasEntityPair.second; + + // Replace all of the old alias references with the new ones + for (auto aliasMapIter : oldAliasToNewAliasMap) + { + newEntityDomString.replace(aliasMapIter.first.c_str(), aliasMapIter.second.c_str()); + } + + // Create the new Entity DOM from parsing the JSON string + Prefab::PrefabDom entityDomAfter(&instanceDomAfter.GetAllocator()); + entityDomAfter.Parse(newEntityDomString.toUtf8().constData()); + + // Add the new Entity DOM to the Entities member of the instance + rapidjson::Value aliasName(newEntityAlias.c_str(), newEntityAlias.length(), instanceDomAfter.GetAllocator()); + entitiesIter->value.AddMember(AZStd::move(aliasName), entityDomAfter, instanceDomAfter.GetAllocator()); + } + + PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance duplication"); + command->Capture(instanceDomBefore, instanceDomAfter, instance->get().GetTemplateId()); + command->SetParent(selCommand); + } + + selCommand->SetParent(currentUndoBatch); + { + AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance:RunRedo"); + selCommand->RunRedo(); + } + + if (createdUndo) + { + ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::EndUndoBatch); + } + + return AZ::Success(); + } + PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) { if (entityIds.empty()) @@ -865,7 +1029,8 @@ namespace AzToolsFramework bool PrefabPublicHandler::RetrieveAndSortPrefabEntitiesAndInstances( const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances) const + EntityList& outEntities, AZStd::vector>& outInstances, + bool shouldDetach) const { if (inputEntities.size() == 0) { @@ -949,14 +1114,16 @@ namespace AzToolsFramework for (AZ::Entity* entity : entities) { - outEntities.emplace_back(commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release()); + AZ::Entity* outEntity = (shouldDetach) ? commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release() : entity; + outEntities.emplace_back(outEntity); } outInstances.clear(); outInstances.reserve(instances.size()); for (Instance* instancePtr : instances) { - outInstances.push_back(AZStd::move(commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias()))); + AZStd::unique_ptr outInstance = (shouldDetach) ? commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias()) : AZStd::unique_ptr(instancePtr); + outInstances.push_back(AZStd::move(outInstance)); } return (outEntities.size() + outInstances.size()) > 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index d88086dda9..b6128f0ab9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -60,11 +60,12 @@ namespace AzToolsFramework PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; + PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; private: PrefabOperationResult DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants); bool RetrieveAndSortPrefabEntitiesAndInstances(const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances) const; + EntityList& outEntities, AZStd::vector>& outInstances, bool shouldDetach = true) const; InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const; bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 1a8da0dfe0..0750c4d264 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -143,6 +143,13 @@ namespace AzToolsFramework * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; + + /** + * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. + * @param entities The entities to duplicate. + * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + */ + virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; }; } // namespace Prefab diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp index 027487a435..d36c20c56a 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,9 @@ void SandboxIntegrationManager::Setup() (m_prefabIntegrationInterface != nullptr), "SandboxIntegrationManager requires a PrefabIntegrationInterface instance to be present on Setup()."); + m_editorEntityAPI = AZ::Interface::Get(); + AZ_Assert(m_editorEntityAPI, "SandboxIntegrationManager requires an EditorEntityAPI instance to be present on Setup()."); + AzToolsFramework::Layers::EditorLayerComponentNotificationBus::Handler::BusConnect(); } @@ -1215,9 +1219,20 @@ void SandboxIntegrationManager::CloneSelection(bool& handled) if (!duplicationSet.empty()) { - AZStd::unordered_set clonedEntities; - handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities); - m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end()); + bool prefabSystemEnabled = false; + AzFramework::ApplicationRequests::Bus::BroadcastResult(prefabSystemEnabled, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled); + + if (prefabSystemEnabled) + { + m_editorEntityAPI->DuplicateSelected(); + handled = true; + } + else + { + AZStd::unordered_set clonedEntities; + handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities); + m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end()); + } } else { diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h index 14f52591a4..528b93e44e 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h @@ -77,6 +77,7 @@ class CHyperGraph; namespace AzToolsFramework { + class EditorEntityAPI; class EditorEntityUiInterface; namespace AssetBrowser @@ -371,6 +372,7 @@ private: AzToolsFramework::EditorEntityUiInterface* m_editorEntityUiInterface = nullptr; AzToolsFramework::Prefab::PrefabIntegrationInterface* m_prefabIntegrationInterface = nullptr; + AzToolsFramework::EditorEntityAPI* m_editorEntityAPI = nullptr; // Overrides UI styling and behavior for Layer Entities AzToolsFramework::LayerUiHandler m_layerUiOverrideHandler; From 2d89c6017562e77faa6d66a49a23ccfb58a5f747 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Thu, 20 May 2021 10:39:57 -0500 Subject: [PATCH 02/15] [LYN-2255] Updated Prefab Duplicate to select the newly duplicated entities. Also addressed some other PR feedback. --- .../Prefab/PrefabPublicHandler.cpp | 140 +++++++----------- .../Prefab/PrefabPublicHandler.h | 2 +- .../Tests/Prefab/PrefabDuplicateTests.cpp | 129 ++++++++++++++++ .../Tests/Prefab/PrefabTestFixture.cpp | 3 + .../Tests/Prefab/PrefabTestFixture.h | 1 + .../Tests/aztoolsframeworktests_files.cmake | 1 + 6 files changed, 191 insertions(+), 85 deletions(-) create mode 100644 Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 549830d24e..a52075f4cd 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -87,7 +87,8 @@ namespace AzToolsFramework commonRootInstanceDomBeforeCreate, commonRootEntityOwningInstance->get()); AZStd::vector entities; - AZStd::vector> instances; + AZStd::vector> instancePtrs; + AZStd::vector instances; // Retrieve all entities affected and identify Instances if (!RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonRootEntityOwningInstance->get(), entities, instances)) @@ -96,11 +97,19 @@ namespace AzToolsFramework AZStd::string("Could not create a new prefab out of the entities provided - invalid selection.")); } + // Detach the retrieved entities + for (AZ::Entity* entity : entities) + { + commonRootEntityOwningInstance->get().DetachEntity(entity->GetId()).release(); + } + // When we create a prefab with other prefab instances, we have to remove the existing links between the source and // target templates of the other instances. for (auto& nestedInstance : instances) { - RemoveLink(nestedInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); + AZStd::unique_ptr outInstance = commonRootEntityOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias()); + instancePtrs.emplace_back(AZStd::move(outInstance)); + RemoveLink(outInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); } PrefabUndoHelpers::UpdatePrefabInstance( @@ -116,7 +125,7 @@ namespace AzToolsFramework // Create the Prefab instanceToCreate = prefabEditorEntityOwnershipInterface->CreatePrefab( - entities, AZStd::move(instances), filePath, commonRootEntityOwningInstance); + entities, AZStd::move(instancePtrs), filePath, commonRootEntityOwningInstance); if (!instanceToCreate) { @@ -286,7 +295,7 @@ namespace AzToolsFramework // Find common root and top level entities bool entitiesHaveCommonRoot = false; - AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( + AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult( entitiesHaveCommonRoot, &AzToolsFramework::ToolsApplicationRequests::FindCommonRootInactive, inputEntityList, commonRootEntityId, &topLevelEntities); @@ -639,18 +648,21 @@ namespace AzToolsFramework { if (entityIds.empty()) { - return AZ::Success(); + return AZ::Failure(AZStd::string("No entities to duplicate.")); } if (!EntitiesBelongToSameInstance(entityIds)) { - return AZ::Failure(AZStd::string("DuplicateEntitiesInInstance - Duplication Error. Cannot duplicate multiple " + return AZ::Failure(AZStd::string("Cannot duplicate multiple " "entities belonging to different instances with one operation.")); } // We've already verified the entities are all owned by the same instance, // so we can just retrieve our instance from the first entity in the list. - InstanceOptionalReference instance = GetOwnerInstanceByEntityId(entityIds[0]); + InstanceOptionalReference commonEntityOwningInstance = GetOwnerInstanceByEntityId(entityIds[0]); + AZ_Assert( + commonEntityOwningInstance.has_value(), + "Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided"); // This will cull out any entities that have ancestors in the list, since we will end up duplicating // the full nested hierarchy with what is returned from RetrieveAndSortPrefabEntitiesAndInstances @@ -658,48 +670,21 @@ namespace AzToolsFramework AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); - UndoSystem::URSequencePoint* currentUndoBatch = nullptr; - ToolsApplicationRequests::Bus::BroadcastResult(currentUndoBatch, &ToolsApplicationRequests::Bus::Events::GetCurrentUndoBatch); - - bool createdUndo = false; - if (!currentUndoBatch) - { - createdUndo = true; - ToolsApplicationRequests::Bus::BroadcastResult( - currentUndoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Duplicate Entities"); - AZ_Assert(currentUndoBatch, "Failed to create new undo batch."); - } - - // In order to undo DuplicateEntitiesInInstance, we have to create a selection command which selects the current selection - // and then add the duplication as children. - // Commands always execute themselves first and then their children (when going forwards) - // and do the opposite when going backwards. - EntityIdList selectedEntities; - ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities); - SelectionCommand* selCommand = aznew SelectionCommand(selectedEntities, "Duplicate Entities"); - - // We insert a "deselect all" command before we duplicate the entities. This ensures the duplicate operations aren't changing - // selection state, which triggers expensive UI updates. By deselecting up front, we are able to do those expensive - // UI updates once at the start instead of once for each entity. - { - EntityIdList deselection; - SelectionCommand* deselectAllCommand = aznew SelectionCommand(deselection, "Deselect Entities"); - deselectAllCommand->SetParent(selCommand); - } + ScopedUndoBatch undoBatch("Duplicate Entities"); { AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); // Take a snapshot of the instance DOM before we manipulate it Prefab::PrefabDom instanceDomBefore; - m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, instance->get()); + m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, commonEntityOwningInstance->get()); AZStd::vector entities; - AZStd::vector> instances; + AZStd::vector instances; // Gather all entities/instances in the hierarchy, but don't detach them because we are duplicating not deleting. EntityList inputEntityList = EntityIdSetToEntityList(duplicationSet); - bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, instance->get(), entities, instances, false); + bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonEntityOwningInstance->get(), entities, instances); if (!success) { @@ -715,7 +700,7 @@ namespace AzToolsFramework for (AZ::Entity* entity : entities) { - EntityAliasOptionalReference oldAliasRef = instance->get().GetEntityAlias(entity->GetId()); + EntityAliasOptionalReference oldAliasRef = commonEntityOwningInstance->get().GetEntityAlias(entity->GetId()); AZ_Assert(oldAliasRef.has_value(), "No alias found for Entity in the DOM"); EntityAlias oldAlias = oldAliasRef.value(); @@ -731,14 +716,10 @@ namespace AzToolsFramework // Update the Entity Id in the Entity DOM for the duplicated Entity auto entityIdIter = entityDomBefore.FindMember(PrefabDomUtils::EntityIdName); - if (entityIdIter != entityDomBefore.MemberEnd()) - { - entityIdIter->value.SetString(newEntityAlias.c_str(), newEntityAlias.length(), entityDomBefore.GetAllocator()); - } + AZ_Assert(entityIdIter != entityDomBefore.MemberEnd(), "Entity DOM missing Id."); + entityIdIter->value.SetString(newEntityAlias.c_str(), newEntityAlias.length(), entityDomBefore.GetAllocator()); rapidjson::StringBuffer buffer; - buffer.Clear(); - rapidjson::Writer writer(buffer); entityDomBefore.Accept(writer); @@ -776,20 +757,27 @@ namespace AzToolsFramework entitiesIter->value.AddMember(AZStd::move(aliasName), entityDomAfter, instanceDomAfter.GetAllocator()); } - PrefabUndoInstance* command = aznew PrefabUndoInstance("Instance duplication"); - command->Capture(instanceDomBefore, instanceDomAfter, instance->get().GetTemplateId()); - command->SetParent(selCommand); - } + PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity duplication"); + command->SetParent(undoBatch.GetUndoBatch()); + command->Capture(instanceDomBefore, instanceDomAfter, commonEntityOwningInstance->get().GetTemplateId()); + command->RunRedo(); - selCommand->SetParent(currentUndoBatch); - { - AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance:RunRedo"); - selCommand->RunRedo(); - } + EntityIdList duplicatedEntityIds; + for (auto aliasMapIter : oldAliasToNewAliasMap) + { + EntityAlias newEntityAlias = aliasMapIter.second; - if (createdUndo) - { - ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::EndUndoBatch); + AliasPath absoluteEntityPath = commonEntityOwningInstance->get().GetAbsoluteInstanceAliasPath(); + absoluteEntityPath.Append(newEntityAlias); + + AZ::EntityId newEntityId = InstanceEntityIdMapper::GenerateEntityIdForAliasPath(absoluteEntityPath); + duplicatedEntityIds.push_back(newEntityId); + } + + // Select the duplicated entities + auto selectionUndo = aznew SelectionCommand(duplicatedEntityIds, "Select Duplicated Entities"); + selectionUndo->SetParent(undoBatch.GetUndoBatch()); + ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, selectionUndo); } return AZ::Success(); @@ -822,17 +810,7 @@ namespace AzToolsFramework AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); - UndoSystem::URSequencePoint* currentUndoBatch = nullptr; - ToolsApplicationRequests::Bus::BroadcastResult(currentUndoBatch, &ToolsApplicationRequests::Bus::Events::GetCurrentUndoBatch); - - bool createdUndo = false; - if (!currentUndoBatch) - { - createdUndo = true; - ToolsApplicationRequests::Bus::BroadcastResult( - currentUndoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Delete Selected"); - AZ_Assert(currentUndoBatch, "Failed to create new undo batch."); - } + ScopedUndoBatch undoBatch("Delete Selected"); // In order to undo DeleteSelected, we have to create a selection command which selects the current selection // and then add the deletion as children. @@ -860,7 +838,7 @@ namespace AzToolsFramework if (deleteDescendants) { AZStd::vector entities; - AZStd::vector> instances; + AZStd::vector instances; bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->get(), entities, instances); @@ -871,13 +849,15 @@ namespace AzToolsFramework for (AZ::Entity* entity : entities) { + commonOwningInstance->get().DetachEntity(entity->GetId()).release(); AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entity->GetId()); } for (auto& nestedInstance : instances) { - RemoveLink(nestedInstance, commonOwningInstance->get().GetTemplateId(), currentUndoBatch); - nestedInstance.reset(); + AZStd::unique_ptr outInstance = commonOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias()); + RemoveLink(outInstance, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); + outInstance.reset(); } } else @@ -889,7 +869,7 @@ namespace AzToolsFramework if (owningInstance->get().GetContainerEntityId() == entityId) { auto instancePtr = commonOwningInstance->get().DetachNestedInstance(owningInstance->get().GetInstanceAlias()); - RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), currentUndoBatch); + RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); } else { @@ -907,17 +887,12 @@ namespace AzToolsFramework command->SetParent(selCommand); } - selCommand->SetParent(currentUndoBatch); + selCommand->SetParent(undoBatch.GetUndoBatch()); { AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "Internal::DeleteEntities:RunRedo"); selCommand->RunRedo(); } - if (createdUndo) - { - ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::EndUndoBatch); - } - return AZ::Success(); } @@ -1029,8 +1004,7 @@ namespace AzToolsFramework bool PrefabPublicHandler::RetrieveAndSortPrefabEntitiesAndInstances( const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances, - bool shouldDetach) const + EntityList& outEntities, AZStd::vector& outInstances) const { if (inputEntities.size() == 0) { @@ -1114,16 +1088,14 @@ namespace AzToolsFramework for (AZ::Entity* entity : entities) { - AZ::Entity* outEntity = (shouldDetach) ? commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release() : entity; - outEntities.emplace_back(outEntity); + outEntities.emplace_back(entity); } outInstances.clear(); outInstances.reserve(instances.size()); for (Instance* instancePtr : instances) { - AZStd::unique_ptr outInstance = (shouldDetach) ? commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias()) : AZStd::unique_ptr(instancePtr); - outInstances.push_back(AZStd::move(outInstance)); + outInstances.push_back(instancePtr); } return (outEntities.size() + outInstances.size()) > 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index b6128f0ab9..b4e428efcc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -65,7 +65,7 @@ namespace AzToolsFramework private: PrefabOperationResult DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants); bool RetrieveAndSortPrefabEntitiesAndInstances(const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances, bool shouldDetach = true) const; + EntityList& outEntities, AZStd::vector& outInstances) const; InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const; bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const; diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp new file mode 100644 index 0000000000..514942166f --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp @@ -0,0 +1,129 @@ +/* +* 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 + +#include +#include +#include + +namespace UnitTest +{ + using PrefabDuplicateTest = PrefabTestFixture; + + TEST_F(PrefabDuplicateTest, PrefabDuplicate_DuplicateSingleEntitySucceeds) + { + AZStd::string entityName("Same Name"); + AZ::Entity* entity1 = CreateEntity(entityName.c_str()); + entity1->Deactivate(); + entity1->CreateComponent(); + entity1->Activate(); + + AzToolsFramework::EditorEntityContextRequestBus::Broadcast( + &AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ entity1 }); + AZStd::unique_ptr newInstance = m_prefabSystemComponent->CreatePrefab( + { entity1 }, + {}, + PrefabMockFilePath); + + // We've created a prefab with a single Entity, so there should only be one EntityAlias in our instance + EXPECT_EQ(newInstance->GetEntityAliases().size(), 1); + + // Duplicate the Entity and trigger the UpdateTemplateInstancesInQueue so the changes get propagated + m_prefabPublicInterface->DuplicateEntitiesInInstance(AzToolsFramework::EntityIdList{ entity1->GetId() }); + m_instanceUpdateExecutorInterface->UpdateTemplateInstancesInQueue(); + + // We duplicated a single Entity, so there should now be two EntityAliases + EXPECT_EQ(newInstance->GetEntityAliases().size(), 2); + + newInstance->GetConstEntities([&](const AZ::Entity& entity) + { + // Both of the entities should have the same name + EXPECT_EQ(entity.GetName(), entityName); + + // Both of the entities should have the PrefabTestComponent we added + auto testComponent = entity.FindComponent(); + EXPECT_NE(nullptr, testComponent); + + return true; + }); + } + + TEST_F(PrefabDuplicateTest, PrefabDuplicate_DuplicateMultipleEntitiesAndFixesReferences) + { + AZ::Entity* parentEntity = CreateEntity("Parent Entity"); + + AZ::Entity* childEntity = CreateEntity("Child Entity"); + childEntity->Deactivate(); + auto newComponent = childEntity->CreateComponent(); + childEntity->Activate(); + + // Set the EntityId reference property on our PrefabTestComponent so we can + // verify that arbitrary EntityId's are fixed up properly + newComponent->m_entityIdProperty = parentEntity->GetId(); + + AzToolsFramework::EditorEntityContextRequestBus::Broadcast( + &AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ parentEntity, childEntity }); + + AZStd::unique_ptr newInstance = m_prefabSystemComponent->CreatePrefab( + { parentEntity, childEntity }, + {}, + PrefabMockFilePath); + + // We've created a prefab with two entities, so there should be two EntityAliases in our instance + EXPECT_EQ(newInstance->GetEntityAliases().size(), 2); + + // Duplicate the entities and trigger the UpdateTemplateInstancesInQueue so the changes get propagated + m_prefabPublicInterface->DuplicateEntitiesInInstance(AzToolsFramework::EntityIdList{ parentEntity->GetId(), childEntity->GetId() }); + m_instanceUpdateExecutorInterface->UpdateTemplateInstancesInQueue(); + + // We duplicated two entities, so there should now be four EntityAliases + EXPECT_EQ(newInstance->GetEntityAliases().size(), 4); + + AzToolsFramework::EntityIdList parentEntityIds; + newInstance->GetConstEntities([&](const AZ::Entity& entity) + { + // Gather the parent EntityIds by tracking which entities don't have a PrefabTestComponent + auto testComponent = entity.FindComponent(); + if (!testComponent) + { + parentEntityIds.push_back(entity.GetId()); + } + + return true; + }); + + // There should only be two parents + EXPECT_EQ(parentEntityIds.size(), 2); + + // Verify that the EntityId reference on the PrefabTestComponent on the children correspond + // to unique entities, which will verify that the EntityIds are fixed up on duplicate + newInstance->GetConstEntities([&](const AZ::Entity& entity) + { + // Only the child entities have a PrefabTestComponent + auto testComponent = entity.FindComponent(); + if (testComponent) + { + auto it = AZStd::find(parentEntityIds.begin(), parentEntityIds.end(), testComponent->m_entityIdProperty); + EXPECT_NE(it, parentEntityIds.end()); + + // Erase when we find it so that the matches will be unique + parentEntityIds.erase(it); + } + + return true; + }); + + // Verify we matched each of the parent EntityIds + EXPECT_EQ(parentEntityIds.size(), 0); + } +} diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp index 3a8d9cc7eb..dbf7397fec 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp @@ -32,6 +32,9 @@ namespace UnitTest m_prefabLoaderInterface = AZ::Interface::Get(); EXPECT_TRUE(m_prefabLoaderInterface); + m_prefabPublicInterface = AZ::Interface::Get(); + EXPECT_TRUE(m_prefabPublicInterface); + m_instanceUpdateExecutorInterface = AZ::Interface::Get(); EXPECT_TRUE(m_instanceUpdateExecutorInterface); diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h index af90309867..1338dba0dd 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h @@ -57,6 +57,7 @@ namespace UnitTest PrefabSystemComponent* m_prefabSystemComponent = nullptr; PrefabLoaderInterface* m_prefabLoaderInterface = nullptr; + PrefabPublicInterface* m_prefabPublicInterface = nullptr; InstanceUpdateExecutorInterface* m_instanceUpdateExecutorInterface = nullptr; InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr; }; diff --git a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake index e54aa187e4..cd3796a64e 100644 --- a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake +++ b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake @@ -54,6 +54,7 @@ set(FILES Prefab/Spawnable/SpawnableMetaDataTests.cpp Prefab/MockPrefabFileIOActionValidator.cpp Prefab/MockPrefabFileIOActionValidator.h + Prefab/PrefabDuplicateTests.cpp Prefab/PrefabEntityAliasTests.cpp Prefab/PrefabInstanceToTemplatePropagatorTests.cpp Prefab/PrefabInstantiateTests.cpp From c105894aa5f6643e7a45aa3aa97c76bd9c91e302 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Thu, 20 May 2021 14:28:04 -0500 Subject: [PATCH 03/15] [LYN-2255] Made PrefabTestFixture have the prefab system enabled so that the duplicate tests pass on Jenkins (they rely on the PrefabEditorEntityOwnershipInterface being registered). --- .../Prefab/PrefabPublicHandler.cpp | 2 +- .../UnitTest/AzToolsFrameworkTestHelpers.h | 8 +++++++- .../Tests/Prefab/PrefabTestFixture.cpp | 16 ++++++++++++++++ .../Tests/Prefab/PrefabTestFixture.h | 12 ++++++++++++ 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 38baf702b0..cc5da669c6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -705,7 +705,7 @@ namespace AzToolsFramework if (!success) { - return AZ::Failure(AZStd::string("DuplicateEntitiesInInstance")); + return AZ::Failure(AZStd::string("Cannot duplicate multiple entities belonging to different instances with one operation")); } // Make a copy of our before instance DOM where we will add our duplicated entities diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h index 53dab661ce..276982d46d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h @@ -138,7 +138,7 @@ namespace UnitTest if (!GetApplication()) { // Create & Start a new ToolsApplication if there's no existing one - m_app = AZStd::make_unique("ToolsApplication"); + m_app = CreateTestApplication(); m_app->Start(AzFramework::Application::Descriptor()); } @@ -216,6 +216,12 @@ namespace UnitTest TestEditorActions m_editorActions; ToolsApplicationMessageHandler m_messageHandler; // used to suppress trace messages in test output + // Override this if your test fixture needs to use a custom TestApplication + virtual AZStd::unique_ptr CreateTestApplication() + { + return AZStd::make_unique("ToolsApplication"); + } + private: AZStd::unique_ptr m_app; }; diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp index dbf7397fec..ace2356732 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp @@ -20,6 +20,17 @@ namespace UnitTest { + PrefabTestToolsApplication::PrefabTestToolsApplication(AZStd::string appName) + : ToolsTestApplication(AZStd::move(appName)) + { + } + + bool PrefabTestToolsApplication::IsPrefabSystemEnabled() const + { + // Make sure our prefab tests always run with prefabs enabled + return true; + } + void PrefabTestFixture::SetUpEditorFixtureImpl() { // Acquire the system entity @@ -44,6 +55,11 @@ namespace UnitTest GetApplication()->RegisterComponentDescriptor(PrefabTestComponent::CreateDescriptor()); } + AZStd::unique_ptr PrefabTestFixture::CreateTestApplication() + { + return AZStd::make_unique("PrefabTestApplication"); + } + AZ::Entity* PrefabTestFixture::CreateEntity(const char* entityName, const bool shouldActivate) { // Circumvent the EntityContext system and generate a new entity with a transformcomponent diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h index 1338dba0dd..ee471cf192 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h @@ -31,6 +31,16 @@ namespace UnitTest using namespace AzToolsFramework::Prefab; using namespace PrefabTestUtils; + class PrefabTestToolsApplication + : public ToolsTestApplication + { + public: + PrefabTestToolsApplication(AZStd::string appName); + + // Make sure our prefab tests always run with prefabs enabled + bool IsPrefabSystemEnabled() const override; + }; + class PrefabTestFixture : public ToolsApplicationFixture, public UnitTest::TraceBusRedirector @@ -45,6 +55,8 @@ namespace UnitTest void SetUpEditorFixtureImpl() override; + AZStd::unique_ptr CreateTestApplication() override; + AZ::Entity* CreateEntity(const char* entityName, const bool shouldActivate = true); void CompareInstances(const Instance& instanceA, const Instance& instanceB, bool shouldCompareLinkIds = true, From d948bf0a7789db63207f35b3e7f9dff51adb51a7 Mon Sep 17 00:00:00 2001 From: Gene Walters Date: Thu, 20 May 2021 22:48:04 -0700 Subject: [PATCH 04/15] Moved network context methods out of MultiplayerComponent into NetBindingComponent because FindComponent did not actually work with finding base classes. +1 for the ability to test! Allow scripting to detect if a networked entity is Authory, Server, Client, or Autonomous. --- .../Components/MultiplayerComponent.cpp | 74 ------------------- .../Source/Components/NetBindComponent.cpp | 74 +++++++++++++++++++ 2 files changed, 74 insertions(+), 74 deletions(-) diff --git a/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp b/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp index 21ae9d9f01..8542288b23 100644 --- a/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp @@ -24,80 +24,6 @@ namespace Multiplayer serializeContext->Class() ->Version(1); } - - AZ::BehaviorContext* behaviorContext = azrtti_cast(context); - if (behaviorContext) - { - behaviorContext->Class("MultiplayerComponent") - ->Attribute(AZ::Script::Attributes::Module, "multiplayer") - ->Attribute(AZ::Script::Attributes::Category, "Multiplayer") - - ->Method("IsAuthority", [](AZ::EntityId id) -> bool { - AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); - if (!entity) - { - AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsAuthority failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) - return false; - } - - MultiplayerComponent* multiplayerComponent = entity->FindComponent(); - if (!multiplayerComponent) - { - AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsAuthority failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str()) - return false; - } - return multiplayerComponent->IsAuthority(); - }) - ->Method("IsAutonomous", [](AZ::EntityId id) -> bool { - AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); - if (!entity) - { - AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsAutonomous failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) - return false; - } - - MultiplayerComponent* multiplayerComponent = entity->FindComponent(); - if (!multiplayerComponent) - { - AZ_Warning("MultiplayerComponent", false, "MultiplayerComponent IsAutonomous failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str()) - return false; - } - return multiplayerComponent->IsAutonomous(); - }) - ->Method("IsClient", [](AZ::EntityId id) -> bool { - AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); - if (!entity) - { - AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsClient failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) - return false; - } - - MultiplayerComponent* multiplayerComponent = entity->FindComponent(); - if (!multiplayerComponent) - { - AZ_Warning("MultiplayerComponent", false, "MultiplayerComponent IsClient failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str()) - return false; - } - return multiplayerComponent->IsClient(); - }) - ->Method("IsServer", [](AZ::EntityId id) -> bool { - AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); - if (!entity) - { - AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsServer failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) - return false; - } - - MultiplayerComponent* multiplayerComponent = entity->FindComponent(); - if (!multiplayerComponent) - { - AZ_Warning("MultiplayerComponent", false, "MultiplayerComponent IsServer failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str()) - return false; - } - return multiplayerComponent->IsServer(); - }) - ; - } } void MultiplayerComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index adc369e9ed..d91bba2e0c 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -46,6 +46,80 @@ namespace Multiplayer ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")); } } + + AZ::BehaviorContext* behaviorContext = azrtti_cast(context); + if (behaviorContext) + { + behaviorContext->Class("NetBindComponent") + ->Attribute(AZ::Script::Attributes::Module, "multiplayer") + ->Attribute(AZ::Script::Attributes::Category, "Multiplayer") + + ->Method("IsAuthority", [](AZ::EntityId id) -> bool { + AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); + if (!entity) + { + AZ_Warning( "NetBindComponent", false, "NetBindComponent IsAuthority failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) + return false; + } + + NetBindComponent* netBindComponent = entity-> FindComponent(); + if (!netBindComponent) + { + AZ_Warning( "NetBindComponent", false, "NetBindComponent IsAuthority failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str()) + return false; + } + return netBindComponent->IsAuthority(); + }) + ->Method("IsAutonomous", [](AZ::EntityId id) -> bool { + AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); + if (!entity) + { + AZ_Warning( "NetBindComponent", false, "NetBindComponent IsAutonomous failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) + return false; + } + + NetBindComponent* netBindComponent = entity->FindComponent(); + if (!netBindComponent) + { + AZ_Warning("NetBindComponent", false, "NetBindComponent IsAutonomous failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str()) + return false; + } + return netBindComponent->IsAutonomous(); + }) + ->Method("IsClient", [](AZ::EntityId id) -> bool { + AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); + if (!entity) + { + AZ_Warning( "NetBindComponent", false, "NetBindComponent IsClient failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) + return false; + } + + NetBindComponent* netBindComponent = entity->FindComponent(); + if (!netBindComponent) + { + AZ_Warning("NetBindComponent", false, "NetBindComponent IsClient failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str()) + return false; + } + return netBindComponent->IsClient(); + }) + ->Method("IsServer", [](AZ::EntityId id) -> bool { + AZ::Entity* entity = AZ::Interface::Get()->FindEntity(id); + if (!entity) + { + AZ_Warning( "NetBindComponent", false, "NetBindComponent IsServer failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str()) + return false; + } + + NetBindComponent* netBindComponent = entity->FindComponent(); + if (!netBindComponent) + { + AZ_Warning("NetBindComponent", false, "NetBindComponent IsServer failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str()) + return false; + } + return netBindComponent->IsServer(); + }) + ; + } } void NetBindComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) From 3d1fa538c2cb8f7dcbbdb7ad0f0e3c3bc39b73b9 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Fri, 21 May 2021 12:05:24 -0500 Subject: [PATCH 05/15] [LYN-2255] Added extra protection for duplicate replacement of alias logic by bookending the strings. --- .../AzToolsFramework/Prefab/PrefabPublicHandler.cpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 05d1f54c75..4b55dca178 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -109,7 +109,6 @@ namespace AzToolsFramework for (auto& nestedInstance : instances) { AZStd::unique_ptr outInstance = commonRootEntityOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias()); - instancePtrs.emplace_back(AZStd::move(outInstance)); auto linkRef = m_prefabSystemComponentInterface->FindLink(nestedInstance->GetLinkId()); @@ -122,6 +121,8 @@ namespace AzToolsFramework } RemoveLink(outInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); + + instancePtrs.emplace_back(AZStd::move(outInstance)); } PrefabUndoHelpers::UpdatePrefabInstance( @@ -823,9 +824,14 @@ namespace AzToolsFramework QString newEntityDomString = aliasEntityPair.second; // Replace all of the old alias references with the new ones + // We bookend the aliases with \" as an extra precaution to prevent + // inadvertently replacing a matching string vs. where an actual EntityId is expected for (auto aliasMapIter : oldAliasToNewAliasMap) { - newEntityDomString.replace(aliasMapIter.first.c_str(), aliasMapIter.second.c_str()); + QString oldAlias = QString("\"%1\"").arg(aliasMapIter.first.c_str()); + QString newAlias = QString("\"%1\"").arg(aliasMapIter.second.c_str()); + + newEntityDomString.replace(oldAlias, newAlias); } // Create the new Entity DOM from parsing the JSON string From 21620a0d738d85b411912c54c1e4dc8c35d1e911 Mon Sep 17 00:00:00 2001 From: Chris Galvan Date: Fri, 21 May 2021 14:19:00 -0500 Subject: [PATCH 06/15] [LYN-2255] Fixed additional alias replacement case, and removed now unneeded specific alias replacement. --- .../Prefab/PrefabPublicHandler.cpp | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 4b55dca178..579f465eb2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -769,7 +769,7 @@ namespace AzToolsFramework if (!success) { - return AZ::Failure(AZStd::string("Cannot duplicate multiple entities belonging to different instances with one operation")); + return AZ::Failure(AZStd::string("Failed to retrieve entities and instances from the given list of entity ids for duplication")); } // Make a copy of our before instance DOM where we will add our duplicated entities @@ -795,11 +795,6 @@ namespace AzToolsFramework EntityAlias newEntityAlias = Instance::GenerateEntityAlias(); oldAliasToNewAliasMap.insert(AZStd::make_pair(oldAlias, newEntityAlias)); - // Update the Entity Id in the Entity DOM for the duplicated Entity - auto entityIdIter = entityDomBefore.FindMember(PrefabDomUtils::EntityIdName); - AZ_Assert(entityIdIter != entityDomBefore.MemberEnd(), "Entity DOM missing Id."); - entityIdIter->value.SetString(newEntityAlias.c_str(), newEntityAlias.length(), entityDomBefore.GetAllocator()); - rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); entityDomBefore.Accept(writer); @@ -824,14 +819,20 @@ namespace AzToolsFramework QString newEntityDomString = aliasEntityPair.second; // Replace all of the old alias references with the new ones - // We bookend the aliases with \" as an extra precaution to prevent + // We bookend the aliases with \" and also with a / as an extra precaution to prevent // inadvertently replacing a matching string vs. where an actual EntityId is expected + // This will cover both cases where an alias could be used in a normal entity vs. an instance for (auto aliasMapIter : oldAliasToNewAliasMap) { - QString oldAlias = QString("\"%1\"").arg(aliasMapIter.first.c_str()); - QString newAlias = QString("\"%1\"").arg(aliasMapIter.second.c_str()); + QString oldAliasQuotes = QString("\"%1\"").arg(aliasMapIter.first.c_str()); + QString newAliasQuotes = QString("\"%1\"").arg(aliasMapIter.second.c_str()); - newEntityDomString.replace(oldAlias, newAlias); + newEntityDomString.replace(oldAliasQuotes, newAliasQuotes); + + QString oldAliasPathRef = QString("/%1").arg(aliasMapIter.first.c_str()); + QString newAliasPathRef = QString("/%1").arg(aliasMapIter.second.c_str()); + + newEntityDomString.replace(oldAliasPathRef, newAliasPathRef); } // Create the new Entity DOM from parsing the JSON string From ed3b1dd8d58c88a6b7097c1dacc1834a17d5a75e Mon Sep 17 00:00:00 2001 From: evanchia Date: Fri, 21 May 2021 12:50:52 -0700 Subject: [PATCH 07/15] adding explicit url for test metrics --- scripts/build/Jenkins/Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index eb0778791d..2f6be2b060 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -352,7 +352,7 @@ def TestMetrics(Map pipelineConfig, String workspace, String branchName, String def command = "${pipelineConfig.PYTHON_DIR}/python.cmd -u mars/scripts/python/ctest_test_metric_scraper.py " + '-e jenkins.creds.user %username% -e jenkins.creds.pass %apitoken% ' + "-e jenkins.base_url ${env.JENKINS_URL} " + - "${cmakeBuildDir} ${branchName} %BUILD_NUMBER% AR ${configuration} ${repoName} " + "${cmakeBuildDir} ${branchName} %BUILD_NUMBER% AR ${configuration} ${repoName} --url ${env.BUILD_URL}" bat label: "Publishing ${buildJobName} Test Metrics", script: command } From d785b3310f0be16c52b1d6ef402a1a76f12f2760 Mon Sep 17 00:00:00 2001 From: Terry Michaels <81711813+tjmichaels@users.noreply.github.com> Date: Fri, 21 May 2021 14:56:59 -0500 Subject: [PATCH 08/15] Make sure to autoscroll to a selected entity after a rename (#872) --- .../UI/Outliner/EntityOutlinerListModel.cpp | 11 +++++++++++ .../UI/Outliner/OutlinerListModel.cpp | 11 +++++++++++ 2 files changed, 22 insertions(+) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp index 5b44594398..4a72afb16b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include //////////////////////////////////////////////////////////////////////////// @@ -1409,6 +1410,16 @@ namespace AzToolsFramework { (void)name; QueueEntityUpdate(entityId); + + bool isSelected = false; + AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( + isSelected, &AzToolsFramework::ToolsApplicationRequests::IsSelected, entityId); + + if (isSelected) + { + // Ask the system to scroll to the entity in case it is off screen after the rename + EntityOutlinerModelNotificationBus::Broadcast(&EntityOutlinerModelNotifications::QueueScrollToNewContent, entityId); + } } void EntityOutlinerListModel::OnEntityInfoUpdatedUnsavedChanges(AZ::EntityId entityId) diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp index ac9b92adce..11c279c7f7 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp @@ -65,6 +65,7 @@ #include "OutlinerTreeView.hxx" #include "Include/ICommandManager.h" #include "Include/IObjectManager.h" +#include "OutlinerCacheBus.h" #include #include @@ -1538,6 +1539,16 @@ void OutlinerListModel::OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZS { (void)name; QueueEntityUpdate(entityId); + + bool isSelected = false; + AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult( + isSelected, &AzToolsFramework::ToolsApplicationRequests::IsSelected, entityId); + + if (isSelected) + { + // Ask the system to scroll to the entity in case it is off screen after the rename + OutlinerModelNotificationBus::Broadcast(&OutlinerModelNotifications::QueueScrollToNewContent, entityId); + } } void OutlinerListModel::OnEntityInfoUpdatedUnsavedChanges(AZ::EntityId entityId) From 35ef2004a6cc2f33db0d66a731ec8fc8b8caadf2 Mon Sep 17 00:00:00 2001 From: pruiksma Date: Fri, 21 May 2021 15:00:39 -0500 Subject: [PATCH 09/15] Halton sequence added to AzCore/Math/Random.h with unit tests. This work is in support of ATOM-13988 for generating sub-pixel camera offsets for TAA jitter. --- Code/Framework/AzCore/AzCore/Math/Random.h | 90 +++++++++++++++++++ .../AzCore/Tests/Math/RandomTests.cpp | 74 +++++++++++++++ .../AzCore/Tests/azcoretests_files.cmake | 1 + 3 files changed, 165 insertions(+) create mode 100644 Code/Framework/AzCore/Tests/Math/RandomTests.cpp diff --git a/Code/Framework/AzCore/AzCore/Math/Random.h b/Code/Framework/AzCore/AzCore/Math/Random.h index 5ae37433ec..165f65dc33 100644 --- a/Code/Framework/AzCore/AzCore/Math/Random.h +++ b/Code/Framework/AzCore/AzCore/Math/Random.h @@ -86,4 +86,94 @@ namespace AZ Normal, UniformReal }; + + //! Halton sequences are deterministic, quasi-random sequences with low discrepancy. They + //! are useful for generating evenly distributed points. + //! See https://en.wikipedia.org/wiki/Halton_sequence for more information. + + //! Returns a single halton number. + //! @param index The index of the number. Indices start at 1. Using index 0 will return 0. + //! @param base The numerical base of the halton number. + inline float GetHaltonNumber(uint32_t index, uint32_t base) + { + float fraction = 1.0f; + float result = 0.0f; + + while (index > 0) + { + fraction = fraction / base; + result += fraction * (index % base); + index = floor(index / base); + } + + return result; + } + + //! A helper class for generating arrays of Halton sequences in n dimensions. + //! The class holds the state of which bases to use, the starting offset + //! of each dimension and how much to increment between each index for each + //! dimension. + template + class HaltonSequence + { + public: + + //! Initializes a Halton sequence with some bases. By default there is no + //! offset and the index increments by one between each number. + HaltonSequence(AZStd::array bases) + : m_bases(bases) + { + m_offsets.fill(1); // Halton sequences start at index 1. + m_increments.fill(1); + } + + //! Returns a Halton sequence in an array of N length + template + AZStd::array, N> GetHaltonSequence() + { + AZStd::array, N> result; + + AZStd::array indices = m_offsets; + + // Generator that returns the Halton number for all bases for a single entry. + auto f = [&] () + { + AZStd::array item; + for (auto d = 0; d < Dimensions; ++d) + { + item[d] = GetHaltonNumber(indices[d], m_bases[d]); + indices[d] += m_increments[d]; + } + return item; + }; + + AZStd::generate(result.begin(), result.end(), f); + return result; + } + + //! Sets the offsets per dimension to start generating a sequence from. + //! By default, there is no offset (offset of 0 corresponds to starting at index 1) + void SetOffsets(AZStd::array offsets) + { + m_offsets = offsets; + + // Halton sequences start at index 1, so increment all the indices. + AZStd::for_each(m_offsets.begin(), m_offsets.end(), [](uint32_t &n){ n++; }); + } + + //! Sets the increment between numbers in the halton sequence per dimension + //! By default this is 1, meaning that no numbers are skipped. Can be negative + //! to generate numbers in reverse order. + void SetIncrements(AZStd::array increments) + { + m_increments = increments; + } + + private: + + AZStd::array m_bases; + AZStd::array m_offsets; + AZStd::array m_increments; + + }; } diff --git a/Code/Framework/AzCore/Tests/Math/RandomTests.cpp b/Code/Framework/AzCore/Tests/Math/RandomTests.cpp new file mode 100644 index 0000000000..ace7d99704 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Math/RandomTests.cpp @@ -0,0 +1,74 @@ +/* +* 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 +#include + +using namespace AZ; + +namespace UnitTest +{ + TEST(MATH_Random, GetHaltonNumber) + { + EXPECT_FLOAT_EQ(0.5, GetHaltonNumber(1, 2)); + EXPECT_FLOAT_EQ(898.0f / 2187.0f, GetHaltonNumber(1234, 3)); + EXPECT_FLOAT_EQ(5981.0f / 15625.0f, GetHaltonNumber(4321, 5)); + } + + TEST(MATH_Random, HaltonSequence) + { + HaltonSequence<3> sequence({ 2, 3, 5 }); + auto regularSequence = sequence.GetHaltonSequence<5>(); + + EXPECT_FLOAT_EQ(1.0f / 2.0f, regularSequence[0][0]); + EXPECT_FLOAT_EQ(1.0f / 3.0f, regularSequence[0][1]); + EXPECT_FLOAT_EQ(1.0f / 5.0f, regularSequence[0][2]); + + EXPECT_FLOAT_EQ(1.0f / 4.0f, regularSequence[1][0]); + EXPECT_FLOAT_EQ(2.0f / 3.0f, regularSequence[1][1]); + EXPECT_FLOAT_EQ(2.0f / 5.0f, regularSequence[1][2]); + + EXPECT_FLOAT_EQ(3.0f / 4.0f, regularSequence[2][0]); + EXPECT_FLOAT_EQ(1.0f / 9.0f, regularSequence[2][1]); + EXPECT_FLOAT_EQ(3.0f / 5.0f, regularSequence[2][2]); + + EXPECT_FLOAT_EQ(1.0f / 8.0f, regularSequence[3][0]); + EXPECT_FLOAT_EQ(4.0f / 9.0f, regularSequence[3][1]); + EXPECT_FLOAT_EQ(4.0f / 5.0f, regularSequence[3][2]); + + EXPECT_FLOAT_EQ(5.0f / 8.0f, regularSequence[4][0]); + EXPECT_FLOAT_EQ(7.0f / 9.0f, regularSequence[4][1]); + EXPECT_FLOAT_EQ(1.0f / 25.0f, regularSequence[4][2]); + + sequence.SetOffsets({ 1, 2, 3 }); + auto offsetSequence = sequence.GetHaltonSequence<2>(); + + EXPECT_FLOAT_EQ(1.0f / 4.0f, offsetSequence[0][0]); + EXPECT_FLOAT_EQ(1.0f / 9.0f, offsetSequence[0][1]); + EXPECT_FLOAT_EQ(4.0f / 5.0f, offsetSequence[0][2]); + + EXPECT_FLOAT_EQ(3.0f / 4.0f, offsetSequence[1][0]); + EXPECT_FLOAT_EQ(4.0f / 9.0f, offsetSequence[1][1]); + EXPECT_FLOAT_EQ(1.0f / 25.0f, offsetSequence[1][2]); + + sequence.SetIncrements({ 1, 2, 3 }); + auto incrementedSequence = sequence.GetHaltonSequence<2>(); + + EXPECT_FLOAT_EQ(1.0f / 4.0f, incrementedSequence[0][0]); + EXPECT_FLOAT_EQ(1.0f / 9.0f, incrementedSequence[0][1]); + EXPECT_FLOAT_EQ(4.0f / 5.0f, incrementedSequence[0][2]); + + EXPECT_FLOAT_EQ(3.0f / 4.0f, incrementedSequence[1][0]); + EXPECT_FLOAT_EQ(7.0f / 9.0f, incrementedSequence[1][1]); + EXPECT_FLOAT_EQ(11.0f / 25.0f, incrementedSequence[1][2]); + } +} diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index f90717d003..78b2701d92 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -152,6 +152,7 @@ set(FILES Math/PlaneTests.cpp Math/QuaternionPerformanceTests.cpp Math/QuaternionTests.cpp + Math/RandomTests.cpp Math/ShapeIntersectionPerformanceTests.cpp Math/ShapeIntersectionTests.cpp Math/SfmtTests.cpp From f3af2722e9011a120b4798a99237442e7d1f0cac Mon Sep 17 00:00:00 2001 From: pruiksma Date: Fri, 21 May 2021 15:10:13 -0500 Subject: [PATCH 10/15] changing floor() to aznumeric_cast() --- Code/Framework/AzCore/AzCore/Math/Random.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Code/Framework/AzCore/AzCore/Math/Random.h b/Code/Framework/AzCore/AzCore/Math/Random.h index 165f65dc33..8b28f6aaad 100644 --- a/Code/Framework/AzCore/AzCore/Math/Random.h +++ b/Code/Framework/AzCore/AzCore/Math/Random.h @@ -103,7 +103,7 @@ namespace AZ { fraction = fraction / base; result += fraction * (index % base); - index = floor(index / base); + index = aznumeric_cast(index / base); } return result; @@ -119,12 +119,12 @@ namespace AZ public: //! Initializes a Halton sequence with some bases. By default there is no - //! offset and the index increments by one between each number. + //! offset and the index increments by 1 between each number. HaltonSequence(AZStd::array bases) : m_bases(bases) { m_offsets.fill(1); // Halton sequences start at index 1. - m_increments.fill(1); + m_increments.fill(1); // By default increment by 1 between each number. } //! Returns a Halton sequence in an array of N length From 6fa78218d251e83a6f69ea5106eaeec7bb68559e Mon Sep 17 00:00:00 2001 From: mbalfour Date: Fri, 21 May 2021 17:32:55 -0500 Subject: [PATCH 11/15] [LYN-3548] Change IMGUI to use list of levels with prefab LoadLevel instead of text box There was an intermittent crash related to garbage in the level name box with prefabs, possibly due to levelName being uninitialized. This change improves the UX by listing out all the possible choices, instead of making people type it in. --- .../Source/LYCommonMenu/ImGuiLYCommonMenu.cpp | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp b/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp index 8e3e2663d5..5107a02835 100644 --- a/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp +++ b/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp @@ -15,6 +15,7 @@ #ifdef IMGUI_ENABLED #include +#include #include #include #include @@ -251,15 +252,37 @@ namespace ImGui if (usePrefabSystemForLevels) { - char levelName[256]; - ImGui::TextColored(ImGui::Colors::s_PlainLabelColor, "Load Level: "); - bool result = ImGui::InputText("", levelName, sizeof(levelName), ImGuiInputTextFlags_EnterReturnsTrue); - if (result) + // Run through all the assets in the asset catalog and gather up the list of level assets + + AZ::Data::AssetType levelAssetType = lvlSystem->GetLevelAssetType(); + AZStd::vector levelNames; + auto enumerateCB = + [levelAssetType, &levelNames]([[maybe_unused]] const AZ::Data::AssetId id, const AZ::Data::AssetInfo& assetInfo) { - AZ_TracePrintf("Imgui", "Attempting to load level '%s'\n", levelName); - AZ::TickBus::QueueFunction([lvlSystem, levelName]() { - lvlSystem->LoadLevel(levelName); - }); + if (assetInfo.m_assetType == levelAssetType) + { + levelNames.emplace_back(assetInfo.m_relativePath); + } + }; + + AZ::Data::AssetCatalogRequestBus::Broadcast( + &AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, nullptr, enumerateCB, nullptr); + + AZStd::sort(levelNames.begin(), levelNames.end()); + + // Create a menu item for each level asset, with an action to load it if selected. + + ImGui::TextColored(ImGui::Colors::s_PlainLabelColor, "Load Level: "); + for (int i = 0; i < levelNames.size(); i++) + { + if (ImGui::MenuItem(AZStd::string::format("%d- %s", i, levelNames[i].c_str()).c_str())) + { + AZ::TickBus::QueueFunction( + [lvlSystem, levelNames, i]() + { + lvlSystem->LoadLevel(levelNames[i].c_str()); + }); + } } } else @@ -269,9 +292,8 @@ namespace ImGui { if (ImGui::MenuItem(AZStd::string::format("%d- %s", i, lvlSystem->GetLevelInfo(i)->GetName()).c_str())) { - AZStd::string mapCommandString = AZStd::string::format("map %s", lvlSystem->GetLevelInfo(i)->GetName()); - AZ::TickBus::QueueFunction([mapCommandString]() { - gEnv->pConsole->ExecuteString(mapCommandString.c_str()); + AZ::TickBus::QueueFunction([lvlSystem, i]() { + lvlSystem->LoadLevel(lvlSystem->GetLevelInfo(i)->GetName()); }); } } From d20f3d8bd4c2a292c76852bd0709aee1943e29ef Mon Sep 17 00:00:00 2001 From: Qing Tao <55564570+VickyAtAZ@users.noreply.github.com> Date: Fri, 21 May 2021 16:38:20 -0700 Subject: [PATCH 12/15] SPEC-6960 Android & Windows asset jobs have failing assets preventing the new ASV code submission gate from passing. Use AZ::u64 instead of uint64_t to avoid issue that PRIu64 won't match uint64_t in some cases --- Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl index fa9395dffc..b3cdb591f4 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl @@ -727,7 +727,7 @@ namespace AZ ImGui::BeginTooltip(); ImGui::Text("Name: %s", passEntry->m_name.GetCStr()); ImGui::Text("Path: %s", passEntry->m_path.GetCStr()); - ImGui::Text("Duration in ticks: %" PRIu64, passEntry->m_timestampResult.GetDurationInTicks()); + ImGui::Text("Duration in ticks: %llu", static_cast(passEntry->m_timestampResult.GetDurationInTicks())); ImGui::Text("Duration in microsecond: %.3f us", passEntry->m_timestampResult.GetDurationInNanoseconds()/1000.f); ImGui::EndTooltip(); } From 6388277cd12c5019f48ee75cdb734a827545bf27 Mon Sep 17 00:00:00 2001 From: antonmic Date: Fri, 21 May 2021 21:20:08 -0700 Subject: [PATCH 13/15] Fixed ImGui Pass as well as how exposure pass enables itself --- .../Assets/Passes/LightAdaptationParent.pass | 2 +- .../ExposureControlSettings.cpp | 25 ---------- .../ExposureControl/ExposureControlSettings.h | 1 - .../PostProcessing/EyeAdaptationPass.cpp | 50 ++++++------------- .../Source/PostProcessing/EyeAdaptationPass.h | 6 +-- .../Include/Atom/RPI.Public/Pass/RenderPass.h | 2 +- .../RPI/Code/Source/RPI.Public/Pass/Pass.cpp | 2 +- .../Source/RPI.Public/Pass/RenderPass.cpp | 2 +- 8 files changed, 21 insertions(+), 69 deletions(-) diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass index 3e804d23e2..ff55ebc200 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass @@ -80,7 +80,7 @@ { "Name": "EyeAdaptationPass", "TemplateName": "EyeAdaptationTemplate", - "Enabled": false, + "Enabled": true, "Connections": [ { "LocalSlot": "SceneLuminanceInput", diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp index b22f4b5861..056f7b7da4 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp @@ -69,7 +69,6 @@ namespace AZ if (m_shouldUpdatePassParameters) { - UpdateEyeAdaptationPass(); UpdateLuminanceHeatmap(); m_shouldUpdatePassParameters = false; @@ -198,30 +197,6 @@ namespace AZ } } - void ExposureControlSettings::UpdateEyeAdaptationPass() - { - auto* passSystem = AZ::RPI::PassSystemInterface::Get(); - - // [GFX-TODO][ATOM-13224] Remove UpdateLuminanceHeatmap and UpdateEyeAdaptationPass - auto passTemplateName = m_eyeAdaptationPassTemplateNameId; - - if (passSystem->HasPassesForTemplateName(passTemplateName)) - { - const AZStd::vector& eyeAdaptationPasses = passSystem->GetPassesForTemplateName(passTemplateName); - for (RPI::Pass* pass : eyeAdaptationPasses) - { - auto* eyeAdaptationPass = azrtti_cast(pass); - auto* renderPipeline = eyeAdaptationPass->GetRenderPipeline(); - - if (renderPipeline && renderPipeline->GetScene() == GetParentScene()) - { - // update eye adaptation pass's enable state - eyeAdaptationPass->UpdateEnable(); - } - } - } - } - void ExposureControlSettings::UpdateLuminanceHeatmap() { auto* passSystem = AZ::RPI::PassSystemInterface::Get(); diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h index 566f60dd28..8344d6aa09 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h @@ -85,7 +85,6 @@ namespace AZ void UpdateExposureControlRelatedPassParameters(); void UpdateLuminanceHeatmap(); - void UpdateEyeAdaptationPass(); PostProcessSettings* m_parentSettings = nullptr; bool m_shouldUpdatePassParameters = true; diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp index 97c11a9d89..e7d4c47f02 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp @@ -62,17 +62,24 @@ namespace AZ m_buffer = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc); } - void EyeAdaptationPass::UpdateEnable() + void EyeAdaptationPass::BuildAttachmentsInternal() { - if (m_pipeline == nullptr) + if (!m_buffer) { - SetEnabled(false); - return; + InitBuffer(); } - AZ_Assert(m_pipeline->GetScene(), "Scene shouldn't nullptr"); + AttachBufferToSlot(EyeAdaptationDataInputOutputSlotName, m_buffer); + } - UpdateInputBufferIndices(); + bool EyeAdaptationPass::IsEnabled() const + { + if (!ComputePass::IsEnabled() || m_pipeline == nullptr) + { + return false; + } + + AZ_Assert(m_pipeline->GetScene(), "EyeAdaptationPass's Pipeline does not have a valid scene pointer"); AZ::RPI::Scene* scene = GetScene(); bool enabled = false; @@ -95,38 +102,9 @@ namespace AZ } } - const bool lastEnabled = IsEnabled(); - SetEnabled(enabled); - - if (IsEnabled() && !lastEnabled) - { - // Need rebuilt this pass's attachment as any connections. So queue parent pass. - GetParent()->QueueForBuildAttachments(); - } + return enabled; } - void EyeAdaptationPass::UpdateInputBufferIndices() - { - if (m_exposureControlBufferInputIndex.IsNull()) - { - m_exposureControlBufferInputIndex = GetView()->GetShaderResourceGroup()->FindShaderInputBufferIndex(Name("m_exposureControl")); - } - } - - void EyeAdaptationPass::BuildAttachmentsInternal() - { - if (m_pipeline == nullptr) - { - return; - } - - if (!m_buffer) - { - InitBuffer(); - } - - AttachBufferToSlot(EyeAdaptationDataInputOutputSlotName, m_buffer); - } void EyeAdaptationPass::FrameBeginInternal(FramePrepareParams params) { diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h index b87d9db86f..cef168a122 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -45,12 +46,11 @@ namespace AZ static RPI::Ptr Create(const RPI::PassDescriptor& descriptor); // Check if we should enable of disable this pass - void UpdateEnable(); + bool IsEnabled() const override; protected: EyeAdaptationPass(const RPI::PassDescriptor& descriptor); void InitBuffer(); - void UpdateInputBufferIndices(); // A StructuredBuffer for exposure calculation on the GPU. struct ExposureCalculationData @@ -65,7 +65,7 @@ namespace AZ AZ::Data::Instance m_buffer; // SRG binding indices... - AZ::RHI::ShaderInputBufferIndex m_exposureControlBufferInputIndex; + AZ::RHI::ShaderInputNameIndex m_exposureControlBufferInputIndex = "m_exposureControl"; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h index 84ccb57e06..5cd917e841 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h @@ -72,7 +72,7 @@ namespace AZ //! Return the View if this pass is associated with a pipeline view via PipelineViewTag. //! It may return nullptr if this pass is independent with any views. - ViewPtr GetView(); + ViewPtr GetView() const; protected: explicit RenderPass(const PassDescriptor& descriptor); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp index 6ed8ac018c..8d613eb2bb 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp @@ -98,7 +98,7 @@ namespace AZ bool Pass::IsEnabled() const { - return m_flags.m_enabled && (m_flags.m_parentEnabled || m_parent == nullptr); + return m_flags.m_enabled; } // --- Error Logging --- diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp index 3f5c16678c..9c6a95e582 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp @@ -342,7 +342,7 @@ namespace AZ } } - ViewPtr RenderPass::GetView() + ViewPtr RenderPass::GetView() const { if (m_flags.m_hasPipelineViewTag && m_pipeline) { From d112ae403b07c7efaef377bab7ad558a55288490 Mon Sep 17 00:00:00 2001 From: Alex Peterson <26804013+AMZN-alexpete@users.noreply.github.com> Date: Fri, 21 May 2021 22:47:41 -0700 Subject: [PATCH 14/15] Engine settings UX update and basic functionality --- Code/Tools/ProjectManager/Resources/Add.svg | 1 - .../Resources/ProjectManager.qrc | 16 +++ .../Resources/ProjectManager.qss | 73 +++++++++++ .../Resources/Select_Folder.svg | 1 - .../ProjectManager/Source/EngineInfo.cpp | 10 +- Code/Tools/ProjectManager/Source/EngineInfo.h | 14 +- .../Source/EngineSettingsScreen.cpp | 87 ++++++++++++- .../Source/EngineSettingsScreen.h | 17 ++- .../Source/EngineSettingsScreen.ui | 82 ------------ .../Source/FirstTimeUseScreen.cpp | 6 +- .../Source/FormBrowseEditWidget.cpp | 49 +++++++ .../Source/FormBrowseEditWidget.h | 33 +++++ .../Source/FormLineEditWidget.cpp | 123 ++++++++++++++++++ .../Source/FormLineEditWidget.h | 60 +++++++++ .../Source/GemCatalog/GemItemDelegate.cpp | 10 +- .../ProjectManager/Source/PathValidator.cpp | 65 +++++++++ .../ProjectManager/Source/PathValidator.h | 45 +++++++ .../Source/ProjectManagerWindow.cpp | 6 +- .../Source/ProjectManagerWindow.ui | 6 +- .../Source/ProjectSettingsCtrl.cpp | 1 + .../Source/ProjectsHomeScreen.ui | 10 +- .../ProjectManager/Source/PythonBindings.cpp | 83 +++++++++++- Code/Tools/ProjectManager/project_manager.qrc | 16 --- .../project_manager_files.cmake | 10 +- cmake/Tools/registration.py | 82 ++++++++++-- 25 files changed, 759 insertions(+), 147 deletions(-) create mode 100644 Code/Tools/ProjectManager/Resources/ProjectManager.qrc delete mode 100644 Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui create mode 100644 Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp create mode 100644 Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h create mode 100644 Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp create mode 100644 Code/Tools/ProjectManager/Source/FormLineEditWidget.h create mode 100644 Code/Tools/ProjectManager/Source/PathValidator.cpp create mode 100644 Code/Tools/ProjectManager/Source/PathValidator.h delete mode 100644 Code/Tools/ProjectManager/project_manager.qrc diff --git a/Code/Tools/ProjectManager/Resources/Add.svg b/Code/Tools/ProjectManager/Resources/Add.svg index d2b9b2e0a6..4fa30932fb 100644 --- a/Code/Tools/ProjectManager/Resources/Add.svg +++ b/Code/Tools/ProjectManager/Resources/Add.svg @@ -1,4 +1,3 @@ - diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc new file mode 100644 index 0000000000..1ffd7cf3e7 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -0,0 +1,16 @@ + + + ProjectManager.qss + + + Add.svg + Select_Folder.svg + o3de_editor.ico + Windows.svg + Android.svg + iOS.svg + Linux.svg + macOS.svg + Backgrounds/FirstTimeBackgroundImage.jpg + + diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index e69de29bb2..16ef48ee7c 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -0,0 +1,73 @@ +/************** General (MainWindow) **************/ +QMainWindow { + background-color: #333333; +} + + +QPushButton:focus { + outline: none; + border:1px solid #1e70eb; +} + +/************** General (Forms) **************/ + +#formLineEditWidget, +#formBrowseEditWidget { + max-width: 780px; +} + +#formFrame { + max-width: 720px; + background-color: #444444; + border:1px solid #dddddd; + border-radius: 4px; + padding: 0px 10px 2px 6px; + margin-top:10px; + margin-left:30px; +} + +#formFrame[Focus="true"] { + border:1px solid #1e70eb; +} + +#formFrame[Valid="false"] { + border:1px solid red; +} + +#formFrame QLabel { + font-size: 13px; + color: #cccccc; +} + +#formFrame QPushButton { + background-color: transparent; + background:transparent url(:/Select_Folder.svg) no-repeat center; + qproperty-flat: true; +} + +#formFrame QPushButton:focus { + border:none; +} + +#formFrame QLineEdit { + background-color: rgba(0,0,0,0); + font-size: 18px; + color: #ffffff; + border:0; + line-height: 30px; + height: 1em; + padding-top: -4px; +} + +#formErrorLabel { + color: #ec3030; + font-size: 14px; + margin-left: 40px; +} + +#formTitleLabel { + font-size:21px; + color:#ffffff; + margin: 10px 0 10px 30px; +} + diff --git a/Code/Tools/ProjectManager/Resources/Select_Folder.svg b/Code/Tools/ProjectManager/Resources/Select_Folder.svg index 72dcd3385e..df20a06e76 100644 --- a/Code/Tools/ProjectManager/Resources/Select_Folder.svg +++ b/Code/Tools/ProjectManager/Resources/Select_Folder.svg @@ -1,4 +1,3 @@ - diff --git a/Code/Tools/ProjectManager/Source/EngineInfo.cpp b/Code/Tools/ProjectManager/Source/EngineInfo.cpp index 8043a498ff..934d3af9d8 100644 --- a/Code/Tools/ProjectManager/Source/EngineInfo.cpp +++ b/Code/Tools/ProjectManager/Source/EngineInfo.cpp @@ -14,8 +14,16 @@ namespace O3DE::ProjectManager { - EngineInfo::EngineInfo(const QString& path) + EngineInfo::EngineInfo(const QString& path, const QString& name, const QString& version, const QString& thirdPartyPath) : m_path(path) + , m_name(name) + , m_version(version) + , m_thirdPartyPath(thirdPartyPath) { } + + bool EngineInfo::IsValid() const + { + return !m_path.isEmpty(); + } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineInfo.h b/Code/Tools/ProjectManager/Source/EngineInfo.h index ada6e73a15..262c42e56b 100644 --- a/Code/Tools/ProjectManager/Source/EngineInfo.h +++ b/Code/Tools/ProjectManager/Source/EngineInfo.h @@ -22,8 +22,20 @@ namespace O3DE::ProjectManager { public: EngineInfo() = default; - EngineInfo(const QString& path); + EngineInfo(const QString& path, const QString& name, const QString& version, const QString& thirdPartyPath); + // from engine.json + QString m_version; + QString m_name; + QString m_thirdPartyPath; + + // from o3de_manifest.json QString m_path; + QString m_defaultProjectsFolder; + QString m_defaultGemsFolder; + QString m_defaultTemplatesFolder; + QString m_defaultRestrictedFolder; + + bool IsValid() const; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp index 1adab41c0e..f51996bd65 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp @@ -11,20 +11,99 @@ */ #include - -#include +#include +#include +#include +#include +#include +#include +#include +#include namespace O3DE::ProjectManager { EngineSettingsScreen::EngineSettingsScreen(QWidget* parent) : ScreenWidget(parent) - , m_ui(new Ui::EngineSettingsClass()) { - m_ui->setupUi(this); + auto* layout = new QVBoxLayout(this); + layout->setAlignment(Qt::AlignTop); + + setObjectName("engineSettingsScreen"); + + EngineInfo engineInfo; + + AZ::Outcome engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); + if (engineInfoResult.IsSuccess()) + { + engineInfo = engineInfoResult.GetValue(); + } + + QLabel* formTitleLabel = new QLabel(tr("O3DE Settings"), this); + formTitleLabel->setObjectName("formTitleLabel"); + layout->addWidget(formTitleLabel); + + m_engineVersion = new FormLineEditWidget(tr("Engine Version"), engineInfo.m_version, this); + m_engineVersion->lineEdit()->setReadOnly(true); + layout->addWidget(m_engineVersion); + + m_thirdParty = new FormBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this); + m_thirdParty->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); + m_thirdParty->lineEdit()->setReadOnly(true); + m_thirdParty->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); + connect(m_thirdParty->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); + layout->addWidget(m_thirdParty); + + m_defaultProjects = new FormBrowseEditWidget(tr("Default Projects Folder"), engineInfo.m_defaultProjectsFolder, this); + m_defaultProjects->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); + m_defaultProjects->lineEdit()->setReadOnly(true); + m_defaultProjects->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); + connect(m_defaultProjects->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); + layout->addWidget(m_defaultProjects); + + m_defaultGems = new FormBrowseEditWidget(tr("Default Gems Folder"), engineInfo.m_defaultGemsFolder, this); + m_defaultGems->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); + m_defaultGems->lineEdit()->setReadOnly(true); + m_defaultGems->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); + connect(m_defaultGems->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); + layout->addWidget(m_defaultGems); + + m_defaultProjectTemplates = new FormBrowseEditWidget(tr("Default Project Templates Folder"), engineInfo.m_defaultTemplatesFolder, this); + m_defaultProjectTemplates->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); + m_defaultProjectTemplates->lineEdit()->setReadOnly(true); + m_defaultProjectTemplates->setErrorLabelText(tr("Please provide a valid path to a folder that exists")); + connect(m_defaultProjectTemplates->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); + layout->addWidget(m_defaultProjectTemplates); + + setLayout(layout); } ProjectManagerScreen EngineSettingsScreen::GetScreenEnum() { return ProjectManagerScreen::EngineSettings; } + + void EngineSettingsScreen::OnTextChanged() + { + // save engine settings + auto engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo(); + if (engineInfoResult.IsSuccess()) + { + EngineInfo engineInfo; + engineInfo = engineInfoResult.GetValue(); + engineInfo.m_thirdPartyPath = m_thirdParty->lineEdit()->text(); + engineInfo.m_defaultProjectsFolder = m_defaultProjects->lineEdit()->text(); + engineInfo.m_defaultGemsFolder = m_defaultGems->lineEdit()->text(); + engineInfo.m_defaultTemplatesFolder = m_defaultProjectTemplates->lineEdit()->text(); + + bool result = PythonBindingsInterface::Get()->SetEngineInfo(engineInfo); + if (!result) + { + QMessageBox::critical(this, tr("Engine Settings"), tr("Failed to save engine settings.")); + } + } + else + { + QMessageBox::critical(this, tr("Engine Settings"), tr("Failed to get engine settings.")); + } + } } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h index 4baa3fb28c..0e91ec2d3b 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h @@ -15,13 +15,11 @@ #include #endif -namespace Ui -{ - class EngineSettingsClass; -} - namespace O3DE::ProjectManager { + QT_FORWARD_DECLARE_CLASS(FormLineEditWidget) + QT_FORWARD_DECLARE_CLASS(FormBrowseEditWidget) + class EngineSettingsScreen : public ScreenWidget { @@ -30,8 +28,15 @@ namespace O3DE::ProjectManager ~EngineSettingsScreen() = default; ProjectManagerScreen GetScreenEnum() override; + protected slots: + void OnTextChanged(); + private: - QScopedPointer m_ui; + FormLineEditWidget* m_engineVersion; + FormBrowseEditWidget* m_thirdParty; + FormBrowseEditWidget* m_defaultProjects; + FormBrowseEditWidget* m_defaultGems; + FormBrowseEditWidget* m_defaultProjectTemplates; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui deleted file mode 100644 index c8fda8bfd7..0000000000 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui +++ /dev/null @@ -1,82 +0,0 @@ - - - EngineSettingsClass - - - - 0 - 0 - 839 - 597 - - - - Form - - - - - - O3DE Settings - - - - - - - Engine Version - - - - - - - v1.01 - - - - - - - 3rd Party Software Folder - - - - - - - - - - Restricted Folder - - - - - - - - - - Default Gems Folder - - - - - - - - - - Default Project Templates Folder - - - - - - - - - - - diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp index 2c96078d43..a1be7e8ac9 100644 --- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp +++ b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp @@ -49,11 +49,11 @@ namespace O3DE::ProjectManager QHBoxLayout* buttonLayout = new QHBoxLayout(); buttonLayout->setSpacing(s_buttonSpacing); - m_createProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Add.svg"), tr("Create Project"), this); + m_createProjectButton = CreateLargeBoxButton(QIcon(":/Add.svg"), tr("Create Project"), this); m_createProjectButton->setIconSize(QSize(s_iconSize, s_iconSize)); buttonLayout->addWidget(m_createProjectButton); - m_addProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Select_Folder.svg"), tr("Add a Project"), this); + m_addProjectButton = CreateLargeBoxButton(QIcon(":/Select_Folder.svg"), tr("Add a Project"), this); m_addProjectButton->setIconSize(QSize(s_iconSize, s_iconSize)); buttonLayout->addWidget(m_addProjectButton); @@ -66,7 +66,7 @@ namespace O3DE::ProjectManager vLayout->addItem(verticalSpacer); // Using border-image allows for scaling options background-image does not support - setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Resources/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }"); + setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }"); connect(m_createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton); connect(m_addProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleAddProjectButton); diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp new file mode 100644 index 0000000000..c30d6a7b30 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp @@ -0,0 +1,49 @@ +/* +* 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 +#include +#include +#include +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + FormBrowseEditWidget::FormBrowseEditWidget(const QString& labelText, const QString& valueText, QWidget* parent) + : FormLineEditWidget(labelText, valueText, parent) + { + setObjectName("formBrowseEditWidget"); + + QPushButton* browseButton = new QPushButton(this); + connect(browseButton, &QPushButton::pressed, this, &FormBrowseEditWidget::HandleBrowseButton); + m_frameLayout->addWidget(browseButton); + } + + void FormBrowseEditWidget::HandleBrowseButton() + { + QString defaultPath = m_lineEdit->text(); + if (defaultPath.isEmpty()) + { + defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation); + } + + QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath)); + if (!directory.isEmpty()) + { + m_lineEdit->setText(directory); + } + + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h new file mode 100644 index 0000000000..887fc29dd9 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h @@ -0,0 +1,33 @@ +/* +* 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. +* +*/ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#endif + +namespace O3DE::ProjectManager +{ + class FormBrowseEditWidget + : public FormLineEditWidget + { + Q_OBJECT + + public: + explicit FormBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr); + ~FormBrowseEditWidget() = default; + + private slots: + void HandleBrowseButton(); + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp new file mode 100644 index 0000000000..7ef7e3c7d8 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp @@ -0,0 +1,123 @@ +/* +* 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace O3DE::ProjectManager +{ + FormLineEditWidget::FormLineEditWidget(const QString& labelText, const QString& valueText, QWidget* parent) + : QWidget(parent) + { + setObjectName("formLineEditWidget"); + + QVBoxLayout* mainLayout = new QVBoxLayout(); + mainLayout->setAlignment(Qt::AlignTop); + { + m_frame = new QFrame(this); + m_frame->setObjectName("formFrame"); + + // use a horizontal box layout so buttons can be added to the right of the field + m_frameLayout = new QHBoxLayout(); + { + QVBoxLayout* fieldLayout = new QVBoxLayout(); + + QLabel* label = new QLabel(labelText, this); + fieldLayout->addWidget(label); + + m_lineEdit = new AzQtComponents::StyledLineEdit(this); + m_lineEdit->setFlavor(AzQtComponents::StyledLineEdit::Question); + AzQtComponents::LineEdit::setErrorIconEnabled(m_lineEdit, false); + m_lineEdit->setText(valueText); + + connect(m_lineEdit, &AzQtComponents::StyledLineEdit::flavorChanged, this, &FormLineEditWidget::flavorChanged); + connect(m_lineEdit, &AzQtComponents::StyledLineEdit::onFocus, this, &FormLineEditWidget::onFocus); + connect(m_lineEdit, &AzQtComponents::StyledLineEdit::onFocusOut, this, &FormLineEditWidget::onFocusOut); + + m_lineEdit->setFrame(false); + fieldLayout->addWidget(m_lineEdit); + + m_frameLayout->addLayout(fieldLayout); + + QWidget* emptyWidget = new QWidget(this); + m_frameLayout->addWidget(emptyWidget); + } + + m_frame->setLayout(m_frameLayout); + + mainLayout->addWidget(m_frame); + + m_errorLabel = new QLabel(this); + m_errorLabel->setObjectName("formErrorLabel"); + m_errorLabel->setVisible(false); + mainLayout->addWidget(m_errorLabel); + } + + setLayout(mainLayout); + } + + void FormLineEditWidget::setErrorLabelText(const QString& labelText) + { + m_errorLabel->setText(labelText); + } + + QLineEdit* FormLineEditWidget::lineEdit() const + { + return m_lineEdit; + } + + void FormLineEditWidget::flavorChanged() + { + if (m_lineEdit->flavor() == AzQtComponents::StyledLineEdit::Flavor::Invalid) + { + m_frame->setProperty("Valid", false); + m_errorLabel->setVisible(true); + } + else + { + m_frame->setProperty("Valid", true); + m_errorLabel->setVisible(false); + } + refreshStyle(); + } + + void FormLineEditWidget::onFocus() + { + m_frame->setProperty("Focus", true); + refreshStyle(); + } + + void FormLineEditWidget::onFocusOut() + { + m_frame->setProperty("Focus", false); + refreshStyle(); + } + + void FormLineEditWidget::refreshStyle() + { + // we must unpolish/polish every child after changing a property + // or else they won't use the correct stylesheet selector + for (auto child : findChildren()) + { + child->style()->unpolish(child); + child->style()->polish(child); + } + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.h b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h new file mode 100644 index 0000000000..3094442cbd --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h @@ -0,0 +1,60 @@ +/* +* 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. +* +*/ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QLineEdit) +QT_FORWARD_DECLARE_CLASS(QLabel) +QT_FORWARD_DECLARE_CLASS(QFrame) +QT_FORWARD_DECLARE_CLASS(QHBoxLayout) + +namespace AzQtComponents +{ + class StyledLineEdit; +} + +namespace O3DE::ProjectManager +{ + class FormLineEditWidget + : public QWidget + { + Q_OBJECT + + public: + explicit FormLineEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr); + ~FormLineEditWidget() = default; + + //! Set the error message for to display when invalid. + void setErrorLabelText(const QString& labelText); + + //! Returns a pointer to the underlying LineEdit. + QLineEdit* lineEdit() const; + + protected: + QLabel* m_errorLabel = nullptr; + QFrame* m_frame = nullptr; + QHBoxLayout* m_frameLayout = nullptr; + AzQtComponents::StyledLineEdit* m_lineEdit = nullptr; + + private slots: + void flavorChanged(); + void onFocus(); + void onFocusOut(); + + private: + void refreshStyle(); + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp index 434a4aeef2..9a45600f70 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp @@ -22,11 +22,11 @@ namespace O3DE::ProjectManager : QStyledItemDelegate(parent) , m_gemModel(gemModel) { - AddPlatformIcon(GemInfo::Android, ":/Resources/Android.svg"); - AddPlatformIcon(GemInfo::iOS, ":/Resources/iOS.svg"); - AddPlatformIcon(GemInfo::Linux, ":/Resources/Linux.svg"); - AddPlatformIcon(GemInfo::macOS, ":/Resources/macOS.svg"); - AddPlatformIcon(GemInfo::Windows, ":/Resources/Windows.svg"); + AddPlatformIcon(GemInfo::Android, ":/Android.svg"); + AddPlatformIcon(GemInfo::iOS, ":/iOS.svg"); + AddPlatformIcon(GemInfo::Linux, ":/Linux.svg"); + AddPlatformIcon(GemInfo::macOS, ":/macOS.svg"); + AddPlatformIcon(GemInfo::Windows, ":/Windows.svg"); } void GemItemDelegate::AddPlatformIcon(GemInfo::Platform platform, const QString& iconPath) diff --git a/Code/Tools/ProjectManager/Source/PathValidator.cpp b/Code/Tools/ProjectManager/Source/PathValidator.cpp new file mode 100644 index 0000000000..8b74284b6c --- /dev/null +++ b/Code/Tools/ProjectManager/Source/PathValidator.cpp @@ -0,0 +1,65 @@ +/* +* 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 "PathValidator.h" + +#include +#include +#include + +namespace O3DE::ProjectManager +{ + PathValidator::PathValidator(PathMode pathMode, QWidget* parent) + : QValidator(parent) + , m_pathMode(pathMode) + { + } + + void PathValidator::setAllowEmpty(bool allowEmpty) + { + m_allowEmpty = allowEmpty; + } + + void PathValidator::setPathMode(PathMode pathMode) + { + m_pathMode = pathMode; + } + + QValidator::State PathValidator::validate(QString &text, int &) const + { + if(text.isEmpty()) + { + return m_allowEmpty ? QValidator::Acceptable : QValidator::Intermediate; + } + + QFileInfo pathInfo(text); + if(!pathInfo.dir().exists()) + { + return QValidator::Intermediate; + } + + switch(m_pathMode) + { + case PathMode::AnyFile://acceptable, as long as it's not an directoy + return pathInfo.isDir() ? QValidator::Intermediate : QValidator::Acceptable; + case PathMode::ExistingFile://must be an existing file + return pathInfo.exists() && pathInfo.isFile() ? QValidator::Acceptable : QValidator::Intermediate; + case PathMode::ExistingFolder://must be an existing folder + return pathInfo.exists() && pathInfo.isDir() ? QValidator::Acceptable : QValidator::Intermediate; + default: + Q_UNREACHABLE(); + } + + return QValidator::Invalid; + } + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/PathValidator.h b/Code/Tools/ProjectManager/Source/PathValidator.h new file mode 100644 index 0000000000..aeb35571b9 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/PathValidator.h @@ -0,0 +1,45 @@ +/* +* 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. +* +*/ + +#pragma once + +#if !defined(Q_MOC_RUN) +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QWidget) + +namespace O3DE::ProjectManager +{ + class PathValidator + : public QValidator + { + public: + enum class PathMode { + ExistingFile, //!< A single, existings file. Useful for "Open file" + ExistingFolder, //!< A single, existing directory. Useful for "Open Folder" + AnyFile //!< A single, valid file, doesn't have to exist but the directory must. Useful for "Save File" + }; + + explicit PathValidator(PathMode pathMode, QWidget* parent = nullptr); + ~PathValidator() = default; + + void setAllowEmpty(bool allowEmpty); + void setPathMode(PathMode pathMode); + + QValidator::State validate(QString &text, int &) const override; + + private: + PathMode m_pathMode = PathMode::AnyFile; + bool m_allowEmpty = false; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp index 6b9d268564..121add657f 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp @@ -44,10 +44,10 @@ namespace O3DE::ProjectManager QDir rootDir = QString::fromUtf8(engineRootPath.Native().data(), aznumeric_cast(engineRootPath.Native().size())); const auto pathOnDisk = rootDir.absoluteFilePath("Code/Tools/ProjectManager/Resources"); - const auto qrcPath = QStringLiteral(":/ProjectManagerWindow"); - AzQtComponents::StyleManager::addSearchPaths("projectmanagerwindow", pathOnDisk, qrcPath, engineRootPath); + const auto qrcPath = QStringLiteral(":/ProjectManager/style"); + AzQtComponents::StyleManager::addSearchPaths("style", pathOnDisk, qrcPath, engineRootPath); - AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("projectlauncherwindow:ProjectManagerWindow.qss")); + AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:ProjectManager.qss")); QVector screenEnums = { diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui index a71ed3aabf..4e33511bff 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui @@ -41,8 +41,8 @@ Icon - - :/Resources/o3de_editor.ico:/Resources/o3de_editor.ico + + :/o3de_editor.ico:/o3de_editor.ico @@ -61,7 +61,7 @@ - + diff --git a/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp b/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp index fd1013c871..95dcec3e18 100644 --- a/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp @@ -106,6 +106,7 @@ namespace O3DE::ProjectManager auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo); if (result.IsSuccess()) { + // adding gems is not implemented yet because we don't know what targets to add or how to add them emit ChangeScreenRequest(ProjectManagerScreen::ProjectsHome); } else diff --git a/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui b/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui index ea3e34d84b..2ba93ccf90 100644 --- a/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui +++ b/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui @@ -48,8 +48,8 @@ - - :/Resources/Add.svg:/Resources/Add.svg + + :/Add.svg:/Add.svg @@ -65,8 +65,8 @@ - - :/Resources/Select_Folder.svg:/Resources/Select_Folder.svg + + :/Select_Folder.svg:/Select_Folder.svg @@ -131,7 +131,7 @@ - + diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index e4642c95e0..9a5e82dafb 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -328,12 +328,91 @@ namespace O3DE::ProjectManager AZ::Outcome PythonBindings::GetEngineInfo() { + EngineInfo engineInfo; + bool result = ExecuteWithLock([&] { + pybind11::str enginePath = m_registration.attr("get_this_engine_path")(); + + auto o3deData = m_registration.attr("load_o3de_manifest")(); + if (pybind11::isinstance(o3deData)) + { + engineInfo.m_path = Py_To_String(enginePath); + engineInfo.m_defaultGemsFolder = Py_To_String(o3deData["default_gems_folder"]); + engineInfo.m_defaultProjectsFolder = Py_To_String(o3deData["default_projects_folder"]); + engineInfo.m_defaultRestrictedFolder = Py_To_String(o3deData["default_restricted_folder"]); + engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]); + engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"third_party_path",""); + } + + auto engineData = m_registration.attr("get_engine_data")(pybind11::none(), enginePath); + if (pybind11::isinstance(engineData)) + { + try + { + engineInfo.m_version = Py_To_String_Optional(engineData,"O3DEVersion","0.0.0.0"); + engineInfo.m_name = Py_To_String_Optional(engineData,"engine_name","O3DE"); + } + catch ([[maybe_unused]] const std::exception& e) + { + AZ_Warning("PythonBindings", false, "Failed to get EngineInfo from %s", Py_To_String(enginePath)); + } + } + }); + + if (!result || !engineInfo.IsValid()) + { + return AZ::Failure(); + } + else + { + return AZ::Success(AZStd::move(engineInfo)); + } + return AZ::Failure(); } - bool PythonBindings::SetEngineInfo([[maybe_unused]] const EngineInfo& engineInfo) + bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo) { - return false; + bool result = ExecuteWithLock([&] { + pybind11::str enginePath = engineInfo.m_path.toStdString(); + pybind11::str defaultProjectsFolder = engineInfo.m_defaultProjectsFolder.toStdString(); + pybind11::str defaultGemsFolder = engineInfo.m_defaultGemsFolder.toStdString(); + pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString(); + + auto registrationResult = m_registration.attr("register")( + enginePath, // engine_path + pybind11::none(), // project_path + pybind11::none(), // gem_path + pybind11::none(), // template_path + pybind11::none(), // restricted_path + pybind11::none(), // repo_uri + pybind11::none(), // default_engines_folder + defaultProjectsFolder, + defaultGemsFolder, + defaultTemplatesFolder + ); + + if (registrationResult.cast() != 0) + { + result = false; + } + + auto manifest = m_registration.attr("load_o3de_manifest")(); + if (pybind11::isinstance(manifest)) + { + try + { + manifest["third_party_path"] = engineInfo.m_thirdPartyPath.toStdString(); + m_registration.attr("save_o3de_manifest")(manifest); + } + catch ([[maybe_unused]] const std::exception& e) + { + AZ_Warning("PythonBindings", false, "Failed to set third party path."); + } + } + + }); + + return result; } AZ::Outcome PythonBindings::GetGem(const QString& path) diff --git a/Code/Tools/ProjectManager/project_manager.qrc b/Code/Tools/ProjectManager/project_manager.qrc deleted file mode 100644 index f36633142f..0000000000 --- a/Code/Tools/ProjectManager/project_manager.qrc +++ /dev/null @@ -1,16 +0,0 @@ - - - Resources/ProjectManager.qss - Resources/Add.svg - Resources/Select_Folder.svg - Resources/o3de_editor.ico - Resources/Windows.svg - Resources/Android.svg - Resources/iOS.svg - Resources/Linux.svg - Resources/macOS.svg - Resources/ArrowDownLine.svg - Resources/ArrowUpLine.svg - Resources/Backgrounds/FirstTimeBackgroundImage.jpg - - diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 3594d1e079..858fb972aa 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -10,7 +10,8 @@ # set(FILES - project_manager.qrc + Resources/ProjectManager.qrc + Resources/ProjectManager.qss Source/main.cpp Source/ScreenDefs.h Source/ScreenFactory.h @@ -22,6 +23,12 @@ set(FILES Source/EngineInfo.cpp Source/FirstTimeUseScreen.h Source/FirstTimeUseScreen.cpp + Source/FormLineEditWidget.h + Source/FormLineEditWidget.cpp + Source/FormBrowseEditWidget.h + Source/FormBrowseEditWidget.cpp + Source/PathValidator.h + Source/PathValidator.cpp Source/ProjectManagerWindow.h Source/ProjectManagerWindow.cpp Source/ProjectTemplateInfo.h @@ -44,7 +51,6 @@ set(FILES Source/ProjectSettingsScreen.ui Source/EngineSettingsScreen.h Source/EngineSettingsScreen.cpp - Source/EngineSettingsScreen.ui Source/LinkWidget.h Source/LinkWidget.cpp Source/TagWidget.h diff --git a/cmake/Tools/registration.py b/cmake/Tools/registration.py index 184d2cdb31..b13419414b 100755 --- a/cmake/Tools/registration.py +++ b/cmake/Tools/registration.py @@ -2118,27 +2118,35 @@ def find_engine_data(json_data: dict, return None -def get_engine_data(engine_name: str = None, - engine_path: str or pathlib.Path = None, ) -> dict or None: +def _validate_engine_name_and_path(engine_name: str = None, + engine_path: str or pathlib.Path = None) -> pathlib.Path or None: if not engine_name and not engine_path: logger.error('Must specify either a Engine name or Engine Path.') - return 1 + return None if engine_name and not engine_path: engine_path = get_registered(engine_name=engine_name) if not engine_path: logger.error(f'Engine Path {engine_path} has not been registered.') - return 1 + return None engine_path = pathlib.Path(engine_path).resolve() engine_json = engine_path / 'engine.json' if not engine_json.is_file(): logger.error(f'Engine json {engine_json} is not present.') - return 1 + return None if not valid_o3de_engine_json(engine_json): logger.error(f'Engine json {engine_json} is not valid.') - return 1 + return None + + return engine_json + +def get_engine_data(engine_name: str = None, + engine_path: str or pathlib.Path = None ) -> dict or None: + engine_json = _validate_engine_name_and_path(engine_name, engine_path) + if not engine_json: + return None with engine_json.open('r') as f: try: @@ -2150,6 +2158,26 @@ def get_engine_data(engine_name: str = None, return None +def set_engine_data(engine_name: str = None, + engine_path: str or pathlib.Path = None, + engine_data: dict = None ) -> int: + if not engine_data: + logger.error('Must provide engine data.') + return 1 + + engine_json = _validate_engine_name_and_path(engine_name, engine_path) + if not engine_json: + return 1 + + with engine_json.open('w') as f: + try: + json.dump(engine_data, f, indent=4) + except Exception as e: + logger.warn(f'Failed to load or write {engine_json}: {str(e)}') + return 1 + + return 0 + def get_project_data(project_name: str = None, project_path: str or pathlib.Path = None, ) -> dict or None: @@ -2184,27 +2212,36 @@ def get_project_data(project_name: str = None, return None -def get_gem_data(gem_name: str = None, - gem_path: str or pathlib.Path = None, ) -> dict or None: +def _validate_gem_name_and_path(gem_name: str = None, + gem_path: str or pathlib.Path = None) -> pathlib.Path or None: if not gem_name and not gem_path: logger.error('Must specify either a Gem name or Gem Path.') - return 1 + return None if gem_name and not gem_path: gem_path = get_registered(gem_name=gem_name) if not gem_path: logger.error(f'Gem Path {gem_path} has not been registered.') - return 1 + return None gem_path = pathlib.Path(gem_path).resolve() gem_json = gem_path / 'gem.json' if not gem_json.is_file(): logger.error(f'Gem json {gem_json} is not present.') - return 1 + return None if not valid_o3de_gem_json(gem_json): logger.error(f'Gem json {gem_json} is not valid.') - return 1 + return None + + return gem_json + + +def get_gem_data(gem_name: str = None, + gem_path: str or pathlib.Path = None) -> dict or None: + gem_json = _validate_gem_name_and_path(gem_name, gem_path) + if not gem_json: + return None with gem_json.open('r') as f: try: @@ -2217,6 +2254,27 @@ def get_gem_data(gem_name: str = None, return None +def set_gem_data(gem_name: str = None, + gem_path: str or pathlib.Path = None, + gem_data: dict = None) -> int: + if not gem_data: + logger.error('Must provide Gem data.') + return 1 + + gem_json = _validate_gem_name_and_path(gem_name, gem_path) + if not gem_json: + return 1 + + with gem_json.open('w') as f: + try: + json.dump(gem_data, f, indent=4) + except Exception as e: + logger.warn(f'Failed to load and write {gem_json}: {str(e)}') + return 1 + + return 0 + + def get_template_data(template_name: str = None, template_path: str or pathlib.Path = None, ) -> dict or None: if not template_name and not template_path: From 0b9e98c50e4c7a87b9281990be57fef2421cfcc1 Mon Sep 17 00:00:00 2001 From: antonmic Date: Sat, 22 May 2021 00:27:06 -0700 Subject: [PATCH 15/15] Fix for potential lighting pass crash with uninitialized pipeline --- .../CoreLights/LightCullingTilePreparePass.cpp | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp index a76467bccd..816ef25bc6 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp @@ -175,12 +175,18 @@ namespace AZ void LightCullingTilePreparePass::OnShaderReinitialized(const AZ::RPI::Shader&) { LoadShader(); - ChooseShaderVariant(); + if (!m_flags.m_queuedForBuildAttachment && !m_flags.m_isBuildingAttachments) + { + ChooseShaderVariant(); + } } void LightCullingTilePreparePass::OnShaderAssetReinitialized(const Data::Asset&) { LoadShader(); - ChooseShaderVariant(); + if (!m_flags.m_queuedForBuildAttachment && !m_flags.m_isBuildingAttachments) + { + ChooseShaderVariant(); + } } void LightCullingTilePreparePass::OnShaderVariantReinitialized( @@ -188,7 +194,10 @@ namespace AZ AZ::RPI::ShaderVariantStableId) { LoadShader(); - ChooseShaderVariant(); + if (!m_flags.m_queuedForBuildAttachment && !m_flags.m_isBuildingAttachments) + { + ChooseShaderVariant(); + } } } // namespace Render