[LYN-2446] Implemented support for duplicating instances. (#1097)

* [LYN-2446] Implemented support for duplicating instances.

* [LYN-2446] Addressed PR feedback.

* [LYN-2446] Addressed additional PR feedback.
This commit is contained in:
cgalvan
2021-06-03 18:20:59 -05:00
committed by GitHub
parent 816d05ef2d
commit deb3c5e74a
4 changed files with 259 additions and 86 deletions
@@ -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<AZ::Entity> DetachEntity(const EntityAlias& entityAlias);
static InstanceAlias GenerateInstanceAlias();
// Provide access to private data members in the serializer
friend class JsonInstanceSerializer;
friend class InstanceEntityIdMapper;
@@ -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<Instance>& 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);
}
@@ -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<AZ::Entity*> entities;
AZStd::vector<Instance*> 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<EntityAlias, EntityAlias> oldAliasToNewAliasMap;
AZStd::unordered_map<EntityAlias, QString> 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<InstanceAlias, Instance*> 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<rapidjson::StringBuffer> 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<AZ::Entity*>& entities, PrefabDom& domToAddDuplicatedEntitiesUnder,
EntityIdList& duplicatedEntityIds)
{
if (entities.empty())
{
return;
}
AZStd::unordered_map<EntityAlias, EntityAlias> oldAliasToNewAliasMap;
AZStd::unordered_map<EntityAlias, QString> 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<rapidjson::StringBuffer> 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<Instance*>& instances, PrefabDom& domToAddDuplicatedInstancesUnder,
EntityIdList& duplicatedEntityIds, AZStd::unordered_map<InstanceAlias, Instance*>& newInstanceAliasToOldInstanceMap)
{
if (instances.empty())
{
return;
}
AZStd::unordered_map<InstanceAlias, InstanceAlias> oldInstanceAliasToNewInstanceAliasMap;
AZStd::unordered_map<InstanceAlias, QString> 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<rapidjson::StringBuffer> 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());
@@ -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<AZ::Entity*>& 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<Instance*>& instances, PrefabDom& domToAddDuplicatedInstancesUnder,
EntityIdList& duplicatedEntityIds, AZStd::unordered_map<InstanceAlias, Instance*>& newInstanceAliasToOldInstanceMap);
/**
* Applies the correct transform changes to the container entity based on the parent and child entities provided, and returns an appropriate patch.