Slice.cpp 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. /*
  2. * Copyright (c) Contributors to the Open 3D Engine Project.
  3. * For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. *
  5. * SPDX-License-Identifier: Apache-2.0 OR MIT
  6. *
  7. */
  8. #include <AzCore/UnitTest/TestTypes.h>
  9. #include <AzCore/Serialization/SerializeContext.h>
  10. #include <AzCore/Asset/AssetManager.h>
  11. #include <AzCore/Slice/SliceAssetHandler.h>
  12. #include <AzCore/UserSettings/UserSettingsComponent.h>
  13. #include <AzFramework/IO/LocalFileIO.h>
  14. #include <AzToolsFramework/Slice/SliceUtilities.h>
  15. #include <AzToolsFramework/Application/ToolsApplication.h>
  16. #include <AzToolsFramework/API/ToolsApplicationAPI.h>
  17. #include <AzToolsFramework/ToolsComponents/TransformComponent.h>
  18. #include <AzToolsFramework/Entity/EditorEntityContextBus.h>
  19. #include <AzToolsFramework/Entity/EditorEntitySortComponent.h>
  20. #include <AzToolsFramework/Entity/SliceEditorEntityOwnershipServiceBus.h>
  21. #include <AzToolsFramework/UI/Slice/SlicePushWidget.hxx>
  22. #include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
  23. #include <AzToolsFramework/UnitTest/ToolsTestApplication.h>
  24. namespace UnitTest
  25. {
  26. class SlicePushCyclicDependencyTest
  27. : public LeakDetectionFixture
  28. {
  29. public:
  30. SlicePushCyclicDependencyTest()
  31. : LeakDetectionFixture()
  32. { }
  33. void SetUp() override
  34. {
  35. AZ::ComponentApplication::Descriptor componentApplicationDesc;
  36. componentApplicationDesc.m_useExistingAllocator = true;
  37. m_application = aznew ToolsTestApplication("SlicePushCyclicDependencyTest");
  38. AZ::ComponentApplication::StartupParameters startupParameters;
  39. startupParameters.m_loadSettingsRegistry = false;
  40. m_application->Start(componentApplicationDesc, startupParameters);
  41. // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is
  42. // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash
  43. // in the unit tests.
  44. AZ::UserSettingsComponentRequestBus::Broadcast(&AZ::UserSettingsComponentRequests::DisableSaveOnFinalize);
  45. }
  46. void TearDown() override
  47. {
  48. // Release all slice asset references, so AssetManager doens't complain.
  49. m_sliceAssets.clear();
  50. delete m_application;
  51. }
  52. // This function transfers the ownership of the argument `entity`. Do not delete or use it afterwards.
  53. AZ::Data::AssetId SaveAsSlice(AZ::Entity* entity)
  54. {
  55. AZStd::vector<AZ::Entity*> entities;
  56. entities.push_back(entity);
  57. return SaveAsSlice(entities);
  58. }
  59. // This function transfers the ownership of all the entity pointers. Do not delete or use them afterwards.
  60. AZ::Data::AssetId SaveAsSlice(AZStd::vector<AZ::Entity*> entities)
  61. {
  62. AZ::Entity* sliceEntity = aznew AZ::Entity();
  63. AZ::SliceComponent* sliceComponent = nullptr;
  64. sliceComponent = aznew AZ::SliceComponent();
  65. sliceComponent->SetSerializeContext(m_application->GetSerializeContext());
  66. for (auto& entity : entities)
  67. {
  68. sliceComponent->AddEntity(entity);
  69. }
  70. // Don't activate `sliceEntity`, whose purpose is to be attached by `sliceComponent`.
  71. sliceEntity->AddComponent(sliceComponent);
  72. AZ::Data::AssetId assetId = AZ::Data::AssetId(AZ::Uuid::CreateRandom(), 0);
  73. AZ::Data::Asset<AZ::SliceAsset> sliceAssetHolder = AZ::Data::AssetManager::Instance().CreateAsset<AZ::SliceAsset>(assetId, AZ::Data::AssetLoadBehavior::Default);
  74. sliceAssetHolder.GetAs<AZ::SliceAsset>()->SetData(sliceEntity, sliceComponent);
  75. // Hold on to sliceAssetHolder so it's not ref-counted away.
  76. m_sliceAssets.emplace(assetId, sliceAssetHolder);
  77. return assetId;
  78. }
  79. AZ::SliceComponent::EntityList InstantiateSlice(AZ::Data::AssetId sliceAssetId)
  80. {
  81. auto foundItr = m_sliceAssets.find(sliceAssetId);
  82. AZ_TEST_ASSERT(foundItr != m_sliceAssets.end());
  83. AZ::SliceComponent* rootSlice;
  84. AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(rootSlice,
  85. &AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
  86. AZ::SliceComponent::SliceInstanceAddress sliceInstAddress = rootSlice->AddSlice(foundItr->second);
  87. rootSlice->Instantiate();
  88. const AZ::SliceComponent::InstantiatedContainer* instanceContainer = sliceInstAddress.GetInstance()->GetInstantiated();
  89. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::HandleEntitiesAdded, instanceContainer->m_entities);
  90. return instanceContainer->m_entities;
  91. }
  92. void RemoveAllSlices()
  93. {
  94. AZ::SliceComponent* rootSlice;
  95. AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(rootSlice,
  96. &AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
  97. for (auto sliceAssetPair : m_sliceAssets)
  98. {
  99. rootSlice->RemoveSlice(sliceAssetPair.second);
  100. }
  101. }
  102. public:
  103. AZ::IO::LocalFileIO m_localFileIO;
  104. ToolsTestApplication* m_application = nullptr;
  105. AZStd::unordered_map<AZ::Data::AssetId, AZ::Data::Asset<AZ::SliceAsset>> m_sliceAssets;
  106. };
  107. // Test pushing slices to create news slices that could result in cyclic
  108. // dependency, e.g. push slice1 => slice2 and slice2 => slice1 at the same
  109. // time.
  110. TEST_F(SlicePushCyclicDependencyTest, PushTwoSlicesToDependOnEachOther)
  111. {
  112. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  113. AZ::Entity* entity = aznew AZ::Entity("TestEntity0");
  114. entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  115. AZ::Data::AssetId sliceAssetId0 = SaveAsSlice(entity);
  116. entity = nullptr;
  117. entity = aznew AZ::Entity("TestEntity1");
  118. entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  119. AZ::Data::AssetId sliceAssetId1 = SaveAsSlice(entity);
  120. entity = nullptr;
  121. AZ::SliceComponent::EntityList slice0EntitiesA = InstantiateSlice(sliceAssetId0);
  122. EXPECT_EQ(slice0EntitiesA.size(), 1);
  123. AZ::SliceComponent::EntityList slice0EntitiesB = InstantiateSlice(sliceAssetId0);
  124. EXPECT_EQ(slice0EntitiesB.size(), 1);
  125. AZ::SliceComponent::EntityList slice1EntitiesA = InstantiateSlice(sliceAssetId1);
  126. EXPECT_EQ(slice1EntitiesA.size(), 1);
  127. AZ::SliceComponent::EntityList slice1EntitiesB = InstantiateSlice(sliceAssetId1);
  128. EXPECT_EQ(slice1EntitiesA.size(), 1);
  129. // Reparent entities to slice1EntityA <-- slice0EntityA, slice0EntityB <-- slice1EntityA (<-- points to parent).
  130. AZ::TransformBus::Event(slice0EntitiesA[0]->GetId(), &AZ::TransformBus::Events::SetParent, slice1EntitiesA[0]->GetId());
  131. AZ::TransformBus::Event(slice1EntitiesB[0]->GetId(), &AZ::TransformBus::Events::SetParent, slice0EntitiesB[0]->GetId());
  132. AZStd::unordered_map<AZ::Data::AssetId, AZ::SliceComponent::EntityIdSet> unpushableEntityIdsPerAsset;
  133. AZStd::unordered_map<AZ::EntityId, AZ::SliceComponent::EntityAncestorList> sliceAncestryMapping;
  134. AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>> newChildEntityIdAncestorPairs;
  135. AZStd::unordered_set<AZ::EntityId> entitiesToAdd;
  136. AzToolsFramework::EntityIdList inputEntityIds = { slice0EntitiesA[0]->GetId(), slice0EntitiesB[0]->GetId(), slice1EntitiesA[0]->GetId(), slice1EntitiesB[0]->GetId() };
  137. AZStd::unordered_set<AZ::EntityId> pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(
  138. inputEntityIds, unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  139. // Because there would be cyclic dependency in the resulting slices, we only allow pushing of one entity.
  140. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 1);
  141. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 1);
  142. RemoveAllSlices();
  143. }
  144. TEST_F(SlicePushCyclicDependencyTest, PushMultipleEntitiesOneOfChildrenCauseCyclicDependency)
  145. {
  146. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  147. AZ::Entity* tempAssetEntity = aznew AZ::Entity("TestEntity0");
  148. tempAssetEntity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  149. AZ::Data::AssetId sliceAssetId0 = SaveAsSlice(tempAssetEntity);
  150. tempAssetEntity = nullptr;
  151. AZ::SliceComponent::EntityList slice0EntitiesA = InstantiateSlice(sliceAssetId0);
  152. EXPECT_EQ(slice0EntitiesA.size(), 1);
  153. AZ::SliceComponent::EntityList slice0EntitiesB = InstantiateSlice(sliceAssetId0);
  154. EXPECT_EQ(slice0EntitiesB.size(), 1);
  155. AZ::Entity* looseEntity0 = aznew AZ::Entity("LooseEntity");
  156. looseEntity0->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  157. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::AddEditorEntity, looseEntity0);
  158. // Add one pushable entity as a parent of the one that will cause cyclic dependency.
  159. AZ::TransformBus::Event(looseEntity0->GetId(), &AZ::TransformBus::Events::SetParent, slice0EntitiesA[0]->GetId());
  160. AZ::TransformBus::Event(slice0EntitiesB[0]->GetId(), &AZ::TransformBus::Events::SetParent, looseEntity0->GetId());
  161. AZ::SliceComponent::EntityIdSet unpushableEntityIds;
  162. AZStd::unordered_set<AZ::EntityId> entitiesToAdd;
  163. AZStd::unordered_map<AZ::Data::AssetId, AZ::SliceComponent::EntityIdSet> unpushableEntityIdsPerAsset;
  164. AZStd::unordered_map<AZ::EntityId, AZ::SliceComponent::EntityAncestorList> sliceAncestryMapping;
  165. AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>> newChildEntityIdAncestorPairs;
  166. AzToolsFramework::EntityIdList inputEntityIds = { slice0EntitiesA[0]->GetId(), slice0EntitiesB[0]->GetId(), looseEntity0->GetId() };
  167. AZStd::unordered_set<AZ::EntityId> pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(
  168. inputEntityIds, unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  169. // slice0EntityB can't be pushed to slice0EntityA, but its parent (looseEntity) can.
  170. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 1);
  171. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 1);
  172. AZ::Entity* looseEntity1 = aznew AZ::Entity("LooseEntity");
  173. looseEntity1->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  174. AzToolsFramework::EditorEntityContextRequestBus::Broadcast(&AzToolsFramework::EditorEntityContextRequestBus::Events::AddEditorEntity, looseEntity1);
  175. // Add one more pushable entity as a parent.
  176. AZ::TransformBus::Event(slice0EntitiesB[0]->GetId(), &AZ::TransformBus::Events::SetParent, looseEntity1->GetId());
  177. AZ::TransformBus::Event(looseEntity1->GetId(), &AZ::TransformBus::Events::SetParent, looseEntity0->GetId());
  178. inputEntityIds.push_back(looseEntity1->GetId());
  179. unpushableEntityIds.clear();
  180. sliceAncestryMapping.clear();
  181. newChildEntityIdAncestorPairs.clear();
  182. pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(inputEntityIds,
  183. unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  184. // slice0EntityB can't be pushed to slice0EntityA, but the two LooseEntity instances can
  185. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 1);
  186. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 2);
  187. tempAssetEntity = aznew AZ::Entity("TestEntity1");
  188. tempAssetEntity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  189. SaveAsSlice(tempAssetEntity);
  190. tempAssetEntity = nullptr;
  191. AZ::SliceComponent::EntityList slice1EntitiesA = InstantiateSlice(sliceAssetId0);
  192. EXPECT_EQ(slice1EntitiesA.size(), 1);
  193. // Add another slice-owned entity `slice1EntitiesA` as the parent of the one causing cyclic dependency,
  194. // and push addition of `slice1EntitiesA`.
  195. AZ::TransformBus::Event(slice0EntitiesB[0]->GetId(), &AZ::TransformBus::Events::SetParent, slice1EntitiesA[0]->GetId());
  196. AZ::TransformBus::Event(slice1EntitiesA[0]->GetId(), &AZ::TransformBus::Events::SetParent, slice0EntitiesA[0]->GetId());
  197. inputEntityIds.clear();
  198. inputEntityIds.push_back(slice0EntitiesA[0]->GetId());
  199. inputEntityIds.push_back(slice0EntitiesB[0]->GetId());
  200. inputEntityIds.push_back(slice1EntitiesA[0]->GetId());
  201. unpushableEntityIds.clear();
  202. sliceAncestryMapping.clear();
  203. newChildEntityIdAncestorPairs.clear();
  204. pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(inputEntityIds,
  205. unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  206. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 1);
  207. if (unpushableEntityIdsPerAsset.size() == 1)
  208. {
  209. AzToolsFramework::EntityIdSet ids = unpushableEntityIdsPerAsset.begin()->second;
  210. AZ_TEST_ASSERT(ids.size() == 2);
  211. }
  212. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 0);
  213. // But if an entity is not a parent of an unpushable one, it should be added.
  214. AZ::TransformBus::Event(looseEntity0->GetId(), &AZ::TransformBus::Events::SetParent, slice0EntitiesA[0]->GetId());
  215. inputEntityIds.push_back(looseEntity0->GetId());
  216. unpushableEntityIds.clear();
  217. sliceAncestryMapping.clear();
  218. newChildEntityIdAncestorPairs.clear();
  219. pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(inputEntityIds,
  220. unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  221. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 1);
  222. if (unpushableEntityIdsPerAsset.size() == 1)
  223. {
  224. AzToolsFramework::EntityIdSet ids = unpushableEntityIdsPerAsset.begin()->second;
  225. AZ_TEST_ASSERT(ids.size() == 2);
  226. }
  227. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 1);
  228. RemoveAllSlices();
  229. }
  230. TEST_F(SlicePushCyclicDependencyTest, PushSliceWithNewDuplicatedChild)
  231. {
  232. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  233. AZ::Entity* entity = aznew AZ::Entity("TestEntity0");
  234. entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  235. AZ::Data::AssetId sliceAssetId0 = SaveAsSlice(entity);
  236. entity = nullptr;
  237. entity = aznew AZ::Entity("TestEntity1");
  238. entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  239. AZ::Data::AssetId sliceAssetId1 = SaveAsSlice(entity);
  240. entity = nullptr;
  241. AZ::SliceComponent::EntityList slice0Entities = InstantiateSlice(sliceAssetId0);
  242. EXPECT_EQ(slice0Entities.size(), 1);
  243. AZ::SliceComponent::EntityList slice1EntitiesA = InstantiateSlice(sliceAssetId1);
  244. EXPECT_EQ(slice1EntitiesA.size(), 1);
  245. AZ::SliceComponent::EntityList slice1EntitiesB = InstantiateSlice(sliceAssetId1);
  246. EXPECT_EQ(slice1EntitiesB.size(), 1);
  247. // Reparent the entity1s to be children of entity0
  248. AZ::TransformBus::Event(slice1EntitiesA[0]->GetId(), &AZ::TransformBus::Events::SetParent, slice0Entities[0]->GetId());
  249. AZ::TransformBus::Event(slice1EntitiesB[0]->GetId(), &AZ::TransformBus::Events::SetParent, slice0Entities[0]->GetId());
  250. AZStd::unordered_set<AZ::EntityId> entitiesToAdd;
  251. AZStd::unordered_map<AZ::Data::AssetId, AZ::SliceComponent::EntityIdSet> unpushableEntityIdsPerAsset;
  252. AZStd::unordered_map<AZ::EntityId, AZ::SliceComponent::EntityAncestorList> sliceAncestryMapping;
  253. AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>> newChildEntityIdAncestorPairs;
  254. AzToolsFramework::EntityIdList inputEntityIds = { slice0Entities[0]->GetId(), slice1EntitiesA[0]->GetId(), slice1EntitiesB[0]->GetId() };
  255. AZStd::unordered_set<AZ::EntityId> pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(
  256. inputEntityIds, unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  257. // Because there would be cyclic dependency in the resulting slices, we only allow pushing of one entity.
  258. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 2);
  259. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 0);
  260. AZ_TEST_ASSERT(newChildEntityIdAncestorPairs.size() == 2);
  261. RemoveAllSlices();
  262. }
  263. // Test pushing slice with children that aren't going to be in the pushed version
  264. // either because the user has chosen to leave them out, or they are unpushable for some reason
  265. // (e.g. they would create a circular dependency).
  266. TEST_F(SlicePushCyclicDependencyTest, SlicePush_DontPushSomeChildren_ChildrenRemovedFromChildOrderArray)
  267. {
  268. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  269. AZ::Data::AssetManager& assetManager = AZ::Data::AssetManager::Instance();
  270. // Create a slice
  271. AZ::Entity* entity = aznew AZ::Entity("TestEntity0");
  272. entity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  273. AZ::Data::AssetId sliceAssetId0 = SaveAsSlice(entity);
  274. entity = nullptr;
  275. // Instantiate two copies of the slice.
  276. AZ::SliceComponent::EntityList parentSlice = InstantiateSlice(sliceAssetId0);
  277. AZ::SliceComponent::EntityList childSlice = InstantiateSlice(sliceAssetId0);
  278. // Make one a child of the other.
  279. AZ::TransformBus::Event(childSlice[0]->GetId(), &AZ::TransformBus::Events::SetParent, parentSlice[0]->GetId());
  280. // Grab the parent entity and add an EditorEntitySortComponent to it.
  281. AzToolsFramework::Components::EditorEntitySortComponent* parentSortComponent;
  282. AZ::Entity* parentEntity = nullptr;
  283. {
  284. AZ::ComponentApplicationBus::BroadcastResult(parentEntity, &AZ::ComponentApplicationBus::Handler::FindEntity, parentSlice[0]->GetId());
  285. AZ_Assert(parentEntity, "Failed to find parentEntity\n");
  286. parentEntity->Deactivate();
  287. parentSortComponent = parentEntity->CreateComponent<AzToolsFramework::Components::EditorEntitySortComponent>();
  288. AZ_Assert(parentSortComponent, "Failed to create parentSortComponent\n");
  289. parentEntity->Activate();
  290. }
  291. // Create two entities and make them children of the parent
  292. AZ::Entity* childEntity0;
  293. {
  294. childEntity0 = aznew AZ::Entity("TestChildEntity");
  295. childEntity0->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  296. childEntity0->Init();
  297. childEntity0->Activate();
  298. AZ::TransformBus::Event(childEntity0->GetId(), &AZ::TransformBus::Events::SetParent, parentEntity->GetId());
  299. AZ_Assert(childEntity0, "Failed to create childEntity0\n");
  300. }
  301. AZ::Entity* childEntity1;
  302. {
  303. childEntity1 = aznew AZ::Entity("TestChildEntity");
  304. childEntity1->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  305. childEntity1->Init();
  306. childEntity1->Activate();
  307. AZ::TransformBus::Event(childEntity1->GetId(), &AZ::TransformBus::Events::SetParent, parentEntity->GetId());
  308. AZ_Assert(childEntity1, "Failed to create childEntity0\n");
  309. }
  310. // Analyse hierarchy for unpushable entities.
  311. AZStd::unordered_map<AZ::Data::AssetId, AZ::SliceComponent::EntityIdSet> unpushableEntityIdsPerAsset;
  312. {
  313. AZStd::unordered_map<AZ::EntityId, AZ::SliceComponent::EntityAncestorList> sliceAncestryMapping;
  314. AZStd::vector<AZStd::pair<AZ::EntityId, AZ::SliceComponent::EntityAncestorList>> newChildEntityIdAncestorPairs;
  315. AZStd::unordered_set<AZ::EntityId> entitiesToAdd;
  316. // Make list of entities to be pushed. Leave out childEntity1 to emulate a user having unchecked it in the advanced push widget.
  317. AzToolsFramework::EntityIdList inputEntityIds = { parentEntity->GetId(), childSlice[0]->GetId(), childEntity0->GetId() };
  318. AZStd::unordered_set<AZ::EntityId> pushableNewChildEntityIds = AzToolsFramework::SliceUtilities::GetPushableNewChildEntityIds(
  319. inputEntityIds, unpushableEntityIdsPerAsset, sliceAncestryMapping, newChildEntityIdAncestorPairs, entitiesToAdd);
  320. // UnpushableEntityIdsPerAsset should now contain a reference to childSlice which can't be
  321. // pushed as it would create a circular reference. This would get picked up by advanced or quick push
  322. // during GetPushableNewChildEntityIds.
  323. AZ_TEST_ASSERT(unpushableEntityIdsPerAsset.size() == 1);
  324. }
  325. // Add all child entities to the parent slice's child order array.
  326. parentSortComponent->AddChildEntity(childSlice[0]->GetId(), false);
  327. parentSortComponent->AddChildEntity(childEntity0->GetId(), false);
  328. parentSortComponent->AddChildEntity(childEntity1->GetId(), false);
  329. AzToolsFramework::EntityOrderArray orderArray = parentSortComponent->GetChildEntityOrderArray();
  330. // Make a list of entities that we don't want to push (childEntity1). This will emulate a user deciding not to push
  331. // certain entities in the advanced push widget.
  332. AZStd::vector <AZ::EntityId> idsNotToPush;
  333. idsNotToPush.push_back(childEntity1->GetId());
  334. // Do the pruning to produce the list of entities that will be pushed.
  335. AzToolsFramework::EntityOrderArray prunedOrderArray;
  336. {
  337. prunedOrderArray.reserve(orderArray.size());
  338. AzToolsFramework::SliceUtilities::WillPushEntityCallback willPushEntityCallback =
  339. [&unpushableEntityIdsPerAsset, &idsNotToPush]
  340. (const AZ::EntityId entityId, const AZ::Data::Asset <AZ::SliceAsset>& assetToPushTo) -> bool
  341. {
  342. if (unpushableEntityIdsPerAsset[assetToPushTo.GetId()].find(entityId) != unpushableEntityIdsPerAsset[assetToPushTo.GetId()].end())
  343. {
  344. return false;
  345. }
  346. for (AZ::EntityId id : idsNotToPush)
  347. {
  348. if (id == entityId)
  349. {
  350. return false;
  351. }
  352. }
  353. return true;
  354. };
  355. AZ::Data::Asset<AZ::SliceAsset> sliceAsset = assetManager.FindOrCreateAsset<AZ::SliceAsset>(sliceAssetId0, AZ::Data::AssetLoadBehavior::Default);
  356. AzToolsFramework::SliceUtilities::RemoveInvalidChildOrderArrayEntries(orderArray, prunedOrderArray, sliceAsset, willPushEntityCallback);
  357. }
  358. // At this point there should only be childEntity0 in the pruned order array.
  359. bool pruningCorrect = false;
  360. if (prunedOrderArray.size() == 1 && prunedOrderArray[0] == childEntity0->GetId())
  361. {
  362. pruningCorrect = true;
  363. }
  364. EXPECT_EQ(pruningCorrect, true);
  365. RemoveAllSlices();
  366. }
  367. // Rename our fixture class for the next test so that it has a more accurate test name.
  368. class SliceActivationOrderTest : public SlicePushCyclicDependencyTest {};
  369. // Class that listens for AZ_Warning messages and asserts if any are found.
  370. class SliceTestWarningInterceptor :
  371. public AZ::Debug::TraceMessageBus::Handler
  372. {
  373. public:
  374. SliceTestWarningInterceptor()
  375. {
  376. AZ::Debug::TraceMessageBus::Handler::BusConnect();
  377. }
  378. ~SliceTestWarningInterceptor() override
  379. {
  380. AZ::Debug::TraceMessageBus::Handler::BusDisconnect();
  381. }
  382. bool OnWarning(const char *window, const char* message) override
  383. {
  384. (void)window;
  385. ADD_FAILURE() << "Test failed due to an undesirable warning being generated:\n" << message;
  386. return true;
  387. }
  388. };
  389. // LY-95800: If a child entity with a transform is present in a slice asset earlier
  390. // than its parent, the activation of the parent entity can cause the child to have a
  391. // state that doesn't match the undo cache, which generates a warning about inconsistent data.
  392. // (See PreemptiveUndoCache::Validate)
  393. // If the bug is present, a warning will be thrown which fails this unit test.
  394. TEST_F(SliceActivationOrderTest, ActivationOrderShouldNotAffectUndoCache)
  395. {
  396. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  397. // Swallow deprecation warnings from the Transform component as they are not relevant to this test
  398. UnitTest::ErrorHandler errorHandler("GetScale is deprecated");
  399. // Create a parent entity with a transform component
  400. AZ::Entity* parentEntity = aznew AZ::Entity("TestParentEntity");
  401. parentEntity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  402. parentEntity->Init();
  403. parentEntity->Activate();
  404. // Create a child entity with a transform component
  405. AZ::Entity* childEntity = aznew AZ::Entity("TestChildEntity");
  406. childEntity->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  407. childEntity->Init();
  408. childEntity->Activate();
  409. // Make the child an actual child of the parent entity
  410. AZ::TransformBus::Event(childEntity->GetId(), &AZ::TransformBus::Events::SetParent, parentEntity->GetId());
  411. AZStd::vector<AZ::Entity*> entities;
  412. // Add our entities to the list of entities to make a slice from.
  413. // IMPORTANT: The child should be added before the parent. For this bug to manifest, the
  414. // child entity needs to get instantiated and activated before the parent when instantiating
  415. // the slice.
  416. childEntity->Deactivate();
  417. parentEntity->Deactivate();
  418. entities.push_back(childEntity);
  419. entities.push_back(parentEntity);
  420. // When saving a slice, SliceUtilities::VerifyAndApplySliceWorldTransformRules() clears out the
  421. // cached world transforms prior to writing out the slice asset.
  422. for (AZ::Entity* entity : entities)
  423. {
  424. AzToolsFramework::Components::TransformComponent* transformComponent = entity->FindComponent<AzToolsFramework::Components::TransformComponent>();
  425. if (transformComponent)
  426. {
  427. transformComponent->ClearCachedWorldTransform();
  428. }
  429. }
  430. // Create our slice asset
  431. AZ::Data::AssetId sliceAssetId = SaveAsSlice(entities);
  432. childEntity = nullptr;
  433. parentEntity = nullptr;
  434. entities.clear();
  435. // Create an undo batch to wrap the slice instantiation.
  436. // This is necessary, because ending the undo batch is what causes the batch to get validated.
  437. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Slice Instantiation");
  438. // Instantiate the slice.
  439. // This will instantiate the child, save it in the undo batch, instantiate the parent,
  440. // save the parent in the undo batch, and modify the child.
  441. // If the bug exists, this will cause the child's undo batch record to become inconsistent,
  442. // which will cause a warning when we call EndUndoBatch.
  443. // If the bug is fixed, the child's undo batch record will be updated.
  444. AZ::SliceComponent::EntityList sliceEntities = InstantiateSlice(sliceAssetId);
  445. // When instantiating a slice, SliceEditorEntityOwnershipService::OnSliceInstantiated() removes any entities
  446. // in the slice from the dirty entity list. This step is important because in the buggy case, the child
  447. // will be marked dirty above, but won't be updated in the undo cache yet. Removing it ensures it never
  448. // will be. If it isn't removed, it will get updated as a dirty entity when the undo batch ends.
  449. for (AZ::Entity* entity : sliceEntities)
  450. {
  451. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::RemoveDirtyEntity, entity->GetId());
  452. }
  453. // End the slice instantiation undo batch.
  454. // At this point, if the child entity's undo record doesn't match the current child entity, a warning will be emitted.
  455. {
  456. // The point of this test is to determine whether or not we got a warning from PreemptiveUndoCache
  457. // about inconsistent undo data. So intercept warnings during this step and fail the test if we get one.
  458. SliceTestWarningInterceptor warningInterceptor;
  459. AzToolsFramework::ToolsApplicationRequests::Bus::Broadcast(&AzToolsFramework::ToolsApplicationRequests::Bus::Events::EndUndoBatch);
  460. }
  461. RemoveAllSlices();
  462. }
  463. class SlicePushWidgetTest : public SlicePushCyclicDependencyTest {};
  464. TEST_F(SlicePushWidgetTest, SlicePushWidget_CalculateLevelReferences_ReferenceCountCorrect)
  465. {
  466. AUTO_RESULT_IF_SETTING_TRUE(UnitTest::prefabSystemSetting, true)
  467. // Create an entities and make it a slice.
  468. AZ::Entity* entity0 = aznew AZ::Entity("TestEntity0");
  469. entity0->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  470. AZ::Data::AssetId sliceAssetIdChild = SaveAsSlice(entity0);
  471. // Instantiate 5 copies.
  472. AZ::SliceComponent::EntityList slice0EntitiesA = InstantiateSlice(sliceAssetIdChild);
  473. AZ::SliceComponent::EntityList slice0EntitiesB = InstantiateSlice(sliceAssetIdChild);
  474. AZ::SliceComponent::EntityList slice0EntitiesC = InstantiateSlice(sliceAssetIdChild);
  475. AZ::SliceComponent::EntityList slice0EntitiesD = InstantiateSlice(sliceAssetIdChild);
  476. AZ::SliceComponent::EntityList slice0EntitiesE = InstantiateSlice(sliceAssetIdChild);
  477. // Make an entity to parent the slice instances
  478. AZ::Entity* parent0 = aznew AZ::Entity("TestParent0");
  479. parent0->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  480. parent0->Init();
  481. parent0->Activate();
  482. AZ::TransformBus::Event(slice0EntitiesA[0]->GetId(), &AZ::TransformBus::Events::SetParent, parent0->GetId());
  483. AZ::TransformBus::Event(slice0EntitiesB[0]->GetId(), &AZ::TransformBus::Events::SetParent, parent0->GetId());
  484. AZ::TransformBus::Event(slice0EntitiesC[0]->GetId(), &AZ::TransformBus::Events::SetParent, parent0->GetId());
  485. AZ::TransformBus::Event(slice0EntitiesD[0]->GetId(), &AZ::TransformBus::Events::SetParent, parent0->GetId());
  486. AZ::TransformBus::Event(slice0EntitiesE[0]->GetId(), &AZ::TransformBus::Events::SetParent, parent0->GetId());
  487. // Save parent as a slice.
  488. AZ::Data::AssetId sliceAssetIdParent = SaveAsSlice(parent0);
  489. AZ::SliceComponent::EntityList slice2EntitiesA = InstantiateSlice(sliceAssetIdParent);
  490. // Make another parent entity and add a sixth instance of the child slice.
  491. AZ::Entity* parent1 = aznew AZ::Entity("TestParent1");
  492. parent1->CreateComponent<AzToolsFramework::Components::TransformComponent>();
  493. parent1->Init();
  494. parent1->Activate();
  495. AZ::SliceComponent::EntityList slice0EntitiesF = InstantiateSlice(sliceAssetIdChild);
  496. AZ::TransformBus::Event(slice0EntitiesF[0]->GetId(), &AZ::TransformBus::Events::SetParent, parent1->GetId());
  497. AZ::SliceComponent* rootSlice;
  498. AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::BroadcastResult(rootSlice, &AzToolsFramework::SliceEditorEntityOwnershipServiceRequestBus::Events::GetEditorRootSlice);
  499. size_t parentSliceCount = AzToolsFramework::SlicePushWidget::CalculateReferenceCount(sliceAssetIdParent, rootSlice);
  500. size_t childSliceCount = AzToolsFramework::SlicePushWidget::CalculateReferenceCount(sliceAssetIdChild, rootSlice);
  501. EXPECT_EQ(parentSliceCount, 1);
  502. EXPECT_EQ(childSliceCount, 6);
  503. RemoveAllSlices();
  504. }
  505. }