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 571aea5875..579f465eb2 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 @@ -83,7 +87,8 @@ namespace AzToolsFramework commonRootInstanceDomBeforeCreate, commonRootEntityOwningInstance->get()); AZStd::vector entities; - AZStd::vector> instances; + AZStd::vector> instancePtrs; + AZStd::vector instances; AZStd::unordered_map nestedInstanceLinkPatchesMap; // Retrieve all entities affected and identify Instances @@ -93,10 +98,18 @@ 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) { + AZStd::unique_ptr outInstance = commonRootEntityOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias()); + auto linkRef = m_prefabSystemComponentInterface->FindLink(nestedInstance->GetLinkId()); if (linkRef.has_value()) @@ -104,10 +117,12 @@ namespace AzToolsFramework PrefabDom oldLinkPatches; oldLinkPatches.CopyFrom(linkRef->get().GetLinkDom(), oldLinkPatches.GetAllocator()); - nestedInstanceLinkPatchesMap.emplace(nestedInstance.get(), AZStd::move(oldLinkPatches)); + nestedInstanceLinkPatchesMap.emplace(nestedInstance, AZStd::move(oldLinkPatches)); } - RemoveLink(nestedInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); + RemoveLink(outInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch()); + + instancePtrs.emplace_back(AZStd::move(outInstance)); } PrefabUndoHelpers::UpdatePrefabInstance( @@ -123,7 +138,7 @@ namespace AzToolsFramework // Create the Prefab instanceToCreate = prefabEditorEntityOwnershipInterface->CreatePrefab( - entities, AZStd::move(instances), filePath, commonRootEntityOwningInstance); + entities, AZStd::move(instancePtrs), filePath, commonRootEntityOwningInstance); if (!instanceToCreate) { @@ -388,7 +403,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); @@ -710,6 +725,151 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } + PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + if (entityIds.empty()) + { + return AZ::Failure(AZStd::string("No entities to duplicate.")); + } + + if (!EntitiesBelongToSameInstance(entityIds)) + { + 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 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 + AzToolsFramework::EntityIdSet duplicationSet = AzToolsFramework::GetCulledEntityHierarchy(entityIds); + + AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework); + + 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, commonEntityOwningInstance->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, commonEntityOwningInstance->get(), entities, instances); + + if (!success) + { + 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 + Prefab::PrefabDom instanceDomAfter; + instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); + + AZStd::unordered_map oldAliasToNewAliasMap; + AZStd::unordered_map aliasToEntityDomMap; + + for (AZ::Entity* entity : entities) + { + EntityAliasOptionalReference oldAliasRef = commonEntityOwningInstance->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)); + + rapidjson::StringBuffer buffer; + 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 + // 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 oldAliasQuotes = QString("\"%1\"").arg(aliasMapIter.first.c_str()); + QString newAliasQuotes = QString("\"%1\"").arg(aliasMapIter.second.c_str()); + + 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 + 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("Entity duplication"); + command->SetParent(undoBatch.GetUndoBatch()); + command->Capture(instanceDomBefore, instanceDomAfter, commonEntityOwningInstance->get().GetTemplateId()); + command->RunRedo(); + + EntityIdList duplicatedEntityIds; + for (auto aliasMapIter : oldAliasToNewAliasMap) + { + EntityAlias newEntityAlias = aliasMapIter.second; + + 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(); + } + PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) { if (entityIds.empty()) @@ -737,17 +897,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. @@ -775,7 +925,7 @@ namespace AzToolsFramework if (deleteDescendants) { AZStd::vector entities; - AZStd::vector> instances; + AZStd::vector instances; bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->get(), entities, instances); @@ -786,13 +936,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 @@ -804,7 +956,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 { @@ -822,17 +974,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(); } @@ -944,7 +1091,7 @@ namespace AzToolsFramework bool PrefabPublicHandler::RetrieveAndSortPrefabEntitiesAndInstances( const EntityList& inputEntities, Instance& commonRootEntityOwningInstance, - EntityList& outEntities, AZStd::vector>& outInstances) const + EntityList& outEntities, AZStd::vector& outInstances) const { if (inputEntities.size() == 0) { @@ -1028,14 +1175,14 @@ namespace AzToolsFramework for (AZ::Entity* entity : entities) { - outEntities.emplace_back(commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release()); + outEntities.emplace_back(entity); } outInstances.clear(); outInstances.reserve(instances.size()); for (Instance* instancePtr : instances) { - outInstances.push_back(AZStd::move(commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias()))); + 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 5ad5b4a9cf..223a725c6c 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) 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/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/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..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 @@ -32,6 +43,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); @@ -41,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 af90309867..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, @@ -57,6 +69,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 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;