From d90a3d46a7ac393a12807fe66eacd4f9bab7c8f1 Mon Sep 17 00:00:00 2001 From: Mike Balfour <82224783+mbalfour-amzn@users.noreply.github.com> Date: Thu, 3 Jun 2021 15:59:45 -0500 Subject: [PATCH] Support for nested slice conversions (#1121) This set of changes enables conversions for singly-nested slices. Multiple nesting hierarchies are only partially supported at this point. Conversion is also significantly more deterministic, which makes it easier to convert single slices without needing to reconvert every slice or level that relies on it as well. Changes: - Added version of Instance::AddInstance() that takes in an alias to allow for deterministic aliases - Added a "SliceConverterEditorEntityContextComponent" that's used to specifically disable entity activation on creation. The disabling is done this way vs adding a new public API, because the disable shouldn't be required in any normal case outside of this tool. - Disabled more AWS gems for the SliceConverter, as they're unneeded and cause issues if they're around in the tool. - Added a small null check to the Camera Controller. - Added the actual support for slice instance conversion. This instantiates the entities, applies the data patches, turns them into a prefab instance, and generates a JSON patch out of the changes. --- .../AzCore/AzCore/Slice/SliceComponent.h | 7 +- .../Prefab/Instance/Instance.cpp | 13 +- .../Prefab/Instance/Instance.h | 1 + .../SerializeContextTools/Application.cpp | 19 +- .../SerializeContextTools/SliceConverter.cpp | 232 +++++++++++++++--- .../SerializeContextTools/SliceConverter.h | 31 ++- ...iceConverterEditorEntityContextComponent.h | 65 +++++ Code/Tools/SerializeContextTools/main.cpp | 3 +- .../serializecontexttools_files.cmake | 1 + .../Code/Source/CameraComponentController.cpp | 5 +- .../gem_autoload.serializecontexttools.setreg | 6 + 11 files changed, 331 insertions(+), 52 deletions(-) create mode 100644 Code/Tools/SerializeContextTools/SliceConverterEditorEntityContextComponent.h diff --git a/Code/Framework/AzCore/AzCore/Slice/SliceComponent.h b/Code/Framework/AzCore/AzCore/Slice/SliceComponent.h index 5ef8df0617..0c9bad6d4a 100644 --- a/Code/Framework/AzCore/AzCore/Slice/SliceComponent.h +++ b/Code/Framework/AzCore/AzCore/Slice/SliceComponent.h @@ -971,6 +971,10 @@ namespace AZ */ void RestoreCachedInstances(); + /// Returns data flags for use when instantiating an instance of this slice. + /// These data flags include those harvested from the entire slice ancestry. + const DataFlagsPerEntity& GetDataFlagsForInstances() const; + protected: ////////////////////////////////////////////////////////////////////////// @@ -1004,9 +1008,6 @@ namespace AZ DataFlagsPerEntity* GetCorrectBundleOfDataFlags(EntityId entityId); const DataFlagsPerEntity* GetCorrectBundleOfDataFlags(EntityId entityId) const; - /// Returns data flags for use when instantiating an instance of this slice. - /// These data flags include those harvested from the entire slice ancestry. - const DataFlagsPerEntity& GetDataFlagsForInstances() const; void BuildDataFlagsForInstances(); /** diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp index 8f483ec818..766a293b4a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -49,8 +50,7 @@ namespace AzToolsFramework m_alias = GenerateInstanceAlias(); m_containerEntity = containerEntity ? AZStd::move(containerEntity) : AZStd::make_unique(); - EntityAlias containerEntityAlias = GenerateEntityAlias(); - RegisterEntity(m_containerEntity->GetId(), containerEntityAlias); + RegisterEntity(m_containerEntity->GetId(), PrefabDomUtils::ContainerEntityName); } Instance::~Instance() @@ -311,8 +311,15 @@ namespace AzToolsFramework Instance& Instance::AddInstance(AZStd::unique_ptr instance) { InstanceAlias newInstanceAlias = GenerateInstanceAlias(); + return AddInstance(AZStd::move(instance), newInstanceAlias); + } + + Instance& Instance::AddInstance(AZStd::unique_ptr instance, InstanceAlias newInstanceAlias) + { AZ_Assert(instance.get(), "instance argument is nullptr"); - AZ_Assert(m_nestedInstances.find(newInstanceAlias) == m_nestedInstances.end(), "InstanceAlias' unique id collision, this should never happen."); + AZ_Assert( + m_nestedInstances.find(newInstanceAlias) == m_nestedInstances.end(), + "InstanceAlias' unique id collision, this should never happen."); instance->m_parent = this; instance->m_alias = newInstanceAlias; return *(m_nestedInstances[newInstanceAlias] = std::move(instance)); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h index 68bc395012..4a69ade6d2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Instance/Instance.h @@ -93,6 +93,7 @@ namespace AzToolsFramework void Reset(); Instance& AddInstance(AZStd::unique_ptr instance); + Instance& AddInstance(AZStd::unique_ptr instance, InstanceAlias instanceAlias); AZStd::unique_ptr DetachNestedInstance(const InstanceAlias& instanceAlias); /** diff --git a/Code/Tools/SerializeContextTools/Application.cpp b/Code/Tools/SerializeContextTools/Application.cpp index 81cc314deb..918afd731c 100644 --- a/Code/Tools/SerializeContextTools/Application.cpp +++ b/Code/Tools/SerializeContextTools/Application.cpp @@ -17,6 +17,7 @@ #include #include +#include namespace AZ { @@ -34,6 +35,9 @@ namespace AZ Application::Application(int argc, char** argv) : AzToolsFramework::ToolsApplication(&argc, &argv) { + // We need a specialized variant of EditorEntityContextCompnent for the SliceConverter, so we register the descriptor here. + RegisterComponentDescriptor(AzToolsFramework::SliceConverterEditorEntityContextComponent::CreateDescriptor()); + AZ::IO::FixedMaxPath projectPath = AZ::Utils::GetProjectPath(); if (projectPath.empty()) { @@ -110,10 +114,21 @@ namespace AZ AZ::ComponentTypeList Application::GetRequiredSystemComponents() const { - // Use all of the default system components, but also add in the ThumbnailerNullComponent so that components requiring - // a ThumbnailService can still be started up. + // By default, we use all of the standard system components. AZ::ComponentTypeList components = AzToolsFramework::ToolsApplication::GetRequiredSystemComponents(); + + // Also add in the ThumbnailerNullComponent so that components requiring a ThumbnailService can still be started up. components.emplace_back(azrtti_typeid()); + + // The Slice Converter requires a specialized variant of the EditorEntityContextComponent that exposes the ability + // to disable the behavior of activating entities on creation. During conversion, the creation flow will be triggered, + // but entity activation requires a significant amount of subsystem initialization that's unneeded for conversion. + // So, to get around this, we swap out EditorEntityContextComponent with SliceConverterEditorEntityContextComponent. + components.erase( + AZStd::remove( + components.begin(), components.end(), azrtti_typeid()), + components.end()); + components.emplace_back(azrtti_typeid()); return components; } } // namespace SerializeContextTools diff --git a/Code/Tools/SerializeContextTools/SliceConverter.cpp b/Code/Tools/SerializeContextTools/SliceConverter.cpp index d06534e303..56b3689d6b 100644 --- a/Code/Tools/SerializeContextTools/SliceConverter.cpp +++ b/Code/Tools/SerializeContextTools/SliceConverter.cpp @@ -30,13 +30,16 @@ #include #include #include +#include #include #include #include #include #include +#include #include + // SliceConverter reads in a slice file (saved in an ObjectStream format), instantiates it, creates a prefab out of the data, // and saves the prefab in a JSON format. This can be used for one-time migrations of slices or slice-based levels to prefabs. // @@ -99,12 +102,26 @@ namespace AZ bool result = true; rapidjson::StringBuffer scratchBuffer; + // For slice conversion, disable the EditorEntityContextComponent logic that activates entities on creation. + // This prevents a lot of error messages and crashes during conversion due to lack of full environment and subsystem setup. + AzToolsFramework::SliceConverterEditorEntityContextComponent::DisableOnContextEntityLogic(); + // Loop through the list of requested files and convert them. AZStd::vector fileList = Utilities::ReadFileListFromCommandLine(application, "files"); for (AZStd::string& filePath : fileList) { bool convertResult = ConvertSliceFile(convertSettings.m_serializeContext, filePath, isDryRun); result = result && convertResult; + + // Clear out all registered prefab templates between each top-level file that gets processed. + auto prefabSystemComponentInterface = AZ::Interface::Get(); + for (auto templateId : m_createdTemplateIds) + { + // We don't just want to call RemoveAllTemplates() because the root template should remain between file conversions. + prefabSystemComponentInterface->RemoveTemplate(templateId); + } + m_aliasIdMapper.clear(); + m_createdTemplateIds.clear(); } DisconnectFromAssetProcessor(); @@ -114,6 +131,13 @@ namespace AZ bool SliceConverter::ConvertSliceFile( AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun) { + /* To convert a slice file, we read the input file in via ObjectStream, then use the "class ready" callback to convert + * the data in memory to a Prefab. + * If the input file is a level file (.ly), we actually need to load the level slice file ("levelentities.editor_xml") from + * within the level file, which effectively is a zip file of the level slice file and a bunch of legacy level files that won't + * be converted, since the systems that would use them no longer exist. + */ + bool result = true; bool packOpened = false; @@ -144,7 +168,7 @@ namespace AZ AZ_STRING_ARG(fileExtension.Native())); } - auto callback = [&outputPath, isDryRun](void* classPtr, const Uuid& classId, SerializeContext* context) + auto callback = [this, &outputPath, isDryRun](void* classPtr, const Uuid& classId, SerializeContext* context) { if (classId != azrtti_typeid()) { @@ -178,6 +202,13 @@ namespace AZ bool SliceConverter::ConvertSliceToPrefab( AZ::SerializeContext* serializeContext, AZ::IO::PathView outputPath, bool isDryRun, AZ::Entity* rootEntity) { + /* Given a root slice entity, we convert it to a prefab by doing the following: + * - Locate the SliceComponent + * - Take all the entities directly located on the slice, and put them into a prefab + * - Fix up any top-level entities to have the prefab container entity as their parent + * - If there are any nested slice instances, convert the nested slices to prefabs, then convert the instances. + */ + auto prefabSystemComponentInterface = AZ::Interface::Get(); // Find the slice from the root entity. @@ -192,9 +223,14 @@ namespace AZ SliceComponent::EntityList sliceEntities = sliceComponent->GetNewEntities(); AZ_Printf("Convert-Slice", " Slice contains %zu entities.\n", sliceEntities.size()); - // Create the Prefab with the entities from the slice + // Create the Prefab with the entities from the slice. + // The entities are added in a separate step so that we can give them deterministic entity aliases that match their entity Ids AZStd::unique_ptr sourceInstance( - prefabSystemComponentInterface->CreatePrefab(sliceEntities, {}, outputPath)); + prefabSystemComponentInterface->CreatePrefab({}, {}, outputPath)); + for (auto& entity : sliceEntities) + { + sourceInstance->AddEntity(*entity, AZStd::string::format("Entity_%s", entity->GetId().ToString().c_str())); + } // Dispatch events here, because prefab creation might trigger asset loads in rare circumstances. AZ::Data::AssetManager::Instance().DispatchEvents(); @@ -204,12 +240,28 @@ namespace AZ AzToolsFramework::Prefab::EntityOptionalReference container = sourceInstance->GetContainerEntity(); FixPrefabEntities(container->get(), sliceEntities); + // Keep track of the template Id we created, we're going to remove it at the end of slice file conversion to make sure + // the data doesn't stick around between file conversions. auto templateId = sourceInstance->GetTemplateId(); if (templateId == AzToolsFramework::Prefab::InvalidTemplateId) { AZ_Printf("Convert-Slice", " Path error. Path could be invalid, or the prefab may not be loaded in this level.\n"); return false; } + m_createdTemplateIds.emplace(templateId); + + // Save off a mapping of the original slice entity IDs to the new prefab template entity aliases. + // When converting nested slices, this mapping will be needed to fix up the parent entity hierarchy correctly. + auto entityAliases = sourceInstance->GetEntityAliases(); + for (auto& alias : entityAliases) + { + auto id = sourceInstance->GetEntityId(alias); + auto result = m_aliasIdMapper.emplace(TemplateEntityIdPair(templateId, id), alias); + if (!result.second) + { + AZ_Printf("Convert-Slice", " Duplicate entity alias -> entity id entries found, conversion may not be successful.\n"); + } + } // Update the prefab template with the fixed-up data in our prefab instance. AzToolsFramework::Prefab::PrefabDom prefabDom; @@ -254,21 +306,26 @@ namespace AZ // via an EditorRequests EBus in CreatePrefab, but the subsystem that listens for it isn't present in this tool.) AzToolsFramework::EditorEntityContextRequestBus::Broadcast( &AzToolsFramework::EditorEntityContextRequestBus::Events::AddRequiredComponents, containerEntity); - containerEntity.AddComponent(aznew AzToolsFramework::Prefab::EditorPrefabComponent()); + if (containerEntity.FindComponent() == nullptr) + { + containerEntity.AddComponent(aznew AzToolsFramework::Prefab::EditorPrefabComponent()); + } + + // Make all the components on the container entity have deterministic component IDs, so that multiple runs of the tool + // on the same slice will produce the same prefab output. We're going to cheat a bit and just use the component type hash + // as the component ID. This would break if we had multiple components of the same type, but that currently doesn't + // happen for the container entity. + auto containerComponents = containerEntity.GetComponents(); + for (auto& component : containerComponents) + { + component->SetId(component->GetUnderlyingComponentType().GetHash()); + } // Reparent any root-level slice entities to the container entity. for (auto entity : sliceEntities) { - AzToolsFramework::Components::TransformComponent* transformComponent = - entity->FindComponent(); - if (transformComponent) - { - if (!transformComponent->GetParentId().IsValid()) - { - transformComponent->SetParent(containerEntity.GetId()); - transformComponent->UpdateCachedWorldTransform(); - } - } + constexpr bool onlySetIfInvalid = true; + SetParentEntity(*entity, containerEntity.GetId(), onlySetIfInvalid); } } @@ -276,9 +333,13 @@ namespace AZ SliceComponent* sliceComponent, AzToolsFramework::Prefab::Instance* sourceInstance, AZ::SerializeContext* serializeContext, bool isDryRun) { + /* Given a root slice, find all the nested slices and convert them. */ + + // Get the list of nested slices that this slice uses. const SliceComponent::SliceList& sliceList = sliceComponent->GetSlices(); auto prefabSystemComponentInterface = AZ::Interface::Get(); + // For each nested slice, convert it. for (auto& slice : sliceList) { // Get the nested slice asset @@ -312,7 +373,7 @@ namespace AZ return false; } - // Load the prefab template for the newly-created nested prefab. + // Find the prefab template we created for the newly-created nested prefab. // To get the template, we need to take our absolute slice path and turn it into a project-relative prefab path. AZ::IO::Path nestedPrefabPath = assetPath; nestedPrefabPath.ReplaceExtension("prefab"); @@ -346,11 +407,25 @@ namespace AZ } bool SliceConverter::ConvertSliceInstance( - [[maybe_unused]] AZ::SliceComponent::SliceInstance& instance, - [[maybe_unused]] AZ::Data::Asset& sliceAsset, + AZ::SliceComponent::SliceInstance& instance, + AZ::Data::Asset& sliceAsset, AzToolsFramework::Prefab::TemplateReference nestedTemplate, AzToolsFramework::Prefab::Instance* topLevelInstance) { + /* To convert a slice instance, it's important to understand the similarities and differences between slices and prefabs. + * Both slices and prefabs have the concept of instances of a nested slice/prefab, where each instance can have its own + * set of changed data (transforms, component values, etc). + * For slices, the changed data comes from applying a DataPatch to an instantiated set of entities from the nested slice. + * From prefabs, the changed data comes from Json patches that are applied to the instantiated set of entities from the + * nested prefab. The prefab instance entities also have different IDs than the slice instance entities, so we'll need + * to remap some of them along the way. + * To get from one to the other, we'll need to do the following: + * - Instantiate the nested slice and nested prefab + * - Patch the nested slice instance and fix up the entity ID references + * - Replace the nested prefab instance entities with the fixed-up slice ones + * - Add the nested instance (and the link patch) to the top-level prefab + */ + auto instanceToTemplateInterface = AZ::Interface::Get(); auto prefabSystemComponentInterface = AZ::Interface::Get(); @@ -371,22 +446,83 @@ namespace AZ AzToolsFramework::Prefab::PrefabDom unmodifiedNestedInstanceDom; instanceToTemplateInterface->GenerateDomForInstance(unmodifiedNestedInstanceDom, *(nestedInstance.get())); - // Currently, DataPatch conversions for nested slices aren't implemented, so all nested slice overrides will - // be lost. - AZ_Warning( - "Convert-Slice", false, " Nested slice instances will lose all of their override data during conversion.", - nestedTemplate->get().GetFilePath().c_str()); - - // Set the container entity of the nested prefab to have the top-level prefab as the parent. - // Once DataPatch conversions are supported, this will need to change to nest the prefab under the appropriate entity - // within the level. + // Instantiate a new instance of the nested slice + SliceComponent* dependentSlice = sliceAsset.Get()->GetComponent(); + [[maybe_unused]] AZ::SliceComponent::InstantiateResult instantiationResult = dependentSlice->Instantiate(); + AZ_Assert(instantiationResult == AZ::SliceComponent::InstantiateResult::Success, "Failed to instantiate instance"); + + // Apply the data patch for this instance of the nested slice. This will provide us with a version of the slice's entities + // with all data overrides applied to them. + DataPatch::FlagsMap sourceDataFlags = dependentSlice->GetDataFlagsForInstances().GetDataFlagsForPatching(); + DataPatch::FlagsMap targetDataFlags = instance.GetDataFlags().GetDataFlagsForPatching(&instance.GetEntityIdToBaseMap()); + AZ::ObjectStream::FilterDescriptor filterDesc(AZ::Data::AssetFilterNoAssetLoading); + + AZ::SliceComponent::InstantiatedContainer sourceObjects(false); + dependentSlice->GetEntities(sourceObjects.m_entities); + dependentSlice->GetAllMetadataEntities(sourceObjects.m_metadataEntities); + + const DataPatch& dataPatch = instance.GetDataPatch(); + auto instantiated = + dataPatch.Apply(&sourceObjects, dependentSlice->GetSerializeContext(), filterDesc, sourceDataFlags, targetDataFlags); + + // Run through all the instantiated entities and fix up their parent hierarchy: + // - Invalid parents need to get set to the container. + // - Valid parents into the top-level instance mean that the nested slice instance is also child-nested under an entity. + // Prefabs handle this type of nesting differently - we need to set the parent to the container, and the container's + // parent to that other instance. auto containerEntity = nestedInstance->GetContainerEntity(); - AzToolsFramework::Components::TransformComponent* transformComponent = - containerEntity->get().FindComponent(); - if (transformComponent) + auto containerEntityId = containerEntity->get().GetId(); + for (auto entity : instantiated->m_entities) { - transformComponent->SetParent(topLevelInstance->GetContainerEntityId()); - transformComponent->UpdateCachedWorldTransform(); + AzToolsFramework::Components::TransformComponent* transformComponent = + entity->FindComponent(); + if (transformComponent) + { + bool onlySetIfInvalid = true; + auto parentId = transformComponent->GetParentId(); + if (parentId.IsValid()) + { + auto parentAlias = m_aliasIdMapper.find(TemplateEntityIdPair(topLevelInstance->GetTemplateId(), parentId)); + if (parentAlias != m_aliasIdMapper.end()) + { + // Set the container's parent to this entity's parent, and set this entity's parent to the container + // (i.e. go from A->B to A->container->B) + auto newParentId = topLevelInstance->GetEntityId(parentAlias->second); + SetParentEntity(containerEntity->get(), newParentId, false); + onlySetIfInvalid = false; + } + } + + SetParentEntity(*entity, containerEntityId, onlySetIfInvalid); + } + } + + // Replace all the entities in the instance with the new patched ones. + // (This is easier than trying to figure out what the patched data changes are - we can let the JSON patch handle it for us) + nestedInstance->RemoveNestedEntities( + [](const AZStd::unique_ptr&) + { + return true; + }); + for (auto& entity : instantiated->m_entities) + { + auto entityAlias = m_aliasIdMapper.find(TemplateEntityIdPair(nestedInstance->GetTemplateId(), entity->GetId())); + if (entityAlias != m_aliasIdMapper.end()) + { + nestedInstance->AddEntity(*entity, entityAlias->second); + } + else + { + AZ_Assert(false, "Failed to find entity alias."); + nestedInstance->AddEntity(*entity); + } + } + + // Set the container entity of the nested prefab to have the top-level prefab as the parent if it hasn't already gotten + // another entity as its parent. + { + constexpr bool onlySetIfInvalid = true; + SetParentEntity(containerEntity->get(), topLevelInstance->GetContainerEntityId(), onlySetIfInvalid); } // Add the nested instance itself to the top-level prefab. To do this, we need to add it to our top-level instance, @@ -395,7 +531,22 @@ namespace AZ AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomBefore; instanceToTemplateInterface->GenerateDomForInstance(topLevelInstanceDomBefore, *topLevelInstance); - AzToolsFramework::Prefab::Instance& addedInstance = topLevelInstance->AddInstance(AZStd::move(nestedInstance)); + // When creating the new instance, we would like to have deterministic instance aliases. Prefabs that depend on this one + // will have patches that reference the alias, so if we reconvert this slice a second time, we would like it to produce + // the same results. To get a deterministic and unique alias, we rely on the slice instance. The slice instance contains + // a map of slice entity IDs to unique instance entity IDs. We'll just consistently use the first entry in the map as the + // unique instance ID. + AZStd::string instanceAlias; + auto entityIdMap = instance.GetEntityIdMap(); + if (!entityIdMap.empty()) + { + instanceAlias = AZStd::string::format("Instance_%s", entityIdMap.begin()->second.ToString().c_str()); + } + else + { + instanceAlias = AZStd::string::format("Instance_%s", AZ::Entity::MakeId().ToString().c_str()); + } + AzToolsFramework::Prefab::Instance& addedInstance = topLevelInstance->AddInstance(AZStd::move(nestedInstance), instanceAlias); AzToolsFramework::Prefab::PrefabDom topLevelInstanceDomAfter; instanceToTemplateInterface->GenerateDomForInstance(topLevelInstanceDomAfter, *topLevelInstance); @@ -418,9 +569,26 @@ namespace AZ AzToolsFramework::Prefab::InvalidLinkId); prefabSystemComponentInterface->PropagateTemplateChanges(topLevelInstance->GetTemplateId()); + AZ::Interface::Get()->UpdateTemplateInstancesInQueue(); + return true; } + void SliceConverter::SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid) + { + AzToolsFramework::Components::TransformComponent* transformComponent = + entity.FindComponent(); + if (transformComponent) + { + // Only set the parent if we didn't set the onlySetIfInvalid flag, or if we did and the parent is currently invalid + if (!onlySetIfInvalid || !transformComponent->GetParentId().IsValid()) + { + transformComponent->SetParent(parentId); + transformComponent->UpdateCachedWorldTransform(); + } + } + } + void SliceConverter::PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId) { auto prefabSystemComponentInterface = AZ::Interface::Get(); diff --git a/Code/Tools/SerializeContextTools/SliceConverter.h b/Code/Tools/SerializeContextTools/SliceConverter.h index bec893ff56..82dcf30383 100644 --- a/Code/Tools/SerializeContextTools/SliceConverter.h +++ b/Code/Tools/SerializeContextTools/SliceConverter.h @@ -39,24 +39,35 @@ namespace AZ class SliceConverter : public Converter { public: - static bool ConvertSliceFiles(Application& application); + bool ConvertSliceFiles(Application& application); private: - static bool ConnectToAssetProcessor(); - static void DisconnectFromAssetProcessor(); + using TemplateEntityIdPair = AZStd::pair; - static bool ConvertSliceFile(AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun); - static bool ConvertSliceToPrefab( + bool ConnectToAssetProcessor(); + void DisconnectFromAssetProcessor(); + + bool ConvertSliceFile(AZ::SerializeContext* serializeContext, const AZStd::string& slicePath, bool isDryRun); + bool ConvertSliceToPrefab( AZ::SerializeContext* serializeContext, AZ::IO::PathView outputPath, bool isDryRun, AZ::Entity* rootEntity); - static void FixPrefabEntities(AZ::Entity& containerEntity, SliceComponent::EntityList& sliceEntities); - static bool ConvertNestedSlices( + void FixPrefabEntities(AZ::Entity& containerEntity, SliceComponent::EntityList& sliceEntities); + bool ConvertNestedSlices( SliceComponent* sliceComponent, AzToolsFramework::Prefab::Instance* sourceInstance, AZ::SerializeContext* serializeContext, bool isDryRun); - static bool ConvertSliceInstance( + bool ConvertSliceInstance( AZ::SliceComponent::SliceInstance& instance, AZ::Data::Asset& sliceAsset, AzToolsFramework::Prefab::TemplateReference nestedTemplate, AzToolsFramework::Prefab::Instance* topLevelInstance); - static void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId); - static bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId); + void SetParentEntity(const AZ::Entity& entity, const AZ::EntityId& parentId, bool onlySetIfInvalid); + void PrintPrefab(AzToolsFramework::Prefab::TemplateId templateId); + bool SavePrefab(AZ::IO::PathView outputPath, AzToolsFramework::Prefab::TemplateId templateId); + + // Track all of the entity IDs created and the prefab entity aliases that map to them. This mapping is used + // with nested slice conversion to remap parent entity IDs to the correct prefab entity IDs. + AZStd::unordered_map m_aliasIdMapper; + + // Track all of the created prefab template IDs on a slice conversion so that they can get removed at the end of the + // conversion for that file. + AZStd::unordered_set m_createdTemplateIds; }; } // namespace SerializeContextTools } // namespace AZ diff --git a/Code/Tools/SerializeContextTools/SliceConverterEditorEntityContextComponent.h b/Code/Tools/SerializeContextTools/SliceConverterEditorEntityContextComponent.h new file mode 100644 index 0000000000..166cb2abdf --- /dev/null +++ b/Code/Tools/SerializeContextTools/SliceConverterEditorEntityContextComponent.h @@ -0,0 +1,65 @@ +/* +* 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. +* +*/ +#pragma once + +#include + +namespace AzToolsFramework +{ + // This class is an inelegant workaround for use by the Slice Converter to selectively disable entity add/remove logic + // during slice conversion in the EditorEntityContextComponent. Specifically, the standard versions of these methods will + // attempt to activate the entities as they're added. This is both unnecessary and undesirable during slice conversion, since + // entity activation requires a lot of subsystems to be active and valid. + // Instead, by selectively disabling this logic, the entities can remain in an initialized state, which is sufficient for conversion, + // without requiring those extra subsystems. + + // This problem also could have been solved by adding APIs to the EditorEntityContextComponent or the EntityContext, but there aren't + // any other known valid use cases for disabling this logic, so the extra APIs would simply encourage "bad behavior" by using them + // when they likely aren't necessary or desired. + + class SliceConverterEditorEntityContextComponent + : public EditorEntityContextComponent + { + public: + + AZ_COMPONENT(SliceConverterEditorEntityContextComponent, "{1CB0C38F-8E85-4422-91C6-E1F3B9B4B853}"); + + SliceConverterEditorEntityContextComponent() : EditorEntityContextComponent() {} + + // Simple API to selectively disable this logic *only* when performing slice to prefab conversion. + static void DisableOnContextEntityLogic() + { + m_enableOnContextEntityLogic = false; + } + + protected: + + void OnContextEntitiesAdded([[maybe_unused]] const EntityList& entities) override + { + if (m_enableOnContextEntityLogic) + { + EditorEntityContextComponent::OnContextEntitiesAdded(entities); + } + } + + void OnContextEntityRemoved([[maybe_unused]] const AZ::EntityId& id) override + { + if (m_enableOnContextEntityLogic) + { + EditorEntityContextComponent::OnContextEntityRemoved(id); + } + } + + // By default, act just like the EditorEntityContextComponent + static inline bool m_enableOnContextEntityLogic = true; + }; +} // namespace AzToolsFramework diff --git a/Code/Tools/SerializeContextTools/main.cpp b/Code/Tools/SerializeContextTools/main.cpp index d9f2cfbcc2..28eeb5ebe8 100644 --- a/Code/Tools/SerializeContextTools/main.cpp +++ b/Code/Tools/SerializeContextTools/main.cpp @@ -125,7 +125,8 @@ int main(int argc, char** argv) } else if (AZ::StringFunc::Equal("convert-slice", action.c_str())) { - result = SliceConverter::ConvertSliceFiles(application); + SliceConverter sliceConverter; + result = sliceConverter.ConvertSliceFiles(application); } else { diff --git a/Code/Tools/SerializeContextTools/serializecontexttools_files.cmake b/Code/Tools/SerializeContextTools/serializecontexttools_files.cmake index 814c55ea08..3427357149 100644 --- a/Code/Tools/SerializeContextTools/serializecontexttools_files.cmake +++ b/Code/Tools/SerializeContextTools/serializecontexttools_files.cmake @@ -17,6 +17,7 @@ set(FILES Dumper.h Dumper.cpp main.cpp + SliceConverterEditorEntityContextComponent.h SliceConverter.h SliceConverter.cpp Utilities.h diff --git a/Gems/Camera/Code/Source/CameraComponentController.cpp b/Gems/Camera/Code/Source/CameraComponentController.cpp index d0a124067b..cad666c1cd 100644 --- a/Gems/Camera/Code/Source/CameraComponentController.cpp +++ b/Gems/Camera/Code/Source/CameraComponentController.cpp @@ -178,7 +178,10 @@ namespace Camera if ((!m_viewSystem)||(!m_system)) { // perform first-time init - m_system = gEnv->pSystem; + if (gEnv) + { + m_system = gEnv->pSystem; + } if (m_system) { // Initialize local view. diff --git a/Registry/gem_autoload.serializecontexttools.setreg b/Registry/gem_autoload.serializecontexttools.setreg index e7e88dd6a6..0f74355084 100644 --- a/Registry/gem_autoload.serializecontexttools.setreg +++ b/Registry/gem_autoload.serializecontexttools.setreg @@ -10,6 +10,9 @@ "PythonAssetBuilder.Editor": { "AutoLoad": false }, + "AWSCore": { + "AutoLoad": false + }, "AWSCore.Editor": { "AutoLoad": false }, @@ -21,6 +24,9 @@ }, "AWSMetrics": { "AutoLoad": false + }, + "AWSMetrics.Editor": { + "AutoLoad": false } } }