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.
o3de/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceCompilation.cpp

831 lines
40 KiB
C++

/*
* 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 <AzCore/Component/Entity.h>
#include <AzCore/Component/Component.h>
#include <AzCore/Slice/SliceComponent.h>
#include <AzCore/Asset/AssetCommon.h>
#include <AzCore/Component/ComponentExport.h>
#include <AzCore/Serialization/EditContext.h>
#include <AzFramework/Components/TransformComponent.h>
#include <AzFramework/InGameUI/UiFrameworkBus.h>
#include <AzToolsFramework/Slice/SliceCompilation.h>
#include <AzToolsFramework/ToolsComponents/EditorComponentBase.h>
#include <AzToolsFramework/ToolsComponents/EditorOnlyEntityComponentBus.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <AzToolsFramework/UI/PropertyEditor/PropertyEditorAPI.h>
#include <AzToolsFramework/ToolsComponents/GenericComponentWrapper.h>
#include <AzToolsFramework/Entity/EditorEntityHelpers.h>
namespace AzToolsFramework
{
namespace Internal
{
using ShouldExportResult = AZ::Outcome<bool, AZStd::string>; // Outcome describing whether a component should be exported based on
// user EditContext attributes. Error string provided in failure case.
using ExportedComponentResult = AZ::Outcome<AZ::ExportedComponent, AZStd::string>; // Outcome describing final resolved component for export.
// Error string provided in error case.
/**
* Checks EditContext attributes to determine if the input component should be exported based on the current platform tags.
*/
ShouldExportResult ShouldExportComponent(AZ::Component* component, const AZ::PlatformTagSet& platformTags, AZ::SerializeContext& serializeContext)
{
const AZ::SerializeContext::ClassData* classData = serializeContext.FindClassData(component->RTTI_GetType());
if (!classData || !classData->m_editData)
{
return AZ::Success(true);
}
const AZ::Edit::ElementData* editorDataElement = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData);
if (!editorDataElement)
{
return AZ::Success(true);
}
AZ::Edit::Attribute* allTagsAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::ExportIfAllPlatformTags);
AZ::Edit::Attribute* anyTagsAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::ExportIfAnyPlatformTags);
AZStd::vector<AZ::Crc32> attributeTags;
// If the component has declared the 'ExportIfAllPlatforms' attribute, skip export if any of the flags are not present.
if (allTagsAttribute)
{
attributeTags.clear();
PropertyAttributeReader reader(component, allTagsAttribute);
if (!reader.Read<decltype(attributeTags)>(attributeTags))
{
return AZ::Failure(AZStd::string("'ExportIfAllPlatforms' attribute is not bound to the correct return type. Expects AZStd::vector<AZ::Crc32>."));
}
for (AZ::Crc32 tag : attributeTags)
{
if (platformTags.find(tag) == platformTags.end())
{
// Export platform tags does not contain all tags specified in 'ExportIfAllPlatforms' attribute.
return AZ::Success(false);
}
}
}
// If the component has declared the 'ExportIfAnyPlatforms' attribute, skip export if none of the flags are present.
if (anyTagsAttribute)
{
attributeTags.clear();
PropertyAttributeReader reader(component, anyTagsAttribute);
if (!reader.Read<decltype(attributeTags)>(attributeTags))
{
return AZ::Failure(AZStd::string("'ExportIfAnyPlatforms' attribute is not bound to the correct return type. Expects AZStd::vector<AZ::Crc32>."));
}
bool anyFlagSet = false;
for (AZ::Crc32 tag : attributeTags)
{
if (platformTags.find(tag) != platformTags.end())
{
anyFlagSet = true;
break;
}
}
if (!anyFlagSet)
{
// None of the flags in 'ExportIfAnyPlatforms' was present in the export platform tags.
return AZ::Success(false);
}
}
return AZ::Success(true);
}
/**
* Recursively resolves to the component that should be exported to the runtime slice per the current platform flags
* and any custom user export callbacks.
* This is recursive to allow deep exports, such an editor component exporting a runtime component, which in turn
* exports a custom version of itself depending on platform.
*/
ExportedComponentResult ResolveExportedComponent(AZ::ExportedComponent component,
const AZ::PlatformTagSet& platformTags,
AZ::SerializeContext& serializeContext)
{
AZ::Component* inputComponent = component.m_component;
if (!inputComponent)
{
return AZ::Success(AZ::ExportedComponent(component));
}
// Don't export the component if it has unmet platform tag requirements.
ShouldExportResult shouldExportResult = ShouldExportComponent(inputComponent, platformTags, serializeContext);
if (!shouldExportResult)
{
return AZ::Failure(shouldExportResult.TakeError());
}
if (!shouldExportResult.GetValue())
{
// If the platform tag requirements aren't met, return a null component that's been flagged as exported,
// so that we know not to try and process it any further.
return AZ::Success(AZ::ExportedComponent());
}
// Determine if the component has a custom export callback, and invoke it if so.
const AZ::SerializeContext::ClassData* classData = serializeContext.FindClassData(inputComponent->RTTI_GetType());
if (classData && classData->m_editData)
{
const AZ::Edit::ElementData* editorDataElement = classData->m_editData->FindElementData(AZ::Edit::ClassElements::EditorData);
if (editorDataElement)
{
AZ::Edit::Attribute* exportCallbackAttribute = editorDataElement->FindAttribute(AZ::Edit::Attributes::RuntimeExportCallback);
if (exportCallbackAttribute)
{
PropertyAttributeReader reader(inputComponent, exportCallbackAttribute);
AZ::ExportedComponent exportedComponent;
if (reader.Read<AZ::ExportedComponent>(exportedComponent, inputComponent, platformTags))
{
// If the callback handled the export and provided a different component instance, continue to resolve recursively.
if (exportedComponent.m_componentExportHandled && (exportedComponent.m_component != inputComponent))
{
return ResolveExportedComponent(exportedComponent, platformTags, serializeContext);
}
else
{
// It provided the *same* component back (or didn't handle the export at all), so we're done.
return AZ::Success(exportedComponent);
}
}
else
{
return AZ::Failure(AZStd::string("Bound 'CustomExportCallback' does not have the required return type/signature."));
}
}
}
}
// If there's no custom export callback, just return what we were given.
return AZ::Success(component);
}
//! Iterates through the list of entities for each handler provided and returns the first handler
//! that can handle at least one of the entities in the list.
//!
//! We currently don't have support the concept of using multiple handlers for a given list of
//! entities. So once a handler is found, we assume that it can handle all of the entities in
//! the list.
//!
//! This may not always be true if the list contains world entities and UI element entities, for
//! example - so this may need udpating eventually.
EditorOnlyEntityHandler* FindHandlerForEntities(const AZ::SliceComponent::EntityList& entities, const EditorOnlyEntityHandlers& editorOnlyEntityHandlers)
{
EditorOnlyEntityHandler* editorOnlyEntityHandler = nullptr;
for (auto handlerCandidate : editorOnlyEntityHandlers)
{
const bool handlerInvalid = handlerCandidate == nullptr;
if (handlerInvalid)
{
continue;
}
// See if this handler can handle at least one of the entities.
for (AZ::Entity* entity : entities)
{
if (handlerCandidate->IsEntityUniquelyForThisHandler(entity))
{
editorOnlyEntityHandler = handlerCandidate;
break;
}
}
const bool editorOnlyHandlerValid = editorOnlyEntityHandler != nullptr;
if (editorOnlyHandlerValid)
{
break;
}
}
return editorOnlyEntityHandler;
}
/**
* Identify and remove any entities marked as editor-only.
* If any are discovered, adjust descendants' transforms to retain spatial relationships.
* Note we cannot use EBuses for this purpose, since we're crunching data, and can't assume any entities are active.
*/
EditorOnlyEntityHandler::Result
AdjustForEditorOnlyEntities(AZ::SliceComponent* slice, const AZStd::unordered_set<AZ::EntityId>& editorOnlyEntities, AZ::SerializeContext& serializeContext, EditorOnlyEntityHandler* customHandler)
{
AzToolsFramework::EntityList entities;
slice->GetEntities(entities);
// Invoke custom handler if provided, so callers can process the slice to account for editor-only entities
// that are about to be removed.
if (customHandler)
{
const EditorOnlyEntityHandler::Result handlerResult = customHandler->HandleEditorOnlyEntities(entities, editorOnlyEntities, serializeContext);
if (!handlerResult)
{
return handlerResult;
}
}
// Remove editor-only entities from the slice's final entity list.
for (auto entityIter = entities.begin(); entityIter != entities.end(); )
{
AZ::Entity* entity = (*entityIter);
if (editorOnlyEntities.find(entity->GetId()) != editorOnlyEntities.end())
{
entityIter = entities.erase(entityIter);
slice->RemoveEntity(entity);
}
else
{
++entityIter;
}
}
return AZ::Success();
}
AZ::TransformInterface* FindTransformInterfaceComponent(AZ::Entity& entity)
{
for (AZ::Component* component : entity.GetComponents())
{
if (auto transformInterface = azrtti_cast<AZ::TransformInterface*>(component))
{
return transformInterface;
}
}
return nullptr;
}
} // namespace Internal
/**
* Compiles the provided source slice into a runtime slice.
* Components are validated and exported considering platform tags and EditContext-driven user validation and export customizations.
*/
SliceCompilationResult CompileEditorSlice(const AZ::Data::Asset<AZ::SliceAsset>& sourceSliceAsset, const AZ::PlatformTagSet& platformTags, AZ::SerializeContext& serializeContext, const EditorOnlyEntityHandlers& editorOnlyEntityHandlers)
{
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
if (!sourceSliceAsset)
{
return AZ::Failure(AZStd::string("Source slice is invalid."));
}
AZ::SliceComponent::EntityList sourceEntities;
sourceSliceAsset.Get()->GetComponent()->GetEntities(sourceEntities);
// Create a new target slice asset to which we'll export entities & components.
AZ::Entity* exportSliceEntity = aznew AZ::Entity(sourceSliceAsset.Get()->GetEntity()->GetId());
AZ::SliceComponent* exportSliceData = exportSliceEntity->CreateComponent<AZ::SliceComponent>();
AZ::Data::Asset<AZ::SliceAsset> exportSliceAsset;
exportSliceAsset.Create(AZ::Data::AssetId(AZ::Uuid::CreateRandom()));
exportSliceAsset.Get()->SetData(exportSliceEntity, exportSliceData);
// For export, components can assume they're initialized, but not activated.
for (AZ::Entity* sourceEntity : sourceEntities)
{
if (sourceEntity->GetState() == AZ::Entity::State::Constructed)
{
sourceEntity->Init();
}
}
// Prepare source entity container for validation callbacks.
AZ::ImmutableEntityVector immutableSourceEntities;
immutableSourceEntities.reserve(sourceEntities.size());
for (AZ::Entity* entity : sourceEntities)
{
immutableSourceEntities.push_back(entity);
}
// Pick the correct handler to use
EditorOnlyEntityHandler* editorOnlyEntityHandler = Internal::FindHandlerForEntities(sourceEntities, editorOnlyEntityHandlers);
const bool editorOnlyHandlerValid = editorOnlyEntityHandler != nullptr;
EntityIdSet editorOnlyEntities;
// Prepare entities for export. This involves invoking BuildGameEntity on source
// entity's components, targeting a separate entity for export.
for (AZ::Entity* sourceEntity : sourceEntities)
{
AZ::Entity* exportEntity = aznew AZ::Entity(sourceEntity->GetId(), sourceEntity->GetName().c_str());
exportEntity->SetRuntimeActiveByDefault(sourceEntity->IsRuntimeActiveByDefault());
bool isEditorOnly = false;
EditorOnlyEntityComponentRequestBus::EventResult(isEditorOnly, sourceEntity->GetId(), &EditorOnlyEntityComponentRequests::IsEditorOnlyEntity);
if (isEditorOnly && editorOnlyHandlerValid)
{
editorOnlyEntityHandler->AddEditorOnlyEntity(sourceEntity, editorOnlyEntities);
}
const AZ::Entity::ComponentArrayType& editorComponents = sourceEntity->GetComponents();
for (AZ::Component* component : editorComponents)
{
auto* asEditorComponent =
azrtti_cast<Components::EditorComponentBase*>(component);
// Call validation callback on source component.
// (Later, we'll call the validation callback on the final exported component as well.)
AZ::ComponentValidationResult result = component->ValidateComponentRequirements(immutableSourceEntities, platformTags);
if (!result.IsSuccess())
{
// Try to cast to GenericComponentWrapper, and if we can, get the internal template.
const char* componentName = component->RTTI_GetTypeName();
Components::GenericComponentWrapper* wrapper = azrtti_cast<Components::GenericComponentWrapper*>(asEditorComponent);
if (wrapper && wrapper->GetTemplate())
{
componentName = wrapper->GetTemplate()->RTTI_GetTypeName();
}
return AZ::Failure(AZStd::string::format("[Entity] \"%s\" [Entity Id] 0x%llu, [Editor Component] \"%s\" could not pass validation for [Slice] \"%s\" [Error] %s",
sourceEntity->GetName().c_str(),
static_cast<AZ::u64>(sourceEntity->GetId()),
componentName,
sourceSliceAsset.GetHint().c_str(),
result.GetError().c_str()));
}
// Whether or not this is an editor component, the source component might have a custom user export callback,
// so try to call it.
const Internal::ExportedComponentResult exportResult = Internal::ResolveExportedComponent(
AZ::ExportedComponent(component, false, false),
platformTags,
serializeContext);
if (!exportResult)
{
return AZ::Failure(AZStd::string::format("Source component \"%s\" could not be exported for Entity \"%s\" [0x%llu] due to export attributes: %s.",
component->RTTI_GetTypeName(),
exportEntity->GetName().c_str(),
static_cast<AZ::u64>(exportEntity->GetId()),
exportResult.GetError().c_str()));
}
AZ::ExportedComponent exportedComponent = exportResult.GetValue();
// If ResolveExportedComponent didn't handle the component export, then we'll do the following:
// - For editor components, fall back on the legacy BuildGameEntity() path for handling component exports.
// - For runtime components, provide a default behavior of "clone / add" to export the component.
if (!exportedComponent.m_componentExportHandled)
{
// Editor components: Try to use BuildGameEntity()
if (asEditorComponent) // BEGIN BuildGameEntity compatibility path for editor components not using the newer RuntimeExportCallback functionality.
{
const size_t oldComponentCount = exportEntity->GetComponents().size();
asEditorComponent->BuildGameEntity(exportEntity);
AZ::ComponentId newID = asEditorComponent->GetId();
for (auto i = oldComponentCount ; i < exportEntity->GetComponents().size(); ++i)
{
AZ::Component* exportComponent = exportEntity->GetComponents()[i];
// Verify that the result of BuildGameEntity() wasn't an editor component.
auto* exportAsEditorComponent = azrtti_cast<Components::EditorComponentBase*>(exportComponent);
if (exportAsEditorComponent)
{
return AZ::Failure(AZStd::string::format("Entity \"%s\" [0x%llu], component \"%s\" exported an editor component from BuildGameEntity() for runtime use.",
sourceEntity->GetName().c_str(),
static_cast<AZ::u64>(sourceEntity->GetId()),
asEditorComponent->RTTI_GetType().ToString<AZStd::string>().c_str()));
}
if (asEditorComponent->GetId() == AZ::InvalidComponentId)
{
return AZ::Failure(AZStd::string::format("Entity \"%s\" [0x%llu], component \"%s\" doesn't have a valid component Id.",
sourceEntity->GetName().c_str(),
static_cast<AZ::u64>(sourceEntity->GetId()),
asEditorComponent->RTTI_GetType().ToString<AZStd::string>().c_str()));
}
exportComponent->SetId(newID++);
// The first time round set the new componet the same as the ditors one. This will change in a seperate ticket
// when 8 bit runtime Ids are implemented.
// Make sure the newID isn't already on the source Entity. If it is increment the ID and try again.
while (sourceEntity->FindComponent(newID))
{
++newID;
}
}
// Since this is an editor component, we very specifically do *not* want to clone and add it as a runtime
// component by default, so regardless of whether or not the BuildGameEntity() call did anything,
// null out the editor component and mark it handled.
exportedComponent = AZ::ExportedComponent();
} // END BuildGameEntity compatibility path for editor components not using the newer RuntimeExportCallback functionality.
else
{
// Nothing else has handled the component export, so fall back on the default behavior
// for runtime components: clone and add the runtime component that already exists.
exportedComponent = AZ::ExportedComponent(component, false, true);
}
}
// At this point, either ResolveExportedComponent or the default logic above should have set the component export
// as being handled. If not, there is likely a new code path that requires a default export behavior.
AZ_Assert(exportedComponent.m_componentExportHandled, "Component \"%s\" had no export handlers and could not be added to Entity \"%s\" [0x%llu].",
component->RTTI_GetTypeName(),
exportEntity->GetName().c_str(),
static_cast<AZ::u64>(exportEntity->GetId()));
// If we have an exported component, we add it to the exported entity.
// If we don't (m_component == nullptr), this component chose not to be exported, so we skip it.
if (exportedComponent.m_componentExportHandled && exportedComponent.m_component)
{
AZ::Component* runtimeComponent = exportedComponent.m_component;
// Verify that we aren't trying to export an editor component.
auto* exportAsEditorComponent = azrtti_cast<Components::EditorComponentBase*>(runtimeComponent);
if (exportAsEditorComponent)
{
return AZ::Failure(AZStd::string::format("Entity \"%s\" [0x%llu], component \"%s\" is trying to export an Editor component for runtime use.",
sourceEntity->GetName().c_str(),
static_cast<AZ::u64>(sourceEntity->GetId()),
asEditorComponent->RTTI_GetType().ToString<AZStd::string>().c_str()));
}
// If the final component is not owned by us, make our own copy.
if (!exportedComponent.m_deleteAfterExport)
{
runtimeComponent = serializeContext.CloneObject(runtimeComponent);
}
// Synchronize to source component Id, and add to the export entity.
runtimeComponent->SetId(component->GetId());
if (!exportEntity->AddComponent(runtimeComponent))
{
return AZ::Failure(AZStd::string::format("Component \"%s\" could not be added to Entity \"%s\" [0x%llu].",
runtimeComponent->RTTI_GetTypeName(),
exportEntity->GetName().c_str(),
static_cast<AZ::u64>(exportEntity->GetId())));
}
}
}
// Pre-sort prior to exporting so it isn't required at instantiation time.
const AZ::Entity::DependencySortOutcome sortOutcome = exportEntity->EvaluateDependenciesGetDetails();
// :CBR_TODO: verify AZ::Entity::DependencySortResult::HasIncompatibleServices and AZ::Entity::DependencySortResult::DescriptorNotRegistered are still covered here
if (!sortOutcome.IsSuccess())
{
return AZ::Failure(AZStd::string::format("Entity \"%s\" %s dependency evaluation failed. %s",
exportEntity->GetName().c_str(),
exportEntity->GetId().ToString().c_str(),
sortOutcome.GetError().m_message.c_str()));
}
exportSliceData->AddEntity(exportEntity);
}
{
AZ::SliceComponent::EntityList exportEntities;
exportSliceData->GetEntities(exportEntities);
if (exportEntities.size() != sourceEntities.size())
{
return AZ::Failure(AZStd::string("Entity export list size must match that of the import list."));
}
}
// Notify user callback, and then strip out any editor-only entities.
// This operation can generate a failure if a the callback failed validation.
if (!editorOnlyEntities.empty())
{
EditorOnlyEntityHandler::Result result = Internal::AdjustForEditorOnlyEntities(exportSliceData, editorOnlyEntities, serializeContext, editorOnlyEntityHandler);
if (!result)
{
return AZ::Failure(result.TakeError());
}
}
// Sort entities by transform hierarchy, so parents will activate before children
{
AZStd::vector<AZ::Entity*> sortedEntities;
exportSliceData->GetEntities(sortedEntities);
SortTransformParentsBeforeChildren(sortedEntities);
// Sort the entities in the slice by removing them, and putting them back in sorted order
exportSliceData->RemoveAllEntities(/*deleteEntities*/ false, /*removeEmptyInstances*/ false);
for (AZ::Entity* entity : sortedEntities)
{
exportSliceData->AddEntity(entity);
}
}
// Call validation callbacks on final runtime components.
AZ::SliceComponent::EntityList exportEntities;
exportSliceData->GetEntities(exportEntities);
AZ::ImmutableEntityVector immutableExportEntities;
immutableExportEntities.reserve(exportEntities.size());
for (AZ::Entity* entity : exportEntities)
{
immutableExportEntities.push_back(entity);
}
for (AZ::Entity* exportEntity : exportEntities)
{
const AZ::Entity::ComponentArrayType& gameComponents = exportEntity->GetComponents();
for (AZ::Component* component : gameComponents)
{
AZ::ComponentValidationResult result = component->ValidateComponentRequirements(immutableExportEntities, platformTags);
if (!result.IsSuccess())
{
// Try to cast to GenericComponentWrapper, and if we can, get the internal template.
const char* componentName = component->RTTI_GetTypeName();
return AZ::Failure(AZStd::string::format("[Entity] \"%s\" [Entity Id] 0x%llu, [Exported Component] \"%s\" could not pass validation for [Slice] \"%s\" [Error] %s",
exportEntity->GetName().c_str(),
static_cast<AZ::u64>(exportEntity->GetId()),
componentName,
sourceSliceAsset.GetHint().c_str(),
result.GetError().c_str()));
}
}
}
return AZ::Success(exportSliceAsset);
}
bool WorldEditorOnlyEntityHandler::IsEntityUniquelyForThisHandler(AZ::Entity* entity)
{
return Internal::FindTransformInterfaceComponent(*entity) != nullptr;
}
EditorOnlyEntityHandler::Result WorldEditorOnlyEntityHandler::HandleEditorOnlyEntities(const AzToolsFramework::EntityList& entities, const AzToolsFramework::EntityIdSet& editorOnlyEntityIds, AZ::SerializeContext& serializeContext)
{
FixTransformRelationships(entities, editorOnlyEntityIds);
return ValidateReferences(entities, editorOnlyEntityIds, serializeContext);
}
void WorldEditorOnlyEntityHandler::FixTransformRelationships(const AzToolsFramework::EntityList& entities, const AzToolsFramework::EntityIdSet& editorOnlyEntityIds)
{
AZStd::unordered_map<AZ::EntityId, AZStd::vector<AZ::Entity*>> parentToChildren;
// Build a map of entity Ids to their parent Ids, for faster lookup during processing.
for (AZ::Entity* entity : entities)
{
AZ::TransformInterface* transformComponent = AZ::EntityUtils::FindFirstDerivedComponent<AZ::TransformInterface>(entity);
if (transformComponent)
{
const AZ::EntityId parentId = transformComponent->GetParentId();
if (parentId.IsValid())
{
parentToChildren[parentId].push_back(entity);
}
}
}
// Identify any editor-only entities. If we encounter one, adjust transform relationships
// for all of its children to ensure relative transforms are maintained and respected at
// runtime.
// This works regardless of entity ordering in the slice because we add reassigned children to
// parentToChildren cache during the operation.
for (AZ::Entity* entity : entities)
{
if (editorOnlyEntityIds.end() == editorOnlyEntityIds.find(entity->GetId()))
{
continue; // This is not an editor-only entity.
}
AZ::TransformInterface* transformComponent = AZ::EntityUtils::FindFirstDerivedComponent<AZ::TransformInterface>(entity);
if (transformComponent)
{
const AZ::Transform& parentLocalTm = transformComponent->GetLocalTM();
// Identify all transform children and adjust them to be children of the removed entity's parent.
for (AZ::Entity* childEntity : parentToChildren[entity->GetId()])
{
AZ::TransformInterface* childTransformComponent = AZ::EntityUtils::FindFirstDerivedComponent<AZ::TransformInterface>(childEntity);
if (childTransformComponent && childTransformComponent->GetParentId() == entity->GetId())
{
const AZ::Transform localTm = childTransformComponent->GetLocalTM();
childTransformComponent->SetParent(transformComponent->GetParentId());
childTransformComponent->SetLocalTM(parentLocalTm * localTm);
parentToChildren[transformComponent->GetParentId()].push_back(childEntity);
}
}
}
}
}
EditorOnlyEntityHandler::Result EditorOnlyEntityHandler::ValidateReferences(const AzToolsFramework::EntityList& entities, const AzToolsFramework::EntityIdSet& editorOnlyEntityIds, AZ::SerializeContext& serializeContext)
{
EditorOnlyEntityHandler::Result result = AZ::Success();
// Inspect all runtime entities via the serialize context and identify any references to editor-only entity Ids.
for (AZ::Entity* runtimeEntity : entities)
{
if (editorOnlyEntityIds.end() != editorOnlyEntityIds.find(runtimeEntity->GetId()))
{
continue; // This is not a runtime entity, so no need to validate its references as it's going away.
}
AZ::EntityUtils::EnumerateEntityIds<AZ::Entity>(runtimeEntity,
[&editorOnlyEntityIds, &result, runtimeEntity](const AZ::EntityId& id, bool /*isEntityId*/, const AZ::SerializeContext::ClassElement* /*elementData*/)
{
if (editorOnlyEntityIds.end() != editorOnlyEntityIds.find(id))
{
result = AZ::Failure(AZStd::string::format("A runtime entity (%s) contains references to an entity marked as editor-only.", runtimeEntity->GetName().c_str()));
return false;
}
return true;
}
, &serializeContext);
if (!result)
{
break;
}
}
return result;
}
// perform breadth-first topological sort, placing parents before their children.
// tolerate ALL possible input errors (looping parents, invalid IDs, etc).
void SortTransformParentsBeforeChildren(AZStd::vector<AZ::Entity*>& entities)
{
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
// IDs of those present in 'entities'. Does not include parent ID if parent not found in 'entities'
AZStd::unordered_set<AZ::EntityId> existingEntityIds;
// map children by their parent ID (even if parent not found in 'entities')
AZStd::unordered_map<AZ::EntityId, AZStd::vector<AZ::Entity*>> parentIdToChildrenPtrs;
// store any entities with bad setups here, we'll put them last in the final sort
AZStd::vector<AZ::Entity*> badEntities;
// gather data about the entities...
for (AZ::Entity* entity : entities)
{
if (!entity)
{
badEntities.push_back(entity);
continue;
}
AZ::EntityId entityId = entity->GetId();
if (!entityId.IsValid())
{
AZ_Warning("Entity", false, "Hierarchy sort found entity '%s' with invalid ID", entity->GetName().c_str());
badEntities.push_back(entity);
continue;
}
bool entityIdIsUnique = existingEntityIds.insert(entityId).second;
if (!entityIdIsUnique)
{
AZ_Warning("Entity", false, "Hierarchy sort found multiple entities using same ID as entity '%s' %s",
entity->GetName().c_str(),
entityId.ToString().c_str());
badEntities.push_back(entity);
continue;
}
// search for any component that implements the TransformInterface.
// don't use EBus because we support sorting entities that haven't been initialized or activated.
// entities with no transform component will be treated like entities with no parent.
AZ::EntityId parentId;
if (AZ::TransformInterface* transformInterface = AZ::EntityUtils::FindFirstDerivedComponent<AZ::TransformInterface>(entity))
{
parentId = transformInterface->GetParentId();
// if entity is parented to itself, sort it as if it had no parent
if (parentId == entityId)
{
AZ_Warning("Entity", false, "Hierarchy sort found entity parented to itself '%s' %s",
entity->GetName().c_str(),
entityId.ToString().c_str());
parentId.SetInvalid();
}
}
parentIdToChildrenPtrs[parentId].push_back(entity);
}
// clear 'entities', we'll refill it in sorted order...
const size_t originalEntityCount = entities.size();
entities.clear();
// use 'candidateIds' to track the parent IDs we're going to process next.
// the first candidates should be the parents of the roots.
AZStd::vector<AZ::EntityId> candidateIds;
candidateIds.reserve(originalEntityCount + 1);
for (auto& parentChildrenPair : parentIdToChildrenPtrs)
{
const AZ::EntityId& parentId = parentChildrenPair.first;
// we found a root if parent ID doesn't correspond to any entity in the list
if (existingEntityIds.find(parentId) == existingEntityIds.end())
{
candidateIds.push_back(parentId);
}
}
// process candidates until everything is sorted:
// - add candidate's children to the final sorted order
// - add candidate's children to list of candidates, so we can process *their* children in a future loop
// - erase parent/children entry from parentToChildrenIds
// - continue until nothing is left in parentToChildrenIds
for (size_t candidateIndex = 0; !parentIdToChildrenPtrs.empty(); ++candidateIndex)
{
// if there are no more candidates, but there are still unsorted children, then we have an infinite loop.
// pick an arbitrary parent from the loop to be the next candidate.
if (candidateIndex == candidateIds.size())
{
const AZ::EntityId& parentFromLoopId = parentIdToChildrenPtrs.begin()->first;
#ifdef AZ_ENABLE_TRACING
// Find name to use in warning message
AZStd::string parentFromLoopName;
for (auto& parentIdChildrenPtrPair : parentIdToChildrenPtrs)
{
for (AZ::Entity* entity : parentIdChildrenPtrPair.second)
{
if (entity->GetId() == parentFromLoopId)
{
parentFromLoopName = entity->GetName();
break;
}
if (!parentFromLoopName.empty())
{
break;
}
}
}
AZ_Warning("Entity", false, "Hierarchy sort found parenting loop involving entity '%s' %s",
parentFromLoopName.c_str(),
parentFromLoopId.ToString().c_str());
#endif // AZ_ENABLE_TRACING
candidateIds.push_back(parentFromLoopId);
}
const AZ::EntityId& parentId = candidateIds[candidateIndex];
auto foundChildren = parentIdToChildrenPtrs.find(parentId);
if (foundChildren != parentIdToChildrenPtrs.end())
{
for (AZ::Entity* child : foundChildren->second)
{
entities.push_back(child);
candidateIds.push_back(child->GetId());
}
parentIdToChildrenPtrs.erase(foundChildren);
}
}
// put bad entities at the end of the sorted list
entities.insert(entities.end(), badEntities.begin(), badEntities.end());
AZ_Assert(entities.size() == originalEntityCount, "Wrong number of entities after sort! This algorithm is busted.");
}
bool UiEditorOnlyEntityHandler::IsEntityUniquelyForThisHandler(AZ::Entity* entity)
{
// Assume that an entity is a UI element if it has a UI element component.
bool uniqueForThisHandler = false;
UiFrameworkBus::BroadcastResult(uniqueForThisHandler, &UiFrameworkInterface::HasUiElementComponent, entity);
return uniqueForThisHandler;
}
void UiEditorOnlyEntityHandler::AddEditorOnlyEntity(AZ::Entity* editorOnlyEntity, EntityIdSet& editorOnlyEntities)
{
UiFrameworkBus::Broadcast(&UiFrameworkInterface::AddEditorOnlyEntity, editorOnlyEntity, editorOnlyEntities);
}
EditorOnlyEntityHandler::Result UiEditorOnlyEntityHandler::HandleEditorOnlyEntities(const AzToolsFramework::EntityList& exportSliceEntities, const AzToolsFramework::EntityIdSet& editorOnlyEntityIds, AZ::SerializeContext& serializeContext)
{
UiFrameworkBus::Broadcast(&UiFrameworkInterface::HandleEditorOnlyEntities, exportSliceEntities, editorOnlyEntityIds);
// Perform a final check to verify that all editor-only entities have been removed
auto result = ValidateReferences(exportSliceEntities, editorOnlyEntityIds, serializeContext);
return result;
}
} // AzToolsFramework