diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h index 4a69ade6d2..d14203e2e5 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h @@ -174,6 +174,8 @@ namespace AzToolsFramework static EntityAlias GenerateEntityAlias(); AliasPath GetAbsoluteInstanceAliasPath() const; + static InstanceAlias GenerateInstanceAlias(); + protected: /** * Gets the entities owned by this instance @@ -190,8 +192,6 @@ namespace AzToolsFramework bool RegisterEntity(const AZ::EntityId& entityId, const EntityAlias& entityAlias); AZStd::unique_ptr DetachEntity(const EntityAlias& entityAlias); - static InstanceAlias GenerateInstanceAlias(); - // Provide access to private data members in the serializer friend class JsonInstanceSerializer; friend class InstanceEntityIdMapper; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceUpdateExecutor.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceUpdateExecutor.cpp index e0d29f2ab1..6194adf784 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceUpdateExecutor.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/InstanceUpdateExecutor.cpp @@ -152,6 +152,30 @@ namespace AzToolsFramework Instance::EntityList newEntities; if (PrefabDomUtils::LoadInstanceFromPrefabDom(*instanceToUpdate, newEntities, currentTemplate.GetPrefabDom())) { + // If a link was created for a nested instance before the changes were propagated, + // then we associate it correctly here + instanceToUpdate->GetNestedInstances([&](AZStd::unique_ptr& nestedInstance) { + if (nestedInstance->GetLinkId() != InvalidLinkId) + { + return; + } + + for (auto linkId : currentTemplate.GetLinks()) + { + LinkReference nestedLink = m_prefabSystemComponentInterface->FindLink(linkId); + if (!nestedLink.has_value()) + { + continue; + } + + if (nestedLink->get().GetInstanceName() == nestedInstance->GetInstanceAlias()) + { + nestedInstance->SetLinkId(linkId); + break; + } + } + }); + AzToolsFramework::EditorEntityContextRequestBus::Broadcast( &AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, newEntities); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 27c812ff9d..4656dcf48f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -899,16 +899,29 @@ namespace AzToolsFramework if (!EntitiesBelongToSameInstance(entityIds)) { - return AZ::Failure(AZStd::string("Cannot duplicate multiple " - "entities belonging to different instances with one operation.")); + return AZ::Failure(AZStd::string("Cannot duplicate multiple entities belonging to different instances with one operation." + "Change your selection to contain entities in the same instance.")); } // 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"); + AZ::EntityId firstEntityIdToDuplicate = entityIds[0]; + InstanceOptionalReference commonOwningInstance = GetOwnerInstanceByEntityId(firstEntityIdToDuplicate); + if (!commonOwningInstance.has_value()) + { + return AZ::Failure(AZStd::string("Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided.")); + } + + // If the first entity id is a container entity id, then we need to mark its parent as the common owning instance because you + // cannot duplicate an instance from itself. + if (commonOwningInstance->get().GetContainerEntityId() == firstEntityIdToDuplicate) + { + commonOwningInstance = commonOwningInstance->get().GetParentInstance(); + } + if (!commonOwningInstance.has_value()) + { + return AZ::Failure(AZStd::string("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 @@ -921,105 +934,63 @@ namespace AzToolsFramework { 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); + bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->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; + // Take a snapshot of the instance DOM before we manipulate it + PrefabDom instanceDomBefore; + m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, commonOwningInstance->get()); + + // Make a copy of our before instance DOM where we will add our duplicated entities and/or instances + PrefabDom instanceDomAfter; instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); - AZStd::unordered_map oldAliasToNewAliasMap; - AZStd::unordered_map aliasToEntityDomMap; + EntityIdList duplicatedEntityAndInstanceIds; - 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(); + // Duplicate any nested entities and instances as requested + AZStd::unordered_map newInstanceAliasToOldInstanceMap; + DuplicateNestedEntitiesInInstance(commonOwningInstance->get(), + entities, instanceDomAfter, duplicatedEntityAndInstanceIds); + DuplicateNestedInstancesInInstance(commonOwningInstance->get(), + instances, instanceDomAfter, duplicatedEntityAndInstanceIds, + newInstanceAliasToOldInstanceMap); - // 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) - { - ReplaceOldAliases(newEntityDomString, aliasMapIter.first, aliasMapIter.second); - } - - // 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"); + PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity/Instance duplication"); command->SetParent(undoBatch.GetUndoBatch()); - command->Capture(instanceDomBefore, instanceDomAfter, commonEntityOwningInstance->get().GetTemplateId()); - command->RunRedo(); + command->Capture(instanceDomBefore, instanceDomAfter, commonOwningInstance->get().GetTemplateId()); + command->Redo(); - EntityIdList duplicatedEntityIds; - for (auto aliasMapIter : oldAliasToNewAliasMap) + // Create links for our duplicated instances (if any were duplicated) + for (auto [newInstanceAlias, oldInstance] : newInstanceAliasToOldInstanceMap) { - EntityAlias newEntityAlias = aliasMapIter.second; + LinkId oldLinkId = oldInstance->GetLinkId(); + auto linkRef = m_prefabSystemComponentInterface->FindLink(oldLinkId); + AZ_Assert( + linkRef.has_value(), "Unable to find link with id '%llu' during instance duplication.", + oldLinkId); - AliasPath absoluteEntityPath = commonEntityOwningInstance->get().GetAbsoluteInstanceAliasPath(); - absoluteEntityPath.Append(newEntityAlias); + PrefabDomValueReference linkPatches = linkRef->get().GetLinkPatches(); + AZ_Assert( + linkPatches.has_value(), "Link with id '%llu' is missing patches.", + oldLinkId); - AZ::EntityId newEntityId = InstanceEntityIdMapper::GenerateEntityIdForAliasPath(absoluteEntityPath); - duplicatedEntityIds.push_back(newEntityId); + PrefabDom linkPatchesCopy; + linkPatchesCopy.CopyFrom(linkPatches->get(), linkPatchesCopy.GetAllocator()); + + m_prefabSystemComponentInterface->CreateLink( + commonOwningInstance->get().GetTemplateId(), oldInstance->GetTemplateId(), newInstanceAlias, linkPatchesCopy); } - // Select the duplicated entities - auto selectionUndo = aznew SelectionCommand(duplicatedEntityIds, "Select Duplicated Entities"); + // Select the duplicated entities/instances + auto selectionUndo = aznew SelectionCommand(duplicatedEntityAndInstanceIds, "Select Duplicated Entities/Instances"); selectionUndo->SetParent(undoBatch.GetUndoBatch()); ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, selectionUndo); } @@ -1508,8 +1479,159 @@ namespace AzToolsFramework return true; } + void PrefabPublicHandler::DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance, + const AZStd::vector& entities, PrefabDom& domToAddDuplicatedEntitiesUnder, + EntityIdList& duplicatedEntityIds) + { + if (entities.empty()) + { + return; + } + + AZStd::unordered_map oldAliasToNewAliasMap; + AZStd::unordered_map aliasToEntityDomMap; + + for (AZ::Entity* entity : entities) + { + EntityAliasOptionalReference oldAliasRef = commonOwningInstance.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 + PrefabDom entityDomBefore(&domToAddDuplicatedEntitiesUnder.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.emplace(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.emplace(newEntityAlias, entityDomString); + } + + auto entitiesIter = domToAddDuplicatedEntitiesUnder.FindMember(PrefabDomUtils::EntitiesName); + AZ_Assert(entitiesIter != domToAddDuplicatedEntitiesUnder.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 [newEntityAlias, newEntityDomString] : aliasToEntityDomMap) + { + // Replace all of the old alias references with the new ones + for (auto [oldAlias, newAlias] : oldAliasToNewAliasMap) + { + ReplaceOldAliases(newEntityDomString, oldAlias, newAlias); + } + + // Create the new Entity DOM from parsing the JSON string + PrefabDom entityDomAfter(&domToAddDuplicatedEntitiesUnder.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(), domToAddDuplicatedEntitiesUnder.GetAllocator()); + entitiesIter->value.AddMember(AZStd::move(aliasName), entityDomAfter, domToAddDuplicatedEntitiesUnder.GetAllocator()); + } + + for (auto aliasMapIter : oldAliasToNewAliasMap) + { + EntityAlias newEntityAlias = aliasMapIter.second; + + AliasPath absoluteEntityPath = commonOwningInstance.GetAbsoluteInstanceAliasPath(); + absoluteEntityPath.Append(newEntityAlias); + + AZ::EntityId newEntityId = InstanceEntityIdMapper::GenerateEntityIdForAliasPath(absoluteEntityPath); + duplicatedEntityIds.push_back(newEntityId); + } + } + + void PrefabPublicHandler::DuplicateNestedInstancesInInstance(Instance& commonOwningInstance, + const AZStd::vector& instances, PrefabDom& domToAddDuplicatedInstancesUnder, + EntityIdList& duplicatedEntityIds, AZStd::unordered_map& newInstanceAliasToOldInstanceMap) + { + if (instances.empty()) + { + return; + } + + AZStd::unordered_map oldInstanceAliasToNewInstanceAliasMap; + AZStd::unordered_map aliasToInstanceDomMap; + + for (auto instance : instances) + { + PrefabDom nestedInstanceDomBefore; + m_instanceToTemplateInterface->GenerateDomForInstance(nestedInstanceDomBefore, *instance); + + // Keep track of the old alias <-> new alias mapping for this duplicated instance + // so we can fixup references later + InstanceAlias oldAlias = instance->GetInstanceAlias(); + InstanceAlias newInstanceAlias = Instance::GenerateInstanceAlias(); + oldInstanceAliasToNewInstanceAliasMap.emplace(oldAlias, newInstanceAlias); + + // Keep track of our new instance alias with the Instance it was duplicated from, + // so that after all instances are duplicated, we can go back and create links for them + newInstanceAliasToOldInstanceMap.emplace(newInstanceAlias, instance); + + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + nestedInstanceDomBefore.Accept(writer); + + // Store our duplicated Instance DOM with its new alias as a string + // so that we can fixup instance alias references before adding it + // to the Instances member of our instance DOM + QString instanceDomString(buffer.GetString()); + aliasToInstanceDomMap.emplace(newInstanceAlias, instanceDomString); + } + + auto instancesIter = domToAddDuplicatedInstancesUnder.FindMember(PrefabDomUtils::InstancesName); + AZ_Assert(instancesIter != domToAddDuplicatedInstancesUnder.MemberEnd(), "Instance DOM missing the Instances member."); + + // Now that all the duplicated Instance DOMs have been created, we need to iterate + // through them and replace any previous InstanceAlias references with the new ones. + for (auto [newInstanceAlias, newInstanceDomString]: aliasToInstanceDomMap) + { + // Replace all of the old alias references with the new ones + for (auto [oldAlias, newAlias] : oldInstanceAliasToNewInstanceAliasMap) + { + ReplaceOldAliases(newInstanceDomString, oldAlias, newAlias); + } + + // Create the new Instance DOM from parsing the JSON string + PrefabDom nestedInstanceDomAfter(&domToAddDuplicatedInstancesUnder.GetAllocator()); + nestedInstanceDomAfter.Parse(newInstanceDomString.toUtf8().constData()); + + // Add the new Instance DOM to the Instances member of the instance + rapidjson::Value aliasName(newInstanceAlias.c_str(), newInstanceAlias.length(), domToAddDuplicatedInstancesUnder.GetAllocator()); + instancesIter->value.AddMember(AZStd::move(aliasName), nestedInstanceDomAfter, domToAddDuplicatedInstancesUnder.GetAllocator()); + } + + for (auto aliasMapIter : oldInstanceAliasToNewInstanceAliasMap) + { + InstanceAlias newInstanceAlias = aliasMapIter.second; + + AliasPath absoluteInstancePath = commonOwningInstance.GetAbsoluteInstanceAliasPath(); + absoluteInstancePath.Append(newInstanceAlias); + + AZ::EntityId newEntityId = InstanceEntityIdMapper::GenerateEntityIdForAliasPath(absoluteInstancePath); + duplicatedEntityIds.push_back(newEntityId); + } + } + void PrefabPublicHandler::ReplaceOldAliases(QString& stringToReplace, AZStd::string_view oldAlias, AZStd::string_view newAlias) { + // 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 QString oldAliasQuotes = QString("\"%1\"").arg(oldAlias.data()); QString newAliasQuotes = QString("\"%1\"").arg(newAlias.data()); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index 99fe8e5b67..167791d1c1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -73,6 +73,33 @@ namespace AzToolsFramework InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const; bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const; + + /** + * Duplicate a list of entities owned by a common owning instance by directly + * copying/modifying their entries in the instance DOM + * + * \param commonOwningInstance The common owning instance of all the entities being duplicated. + * \param entities The list of Entities that will be duplicated. + * \param domToAddDuplicatedEntitiesUnder The DOM of the common owning instance where the duplicated + * entity DOM values will be added to. + * \param duplicatedEntityIds A list of EntityIds corresponding to the entities that were duplicated. + */ + void DuplicateNestedEntitiesInInstance(Instance& commonOwningInstance, + const AZStd::vector& entities, PrefabDom& domToAddDuplicatedEntitiesUnder, + EntityIdList& duplicatedEntityIds); + /** + * Duplicate a list of instances owned by a common owning instance by directly + * copying/modifying their entries in the instance DOM + * + * \param commonOwningInstance The common owning instance of all the instances being duplicated. + * \param entities The list of Instances that will be duplicated. + * \param domToAddDuplicatedInstancesUnder The DOM of the common owning instance where the duplicated + * instance DOM values will be added to. + * \param duplicatedEntityIds A list of EntityIds corresponding to the instances that were duplicated. + */ + void DuplicateNestedInstancesInInstance(Instance& commonOwningInstance, + const AZStd::vector& instances, PrefabDom& domToAddDuplicatedInstancesUnder, + EntityIdList& duplicatedEntityIds, AZStd::unordered_map& newInstanceAliasToOldInstanceMap); /** * Applies the correct transform changes to the container entity based on the parent and child entities provided, and returns an appropriate patch.