You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
373 lines
17 KiB
C++
373 lines
17 KiB
C++
/*
|
|
* Copyright (c) Contributors to the Open 3D Engine Project.
|
|
* For complete copyright and license terms please see the LICENSE at the root of this distribution.
|
|
*
|
|
* SPDX-License-Identifier: Apache-2.0 OR MIT
|
|
*
|
|
*/
|
|
#include "EditorCommon.h"
|
|
|
|
#include <AzCore/Serialization/Utils.h>
|
|
#include <AzCore/Component/ComponentApplicationBus.h>
|
|
#include <AzCore/Asset/AssetManager.h>
|
|
#include <AzCore/std/parallel/thread.h>
|
|
|
|
#include <QMessageBox>
|
|
#include <QApplication>
|
|
|
|
namespace SerializeHelpers
|
|
{
|
|
bool s_initializedReflection = false;
|
|
|
|
//! Simple helper class for serializing a vector of entities, their child entities
|
|
//! and their slice instance information. This is only serialized for the undo system
|
|
//! or the clipboard so it does not require version conversion.
|
|
//! m_entities is the set of entities that were chosen to be serialized (e.g. by a copy
|
|
//! command), m_childEntities are all the descendants of the entities in m_entities.
|
|
class SerializedElementContainer
|
|
{
|
|
public:
|
|
virtual ~SerializedElementContainer() { }
|
|
AZ_CLASS_ALLOCATOR(SerializedElementContainer, AZ::SystemAllocator, 0);
|
|
AZ_RTTI(SerializedElementContainer, "{4A12708F-7EC5-4F56-827A-6E67C3C49B3D}");
|
|
AZStd::vector<AZ::Entity*> m_entities;
|
|
AZStd::vector<AZ::Entity*> m_childEntities;
|
|
EntityRestoreVec m_entityRestoreInfos;
|
|
EntityRestoreVec m_childEntityRestoreInfos;
|
|
};
|
|
|
|
|
|
namespace Internal
|
|
{
|
|
void DetachEntitiesIfFullSliceInstanceNotBeingCopied(SerializedElementContainer& entitiesToSerialize)
|
|
{
|
|
// We simplify this a bit in the same was as SandboxIntegrationManager::CloneSelection and instead say that,
|
|
// unless every entity in the slice instance is being copied we do not preserve the connection to the slice.
|
|
|
|
// make a set of all the entities in entitiesToSerialize
|
|
AZStd::unordered_set<AZ::EntityId> allEntitiesBeingCopied;
|
|
for (auto entity : entitiesToSerialize.m_entities)
|
|
{
|
|
allEntitiesBeingCopied.insert(entity->GetId());
|
|
}
|
|
for (auto entity : entitiesToSerialize.m_childEntities)
|
|
{
|
|
allEntitiesBeingCopied.insert(entity->GetId());
|
|
}
|
|
|
|
// Create a local function to avoid duplicating code because we have two sets of lists to process
|
|
auto CheckEntities = [allEntitiesBeingCopied](AZStd::vector<AZ::Entity*>& entities, EntityRestoreVec& entityRestoreInfos)
|
|
{
|
|
for (int i = 0; i < entities.size(); ++i)
|
|
{
|
|
AZ::Entity* entity = entities[i];
|
|
|
|
AZ::SliceComponent::SliceInstanceAddress sliceAddress;
|
|
AzFramework::SliceEntityRequestBus::EventResult(sliceAddress, entity->GetId(),
|
|
&AzFramework::SliceEntityRequestBus::Events::GetOwningSlice);
|
|
|
|
if (sliceAddress.IsValid())
|
|
{
|
|
const AZ::SliceComponent::EntityList& entitiesInSlice = sliceAddress.GetInstance()->GetInstantiated()->m_entities;
|
|
for (AZ::Entity* entityInSlice : entitiesInSlice)
|
|
{
|
|
if (allEntitiesBeingCopied.end() == allEntitiesBeingCopied.find(entityInSlice->GetId()))
|
|
{
|
|
// at least one of the entities in the slice instance is not in the set being copied so
|
|
// remove this entities connection to the slice
|
|
entityRestoreInfos[i].m_assetId.SetInvalid();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
CheckEntities(entitiesToSerialize.m_entities, entitiesToSerialize.m_entityRestoreInfos);
|
|
CheckEntities(entitiesToSerialize.m_childEntities, entitiesToSerialize.m_childEntityRestoreInfos);
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void InitializeReflection()
|
|
{
|
|
// Reflect the SerializedElementContainer on first use.
|
|
if (!s_initializedReflection)
|
|
{
|
|
AZ::SerializeContext* context = nullptr;
|
|
EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext);
|
|
AZ_Assert(context, "No serialize context");
|
|
|
|
context->Class<SerializedElementContainer>()
|
|
->Version(1)
|
|
->Field("Entities", &SerializedElementContainer::m_entities)
|
|
->Field("ChildEntities", &SerializedElementContainer::m_childEntities)
|
|
->Field("RestoreInfos", &SerializedElementContainer::m_entityRestoreInfos)
|
|
->Field("ChildRestoreInfos", &SerializedElementContainer::m_childEntityRestoreInfos);
|
|
|
|
s_initializedReflection = true;
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void RestoreSerializedElements(
|
|
AZ::EntityId canvasEntityId,
|
|
AZ::Entity* parent,
|
|
AZ::Entity* insertBefore,
|
|
UiEditorEntityContext* entityContext,
|
|
const AZStd::string& xml,
|
|
bool isCopyOperation,
|
|
LyShine::EntityArray* cumulativeListOfCreatedEntities)
|
|
{
|
|
LyShine::EntityArray listOfNewlyCreatedTopLevelElements;
|
|
LyShine::EntityArray listOfAllCreatedEntities;
|
|
EntityRestoreVec entityRestoreInfos;
|
|
|
|
LoadElementsFromXmlString(
|
|
canvasEntityId,
|
|
xml.c_str(),
|
|
isCopyOperation,
|
|
parent,
|
|
insertBefore,
|
|
listOfNewlyCreatedTopLevelElements,
|
|
listOfAllCreatedEntities,
|
|
entityRestoreInfos);
|
|
|
|
if (listOfNewlyCreatedTopLevelElements.empty())
|
|
{
|
|
// This happens when the serialization version numbers DON'T match.
|
|
QMessageBox(QMessageBox::Critical,
|
|
"Error",
|
|
QString("Failed to restore elements. The clipboard serialization format is incompatible."),
|
|
QMessageBox::Ok, QApplication::activeWindow()).exec();
|
|
|
|
// Nothing more to do.
|
|
return;
|
|
}
|
|
|
|
// This is for error handling only. In the case of an error RestoreSliceEntity will delete the
|
|
// entity. We need to know when this has happened. So we record all the entityIds here and check
|
|
// them afterwards.
|
|
AzToolsFramework::EntityIdList idsOfNewlyCreatedTopLevelElements;
|
|
for (auto entity : listOfNewlyCreatedTopLevelElements)
|
|
{
|
|
idsOfNewlyCreatedTopLevelElements.push_back(entity->GetId());
|
|
}
|
|
|
|
// Now we need to restore the slice info for all the created elements
|
|
// In the case of a copy operation we need to generate new sliceInstanceIds. We use a map so that
|
|
// all entities copied from the same slice instance will end up in the same new slice instance.
|
|
AZStd::unordered_map<AZ::SliceComponent::SliceInstanceId, AZ::SliceComponent::SliceInstanceId> sliceInstanceMap;
|
|
for (int i=0; i < listOfAllCreatedEntities.size(); ++i)
|
|
{
|
|
AZ::Entity* entity = listOfAllCreatedEntities[i];
|
|
|
|
AZ::SliceComponent::EntityRestoreInfo& sliceRestoreInfo = entityRestoreInfos[i];
|
|
|
|
if (sliceRestoreInfo)
|
|
{
|
|
if (isCopyOperation)
|
|
{
|
|
// if a copy we can't use the instanceId of the instance that was copied from so generate
|
|
// a new one - but only want one new id per original slice instance - so we use a map to
|
|
// keep track of which instance Ids we have created new Ids for.
|
|
auto iter = sliceInstanceMap.find(sliceRestoreInfo.m_instanceId);
|
|
if (iter == sliceInstanceMap.end())
|
|
{
|
|
sliceInstanceMap[sliceRestoreInfo.m_instanceId] = AZ::SliceComponent::SliceInstanceId::CreateRandom();
|
|
}
|
|
|
|
sliceRestoreInfo.m_instanceId = sliceInstanceMap[sliceRestoreInfo.m_instanceId];
|
|
}
|
|
|
|
EBUS_EVENT_ID(entityContext->GetContextId(), UiEditorEntityContextRequestBus, RestoreSliceEntity, entity, sliceRestoreInfo);
|
|
}
|
|
else
|
|
{
|
|
entityContext->AddUiEntity(entity);
|
|
}
|
|
}
|
|
|
|
// If we are restoring slice entities and any of the entities are the first to be using that slice
|
|
// then we have to wait for that slice to be reloaded. We need to wait because we can't create hierarchy
|
|
// items for entities before they are recreated. We have tried trying to solve this by deferring the creation
|
|
// of the hierarchy items on a queue but that gets really complicated because this function is called
|
|
// in several situations. It also seems problematic to return control to the user - they could add or
|
|
// delete more items while we are waiting for the assets to load.
|
|
if (AZ::Data::AssetManager::IsReady())
|
|
{
|
|
bool areRequestsPending = false;
|
|
EBUS_EVENT_ID_RESULT(areRequestsPending, entityContext->GetContextId(), UiEditorEntityContextRequestBus, HasPendingRequests);
|
|
while (areRequestsPending)
|
|
{
|
|
AZ::Data::AssetManager::Instance().DispatchEvents();
|
|
AZStd::this_thread::sleep_for(AZStd::chrono::milliseconds(50));
|
|
EBUS_EVENT_ID_RESULT(areRequestsPending, entityContext->GetContextId(), UiEditorEntityContextRequestBus, HasPendingRequests);
|
|
}
|
|
}
|
|
|
|
// Because RestoreSliceEntity can delete the entity we have some recovery code here that will
|
|
// create a new list of top level entities excluding any that have been removed.
|
|
// An error should already have been reported in this case so we don't report it again.
|
|
LyShine::EntityArray validatedListOfNewlyCreatedTopLevelElements;
|
|
for (auto entityId : idsOfNewlyCreatedTopLevelElements)
|
|
{
|
|
AZ::Entity* entity = nullptr;
|
|
EBUS_EVENT_RESULT(entity, AZ::ComponentApplicationBus, FindEntity, entityId);
|
|
|
|
// Only add it to the validated list if the entity still exists
|
|
if (entity)
|
|
{
|
|
validatedListOfNewlyCreatedTopLevelElements.push_back(entity);
|
|
}
|
|
}
|
|
|
|
// Fixup the created entities, we do this before adding the top level element to the parent so that
|
|
// MakeUniqueChileName works correctly
|
|
EBUS_EVENT_ID(canvasEntityId,
|
|
UiCanvasBus,
|
|
FixupCreatedEntities,
|
|
validatedListOfNewlyCreatedTopLevelElements,
|
|
isCopyOperation,
|
|
parent);
|
|
|
|
// Now add the top-level created elements as children of the parent
|
|
for (auto entity : validatedListOfNewlyCreatedTopLevelElements)
|
|
{
|
|
// add this new entity as a child of the parent (insertionPoint or root)
|
|
EBUS_EVENT_ID(canvasEntityId,
|
|
UiCanvasBus,
|
|
AddElement,
|
|
entity,
|
|
parent,
|
|
insertBefore);
|
|
}
|
|
|
|
// if a list of entities was passed then add all the entities that we added
|
|
// to the list
|
|
if (cumulativeListOfCreatedEntities)
|
|
{
|
|
cumulativeListOfCreatedEntities->insert(
|
|
cumulativeListOfCreatedEntities->end(),
|
|
validatedListOfNewlyCreatedTopLevelElements.begin(),
|
|
validatedListOfNewlyCreatedTopLevelElements.end());
|
|
}
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
AZStd::string SaveElementsToXmlString(const LyShine::EntityArray& elements, AZ::SliceComponent* rootSlice, bool isCopyOperation, AZStd::unordered_set<AZ::Data::AssetId>& referencedSliceAssets)
|
|
{
|
|
InitializeReflection();
|
|
|
|
// The easiest way to write multiple elements to a stream is to create class that contains them
|
|
// that has an allocator. SerializedElementContainer exists for this purpose.
|
|
// It saves/loads two lists. One is a list of top-level elements, the second is a list of all of
|
|
// the children of those elements.
|
|
SerializedElementContainer entitiesToSerialize;
|
|
for (auto element : elements)
|
|
{
|
|
entitiesToSerialize.m_entities.push_back(element);
|
|
|
|
// add the slice restore info for this top level element
|
|
AZ::SliceComponent::EntityRestoreInfo sliceRestoreInfo;
|
|
rootSlice->GetEntityRestoreInfo(element->GetId(), sliceRestoreInfo);
|
|
entitiesToSerialize.m_entityRestoreInfos.push_back(sliceRestoreInfo);
|
|
|
|
LyShine::EntityArray childElements;
|
|
EBUS_EVENT_ID(element->GetId(), UiElementBus, FindDescendantElements,
|
|
[]([[maybe_unused]] const AZ::Entity* entity) { return true; },
|
|
childElements);
|
|
|
|
for (auto child : childElements)
|
|
{
|
|
entitiesToSerialize.m_childEntities.push_back(child);
|
|
|
|
// add the slice restore info for this child element
|
|
rootSlice->GetEntityRestoreInfo(child->GetId(), sliceRestoreInfo);
|
|
entitiesToSerialize.m_childEntityRestoreInfos.push_back(sliceRestoreInfo);
|
|
}
|
|
}
|
|
|
|
// if this is a copy operation we could be copying some elements in a slice instance without copying the root element
|
|
// of the slice instance. This would cause issues. So we need to detect that situation and change the entity restore infos
|
|
// to remove the slice instance association.
|
|
if (isCopyOperation)
|
|
{
|
|
Internal::DetachEntitiesIfFullSliceInstanceNotBeingCopied(entitiesToSerialize);
|
|
}
|
|
|
|
// now record the referenced slice assets
|
|
for (auto& sliceRestoreInfo : entitiesToSerialize.m_entityRestoreInfos)
|
|
{
|
|
if (sliceRestoreInfo)
|
|
{
|
|
referencedSliceAssets.insert(sliceRestoreInfo.m_assetId);
|
|
}
|
|
}
|
|
|
|
for (auto& sliceRestoreInfo : entitiesToSerialize.m_childEntityRestoreInfos)
|
|
{
|
|
if (sliceRestoreInfo)
|
|
{
|
|
referencedSliceAssets.insert(sliceRestoreInfo.m_assetId);
|
|
}
|
|
}
|
|
|
|
// save the entitiesToSerialize structure to the buffer
|
|
AZStd::string charBuffer;
|
|
AZ::IO::ByteContainerStream<AZStd::string> charStream(&charBuffer);
|
|
[[maybe_unused]] bool success = AZ::Utils::SaveObjectToStream(charStream, AZ::ObjectStream::ST_XML, &entitiesToSerialize);
|
|
AZ_Assert(success, "Failed to serialize elements to XML");
|
|
|
|
return charBuffer;
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
void LoadElementsFromXmlString(
|
|
[[maybe_unused]] AZ::EntityId canvasEntityId,
|
|
const AZStd::string& string,
|
|
bool makeNewIDs,
|
|
[[maybe_unused]] AZ::Entity* insertionPoint,
|
|
[[maybe_unused]] AZ::Entity* insertBefore,
|
|
LyShine::EntityArray& listOfCreatedTopLevelElements,
|
|
LyShine::EntityArray& listOfAllCreatedElements,
|
|
EntityRestoreVec& entityRestoreInfos)
|
|
{
|
|
InitializeReflection();
|
|
|
|
AZ::IO::ByteContainerStream<const AZStd::string> charStream(&string);
|
|
SerializedElementContainer* unserializedEntities =
|
|
AZ::Utils::LoadObjectFromStream<SerializedElementContainer>(charStream);
|
|
|
|
// If we want new IDs then generate them and fixup all references within the list of entities
|
|
if (makeNewIDs)
|
|
{
|
|
AZ::SerializeContext* context = nullptr;
|
|
EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext);
|
|
AZ_Assert(context, "No serialization context found");
|
|
|
|
AZ::SliceComponent::EntityIdToEntityIdMap entityIdMap;
|
|
AZ::IdUtils::Remapper<AZ::EntityId>::GenerateNewIdsAndFixRefs(unserializedEntities, entityIdMap, context);
|
|
}
|
|
|
|
// copy unserializedEntities into the return output list of top-level entities
|
|
for (auto newEntity : unserializedEntities->m_entities)
|
|
{
|
|
listOfCreatedTopLevelElements.push_back(newEntity);
|
|
}
|
|
|
|
// we also return a list of all of the created entities (not just top level ones)
|
|
listOfAllCreatedElements.insert(listOfAllCreatedElements.end(),
|
|
unserializedEntities->m_entities.begin(), unserializedEntities->m_entities.end());
|
|
listOfAllCreatedElements.insert(listOfAllCreatedElements.end(),
|
|
unserializedEntities->m_childEntities.begin(), unserializedEntities->m_childEntities.end());
|
|
|
|
// return a list of the EntityRestoreInfos in the same order
|
|
entityRestoreInfos.insert(entityRestoreInfos.end(),
|
|
unserializedEntities->m_entityRestoreInfos.begin(), unserializedEntities->m_entityRestoreInfos.end());
|
|
entityRestoreInfos.insert(entityRestoreInfos.end(),
|
|
unserializedEntities->m_childEntityRestoreInfos.begin(), unserializedEntities->m_childEntityRestoreInfos.end());
|
|
}
|
|
|
|
} // namespace EntityHelpers
|