From 6587e149b758a35b3fa62655db5e640b3b81e0ac Mon Sep 17 00:00:00 2001 From: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> Date: Thu, 28 Oct 2021 09:57:33 -0700 Subject: [PATCH] Added unit tests for spawnable entity aliases. This also fixes several issues discovered through the unit tests and renames a few functions to be clearer. Signed-off-by: AMZN-koppersr <82230785+AMZN-koppersr@users.noreply.github.com> --- .../AzFramework/Spawnable/Spawnable.cpp | 108 ++-- .../AzFramework/Spawnable/Spawnable.h | 16 +- .../Spawnable/SpawnableAssetHandler.cpp | 4 +- .../Spawnable/SpawnableEntitiesManager.cpp | 29 +- .../SpawnableEntitiesManagerTests.cpp | 479 ++++++++++++++++- .../Tests/Spawnable/SpawnableTests.cpp | 499 ++++++++++++++++++ .../Tests/frameworktests_files.cmake | 1 + .../Spawnable/PrefabProcessorContext.cpp | 2 +- .../Prefab/Spawnable/PrefabProcessorContext.h | 3 +- 9 files changed, 1057 insertions(+), 84 deletions(-) create mode 100644 Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp index 48662fae99..7548a8f181 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.cpp @@ -9,7 +9,9 @@ #include #include #include +#include #include +#include #include namespace AzFramework @@ -31,7 +33,7 @@ namespace AzFramework // EntityAliasVisitorBase // - bool Spawnable::EntityAliasVisitorBase::HasLock(const EntityAliasList* aliases) const + bool Spawnable::EntityAliasVisitorBase::IsSet(const EntityAliasList* aliases) const { return aliases != nullptr; } @@ -92,11 +94,15 @@ namespace AzFramework AZStd::unordered_set spawnableIds; for (const Spawnable::EntityAlias& alias : *aliases) { - auto it = spawnableIds.find(alias.m_spawnable.GetId()); - if (it == spawnableIds.end()) + // If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on. + if (alias.m_spawnable.GetId().IsValid()) { - callback(alias.m_spawnable); - spawnableIds.emplace(alias.m_spawnable.GetId()); + auto it = spawnableIds.find(alias.m_spawnable.GetId()); + if (it == spawnableIds.end()) + { + callback(alias.m_spawnable); + spawnableIds.emplace(alias.m_spawnable.GetId()); + } } } } @@ -108,7 +114,8 @@ namespace AzFramework AZStd::unordered_set spawnableIds; for (const Spawnable::EntityAlias& alias : *aliases) { - if (alias.m_tag == tag) + // If the spawnable id is not valid it means that the alias is referencing the spawnable it's stored on. + if (alias.m_tag == tag && alias.m_spawnable.GetId().IsValid()) { auto it = spawnableIds.find(alias.m_spawnable.GetId()); if (it == spawnableIds.end()) @@ -134,7 +141,7 @@ namespace AzFramework Spawnable::EntityAliasVisitor::~EntityAliasVisitor() { - if (HasLock()) + if (IsSet()) { Optimize(); @@ -169,9 +176,9 @@ namespace AzFramework return *this; } - bool Spawnable::EntityAliasVisitor::HasLock() const + bool Spawnable::EntityAliasVisitor::IsSet() const { - return EntityAliasVisitorBase::HasLock(m_entityAliasList); + return EntityAliasVisitorBase::IsSet(m_entityAliasList); } bool Spawnable::EntityAliasVisitor::HasAliases() const @@ -235,7 +242,7 @@ namespace AzFramework m_dirty = true; } - void Spawnable::EntityAliasVisitor::ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback) + void Spawnable::EntityAliasVisitor::ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback) { AZ_Assert(m_entityAliasList, "Attempting to visit entity aliases on a spawnable that wasn't locked."); for (Spawnable::EntityAlias& alias : *m_entityAliasList) @@ -306,75 +313,92 @@ namespace AzFramework // aliases, for instance Networking can decide to disable certain aliases when running on a client. This in turn also requires // the aliases to be in their recorded order during building as the ebus handlers may depend on that order to determine what // entities need to be updated. - Spawnable::EntityAlias* compare = m_entityAliasList->begin(); - Spawnable::EntityAlias* it = m_entityAliasList->begin() + 1; + uint32_t previousIndex = AZStd::numeric_limits::max(); + Spawnable::EntityAliasType previousType = + static_cast(AZStd::numeric_limits>::max()); + Spawnable::EntityAlias* it = m_entityAliasList->begin(); Spawnable::EntityAlias* end = m_entityAliasList->end(); while (it < end) { + // If there's a switch to a new source index and the previous index only had an original it can + // be removed. + if (previousType == Spawnable::EntityAliasType::Original && previousIndex != it->m_sourceIndex) + { + it = m_entityAliasList->erase(it - 1); + end = m_entityAliasList->end(); + if (it == end) + { + break; + } + } + switch (it->m_aliasType) { case Spawnable::EntityAliasType::Original: - // If this is the only alias for the entity then the original can be removed. - { - Spawnable::EntityAlias* next = it + 1; - if (next == end || next->m_sourceIndex != it->m_sourceIndex) - { - // Erase instead of a swap-and-pop in order to preserver the order. - m_entityAliasList->erase(compare); - --end; - break; - } - } [[fallthrough]]; case Spawnable::EntityAliasType::Disabled: [[fallthrough]]; case Spawnable::EntityAliasType::Replace: // If the previous entry was a disabled, original or replace alias then remove it as it will be overwritten by the // current entry. - if (compare->m_sourceIndex == it->m_sourceIndex && - (compare->m_aliasType == Spawnable::EntityAliasType::Original || - compare->m_aliasType == Spawnable::EntityAliasType::Disabled || - compare->m_aliasType == Spawnable::EntityAliasType::Replace)) + if (previousIndex == it->m_sourceIndex && + (previousType == Spawnable::EntityAliasType::Original || + previousType == Spawnable::EntityAliasType::Disabled || + previousType == Spawnable::EntityAliasType::Replace)) { + previousIndex = it->m_sourceIndex; + previousType = it->m_aliasType; // Erase instead of a swap-and-pop in order to preserver the order. - m_entityAliasList->erase(compare); - --end; + it = m_entityAliasList->erase(it - 1) + 1; + end = m_entityAliasList->end(); } else { - ++compare; + previousIndex = it->m_sourceIndex; + previousType = it->m_aliasType; ++it; } break; case Spawnable::EntityAliasType::Additional: [[fallthrough]]; case Spawnable::EntityAliasType::Merge: - // If this is the first entry for this type insert an original in front of it so the spawnable entity manager - // does have to check for the case there's a merge and/or addition without a prefix. - if (compare->m_sourceIndex != it->m_sourceIndex) + // If this is the first entry for this index then insert an original in front of it so the spawnable entity manager + // doesn't have to check for the case there's a merge and/or addition without an entity to extend. + if (previousIndex != it->m_sourceIndex) { Spawnable::EntityAlias insert; // No load, as the asset is already loaded. - insert.m_spawnable = AZ::Data::Asset(&m_owner, AZ::Data::AssetLoadBehavior::NoLoad); + insert.m_spawnable = AZ::Data::Asset({}, azrtti_typeid()); insert.m_sourceIndex = it->m_sourceIndex; insert.m_targetIndex = it->m_sourceIndex; // Source index as the original entry for this slot is added. insert.m_aliasType = Spawnable::EntityAliasType::Original; - m_entityAliasList->insert(compare, AZStd::move(insert)); - compare += 2; + + previousIndex = it->m_sourceIndex; + previousType = it->m_aliasType; + + // Insert to maintain the order. + it = m_entityAliasList->insert(it, AZStd::move(insert)); it += 2; - ++end; + end = m_entityAliasList->end(); } else { - ++compare; + previousType = it->m_aliasType; ++it; } break; default: - AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", compare->m_aliasType); + AZ_Assert(false, "Invalid Spawnable entity alias type found during asset loading: %i", it->m_aliasType); break; } } + + // Check if the last entry is an "Original" in which case it can be removed. + if (!m_entityAliasList->empty() && m_entityAliasList->back().m_aliasType == Spawnable::EntityAliasType::Original) + { + m_entityAliasList->pop_back(); + } + // Reclaim memory because after this point the aliases will not change anymore. m_entityAliasList->shrink_to_fit(); m_dirty = false; @@ -395,7 +419,7 @@ namespace AzFramework Spawnable::EntityAliasConstVisitor::~EntityAliasConstVisitor() { - if (HasLock()) + if (IsSet()) { AZ_Assert( m_owner.m_shareState <= ShareState::Read, "Attempting to unlock a read shared spawnable that was not in a read shared mode (%i).", @@ -404,9 +428,9 @@ namespace AzFramework } } - bool Spawnable::EntityAliasConstVisitor::HasLock() const + bool Spawnable::EntityAliasConstVisitor::IsSet() const { - return EntityAliasVisitorBase::HasLock(m_entityAliasList); + return EntityAliasVisitorBase::IsSet(m_entityAliasList); } bool Spawnable::EntityAliasConstVisitor::HasAliases() const diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h index 4fb1f84b6f..0a35b81fb1 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/Spawnable.h @@ -71,7 +71,8 @@ namespace AzFramework class EntityAliasVisitorBase { protected: - bool HasLock(const EntityAliasList* aliases) const; + bool IsSet(const EntityAliasList* aliases) const; + bool HasAliases(const EntityAliasList* aliases) const; bool AreAllSpawnablesReady(const EntityAliasList* aliases) const; @@ -98,7 +99,9 @@ namespace AzFramework EntityAliasVisitor(const EntityAliasVisitor& rhs) = delete; EntityAliasVisitor& operator=(const EntityAliasVisitor& rhs) = delete; - bool HasLock() const; + //! Checks if the visitor was able to retrieve data. This needs to be checked before calling any other functions. + bool IsSet() const; + bool HasAliases() const; bool AreAllSpawnablesReady() const; @@ -118,8 +121,8 @@ namespace AzFramework Spawnable::EntityAliasType aliasType, bool queueLoad); - using ListSpawnablesPendingLoadCallback = AZStd::function& spawnablePendingLoad)>; - void ListSpawnablesPendingLoad(const ListSpawnablesPendingLoadCallback& callback); + using ListSpawnablesRequiringLoadCallback = AZStd::function& spawnablePendingLoad)>; + void ListSpawnablesRequiringLoad(const ListSpawnablesRequiringLoadCallback& callback); using UpdateCallback = AZStd::functionTryGetAliases(); - AZ_Assert(aliases.HasLock(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); + AZ_Assert(aliases.IsSet(), "Newly created Spawnable '%s' was already locked.", asset.GetHint().c_str()); if (aliases.HasAliases()) { AZ_Assert( @@ -119,7 +119,7 @@ namespace AzFramework &SpawnableAssetEvents::OnResolveAliases, aliases, spawnable->GetMetaData(), spawnable->GetEntities()); aliases.Optimize(); - aliases.ListSpawnablesPendingLoad( + aliases.ListSpawnablesRequiringLoad( [&assetLoadFilterCB, streamingDeadline, streamingPriority](AZ::Data::Asset& assetPendingLoad) { AZ::Data::AssetLoadParameters loadInfo; diff --git a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp index a018a55210..d7d3f59154 100644 --- a/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp +++ b/Code/Framework/AzFramework/AzFramework/Spawnable/SpawnableEntitiesManager.cpp @@ -342,9 +342,6 @@ namespace AzFramework return clone; case Spawnable::EntityAliasType::Merge: AZ_Assert(previouslySpawnedEntity != nullptr, "Merging components but there's no entity to add to yet."); - AZ_Assert( - previouslySpawnedEntity->GetId() == alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetId(), - "Entity ids for merging spawnables don't match."); AppendComponents( *previouslySpawnedEntity, alias.m_spawnable->GetEntities()[alias.m_targetIndex]->GetComponents(), templateToCloneMap, serializeContext); return nullptr; @@ -412,7 +409,7 @@ namespace AzFramework if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); - aliases.HasLock() && aliases.AreAllSpawnablesReady()) + aliases.IsSet() && aliases.AreAllSpawnablesReady()) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -475,9 +472,12 @@ namespace AzFramework AZ::Entity* clone = CloneSingleAliasedEntity( *entitiesToSpawn[i], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, *request.m_serializeContext); - previousEntity = clone; - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(i); + previousEntity = clone; + if (clone) + { + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(i); + } ++aliasIt; } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == i); } @@ -527,7 +527,7 @@ namespace AzFramework if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { if (Spawnable::EntityAliasConstVisitor aliases = ticket.m_spawnable->TryGetAliasesConst(); - aliases.HasLock() && aliases.AreAllSpawnablesReady()) + aliases.IsSet() && aliases.AreAllSpawnablesReady()) { AZStd::vector& spawnedEntities = ticket.m_spawnedEntities; AZStd::vector& spawnedEntityIndices = ticket.m_spawnedEntityIndices; @@ -593,7 +593,7 @@ namespace AzFramework return lhs.m_sourceIndex < rhs; }); - if (aliasIt == aliasEnd) + if (aliasIt == aliasEnd || aliasIt->m_sourceIndex != index) { spawnedEntities.emplace_back( CloneSingleEntity(*entitiesToSpawn[index], ticket.m_entityIdReferenceMap, *request.m_serializeContext)); @@ -610,8 +610,11 @@ namespace AzFramework *entitiesToSpawn[index], *aliasIt, ticket.m_entityIdReferenceMap, previousEntity, *request.m_serializeContext); previousEntity = clone; - spawnedEntities.emplace_back(clone); - spawnedEntityIndices.push_back(index); + if (clone) + { + spawnedEntities.emplace_back(clone); + spawnedEntityIndices.push_back(index); + } ++aliasIt; } while (aliasIt != aliasEnd && aliasIt->m_sourceIndex == index); @@ -816,7 +819,7 @@ namespace AzFramework Ticket& ticket = *request.m_ticket; if (ticket.m_spawnable.IsReady() && request.m_requestId == ticket.m_currentRequestId) { - if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.HasLock()) + if (Spawnable::EntityAliasVisitor aliases = ticket.m_spawnable->TryGetAliases(); aliases.IsSet()) { for (EntityAliasTypeChange& replacement : request.m_entityAliases) { @@ -918,7 +921,7 @@ namespace AzFramework if (request.m_checkAliasSpawnables) { if (Spawnable::EntityAliasConstVisitor visitor = ticket.m_spawnable->TryGetAliasesConst(); - !visitor.HasLock() || !visitor.AreAllSpawnablesReady()) + !visitor.IsSet() || !visitor.AreAllSpawnablesReady()) { return CommandResult::Requeue; } diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp index ff48f73769..38847edc07 100644 --- a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableEntitiesManagerTests.cpp @@ -55,6 +55,40 @@ namespace UnitTest AZ::EntityId m_entityReference; }; + class SourceSpawnableComponent : public AZ::Component + { + public: + AZ_COMPONENT(SourceSpawnableComponent, "{47FF79CE-A95B-420E-8BEB-F1CC58087B87}"); + + void Activate() override {} + void Deactivate() override {} + + static void Reflect(AZ::ReflectContext* reflection) + { + if (auto* serializeContext = azrtti_cast(reflection)) + { + serializeContext->Class(); + } + } + }; + + class TargetSpawnableComponent : public AZ::Component + { + public: + AZ_COMPONENT(TargetSpawnableComponent, "{B4041561-63A7-4E1E-80F1-78C08D497960}"); + + void Activate() override {} + void Deactivate() override {} + + static void Reflect(AZ::ReflectContext* reflection) + { + if (auto* serializeContext = azrtti_cast(reflection)) + { + serializeContext->Class(); + } + } + }; + class SpawnableEntitiesManagerTest : public AllocatorsFixture { public: @@ -66,6 +100,8 @@ namespace UnitTest AZ::ComponentApplication::Descriptor descriptor; m_application->Start(descriptor); m_application->RegisterComponentDescriptor(ComponentWithEntityReference::CreateDescriptor()); + m_application->RegisterComponentDescriptor(SourceSpawnableComponent::CreateDescriptor()); + m_application->RegisterComponentDescriptor(TargetSpawnableComponent::CreateDescriptor()); // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash @@ -109,7 +145,50 @@ namespace UnitTest entities.reserve(numElements); for (size_t i=0; i()); + auto entry = AZStd::make_unique(); + entry->AddComponent(aznew SourceSpawnableComponent()); + entities.push_back(AZStd::move(entry)); + } + } + + AZ::Data::Asset CreateTargetSpawnable(size_t numElements) + { + auto target = aznew AzFramework::Spawnable( + AZ::Data::AssetId(AZ::Uuid("{716CD8C3-0BA8-4F32-B579-0EC7C967796F}")), AZ::Data::AssetData::AssetStatus::Ready); + + AzFramework::Spawnable::EntityList& entities = target->GetEntities(); + entities.reserve(numElements); + for (size_t i = 0; i < numElements; ++i) + { + auto entry = AZStd::make_unique(); + entry->AddComponent(aznew TargetSpawnableComponent()); + entities.push_back(AZStd::move(entry)); + } + + return AZ::Data::Asset(target, AZ::Data::AssetLoadBehavior::NoLoad); + } + + template + void InsertEntityAliases( + const AZStd::array& sourceIds, + const AZStd::array& targetIds, + const AZStd::array& aliasTypes, + AZ::Data::Asset* target = nullptr) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + + for (uint32_t i = 0; i < AliasCount; ++i) + { + if (target) + { + visitor.AddAlias(*target, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false); + } + else + { + AZ::Data::Asset spawnable( + AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid()); + visitor.AddAlias(AZStd::move(spawnable), AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], false); + } } } @@ -245,6 +324,30 @@ namespace UnitTest TestApplication* m_application { nullptr }; }; + + // + // Constructors + // + + TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works) + { + AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset); + AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset); + + const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId(); + const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId(); + + AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1)); + EXPECT_TRUE(ticketMoveConstructor.IsValid()); + EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id); + + AzFramework::EntitySpawnTicket ticketMoveOperator; + ticketMoveOperator = AZStd::move(ticket2); + EXPECT_TRUE(ticketMoveOperator.IsValid()); + EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id); + } + + // // SpawnAllEntitities // @@ -366,24 +469,6 @@ namespace UnitTest } } - TEST_F(SpawnableEntitiesManagerTest, EntitySpawnTicket_Move_Works) - { - AzFramework::EntitySpawnTicket ticket1(*m_spawnableAsset); - AzFramework::EntitySpawnTicket ticket2(*m_spawnableAsset); - - const AzFramework::EntitySpawnTicket::Id ticket1Id = ticket1.GetId(); - const AzFramework::EntitySpawnTicket::Id ticket2Id = ticket2.GetId(); - - AzFramework::EntitySpawnTicket ticketMoveConstructor(AZStd::move(ticket1)); - EXPECT_TRUE(ticketMoveConstructor.IsValid()); - EXPECT_EQ(ticketMoveConstructor.GetId(), ticket1Id); - - AzFramework::EntitySpawnTicket ticketMoveOperator; - ticketMoveOperator = AZStd::move(ticket2); - EXPECT_TRUE(ticketMoveOperator.IsValid()); - EXPECT_EQ(ticketMoveOperator.GetId(), ticket2Id); - } - TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_DeleteTicketBeforeCall_NoCrash) { { @@ -393,6 +478,178 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithDisabled_NoEntitiesSpawned) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + InsertEntityAliases( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Disabled }); + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(0, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 8; + FillSpawnable(NumEntities); + InsertEntityAliases<2>({ 1, 3 }, { 1, 3 }, { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled }); + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(6, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace }, + &target); + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional }, + &target); + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + bool onSource = true; + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (onSource) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() == nullptr; + } + else + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + onSource = !onSource; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(8, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnAllEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, + Spawnable::EntityAliasType::Merge }, + &target); + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnAllEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnAllEntities(*m_ticket, AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } // // SpawnEntities @@ -754,6 +1011,190 @@ namespace UnitTest m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); } + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithDisabled_NoEntitiesSpawned) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + + InsertEntityAliases( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Disabled }); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(0, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_SomeAliasesWithDisabled_RegularEntitiesAreSpawned) + { + using namespace AzFramework; + FillSpawnable(8); + InsertEntityAliases<3>( + { 1, 3, 6 }, { 1, 3, 6 }, + { Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Disabled }); + + AZStd::vector indices = { 0, 2, 3, 1, 2, 3, 0, 1, 6, 4, 5, 7, 4, 1, 0, 6 }; + + size_t spawnedEntitiesCount = 0; + auto callback = [&spawnedEntitiesCount](AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(9, spawnedEntitiesCount); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithReplace_EntitiesSpawnedFromTarget) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace }, + &target); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithAdditional_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional }, + &target); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + bool onSource = true; + for (const AZ::Entity* entity : entities) + { + if (entity) + { + if (onSource) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() == nullptr; + } + else + { + allReplaced = allReplaced && entity->FindComponent() == nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + onSource = !onSource; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(8, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } + + TEST_F(SpawnableEntitiesManagerTest, SpawnEntities_AllAliasesWithMerge_SourceAndTargetComponentsMerged) + { + using namespace AzFramework; + static constexpr size_t NumEntities = 4; + FillSpawnable(NumEntities); + AZ::Data::Asset target = CreateTargetSpawnable(4); + InsertEntityAliases<4>( + { 0, 1, 2, 3 }, { 0, 1, 2, 3 }, + { Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Merge, + Spawnable::EntityAliasType::Merge }, + &target); + + AZStd::vector indices = { 0, 2, 3, 1 }; + + size_t spawnedEntitiesCount = 0; + bool allReplaced = true; + auto callback = [&spawnedEntitiesCount, &allReplaced]( + AzFramework::EntitySpawnTicket::Id, AzFramework::SpawnableConstEntityContainerView entities) + { + spawnedEntitiesCount += entities.size(); + for (const AZ::Entity* entity : entities) + { + if (entity) + { + allReplaced = allReplaced && entity->FindComponent() != nullptr; + allReplaced = allReplaced && entity->FindComponent() != nullptr; + } + else + { + allReplaced = false; + } + } + }; + AzFramework::SpawnEntitiesOptionalArgs optionalArgs; + optionalArgs.m_completionCallback = AZStd::move(callback); + m_manager->SpawnEntities(*m_ticket, AZStd::move(indices), AZStd::move(optionalArgs)); + m_manager->ProcessQueue(AzFramework::SpawnableEntitiesManager::CommandQueuePriority::Regular); + + EXPECT_EQ(4, spawnedEntitiesCount); + EXPECT_TRUE(allReplaced); + } // // DespawnAllEntities diff --git a/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp new file mode 100644 index 0000000000..f7d6d5190f --- /dev/null +++ b/Code/Framework/AzFramework/Tests/Spawnable/SpawnableTests.cpp @@ -0,0 +1,499 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace UnitTest +{ + class SpawnableTest : public AllocatorsFixture + { + public: + void SetUp() override + { + AllocatorsFixture::SetUp(); + + m_spawnable = aznew AzFramework::Spawnable(); + } + + void TearDown() override + { + delete m_spawnable; + m_spawnable = nullptr; + + AllocatorsFixture::TearDown(); + } + + void InsertEightEntities() + { + AzFramework::Spawnable::EntityList& entities = m_spawnable->GetEntities(); + entities.reserve(entities.size() + 8); + for (size_t i = 0; i < 8; ++i) + { + entities.emplace_back(AZStd::make_unique()); + } + } + + void InsertEightEntityAliases( + const AZStd::array& sourceIds, + const AZStd::array& targetIds, + const AZStd::array& aliasTypes, + bool queueLoad = false) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + + for (uint32_t i = 0; i < 8; ++i) + { + AZ::Data::Asset spawnable( + AZ::Data::AssetId(AZ::Uuid("{4CBEC17A-52D6-42D5-9037-F4C05B9CE1D9}"), i), azrtti_typeid()); + visitor.AddAlias(spawnable, AZ::Crc32(i), sourceIds[i], targetIds[i], aliasTypes[i], queueLoad); + } + } + + void InsertEightEntityAliases(bool queueLoad) + { + using namespace AzFramework; + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }, + queueLoad); + } + + void InsertEightEntityAliases() + { + InsertEightEntityAliases(false); + } + + protected: + AzFramework::Spawnable* m_spawnable; + }; + + + // + // TryGetAliasesConst + // + + TEST_F(SpawnableTest, TryGetAliasesConst_GetVisitor_VisitorDataIsAvailable) + { + AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); + EXPECT_TRUE(visitor.IsSet()); + } + + TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsNotReadShared_VisitorDataIsNotAvailable) + { + AzFramework::Spawnable::EntityAliasVisitor readWriteVisitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(readWriteVisitor.IsSet()); + + AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); + EXPECT_FALSE(visitor.IsSet()); + } + + TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyReadShared_VisitorDataIsAvailable) + { + AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst(); + ASSERT_TRUE(readVisitor.IsSet()); + + AzFramework::Spawnable::EntityAliasConstVisitor visitor = m_spawnable->TryGetAliasesConst(); + EXPECT_TRUE(visitor.IsSet()); + } + + + // + // TryGetAliases + // + + TEST_F(SpawnableTest, TryGetAliases_GetVisitor_VisitorDataIsAvailable) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + EXPECT_TRUE(visitor.IsSet()); + } + + TEST_F(SpawnableTest, TryGetAliasesConst_VisitorThatIsAlreadyShared_VisitorDataNotIsAvailable) + { + AzFramework::Spawnable::EntityAliasConstVisitor readVisitor = m_spawnable->TryGetAliasesConst(); + ASSERT_TRUE(readVisitor.IsSet()); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + EXPECT_FALSE(visitor.IsSet()); + } + + + // + // EntityAliasVisitor + // + + + // + // HasAliases + // + + TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_EmptyAliasList_ReturnsFalse) + { + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_FALSE(visitor.HasAliases()); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_HasAliases_FilledInAliasList_ReturnsTue) + { + InsertEightEntities(); + InsertEightEntityAliases(); + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_TRUE(visitor.HasAliases()); + } + + + // + // Optimize + // + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_SortEntityAliases_AliasesAreSortedBySourceAndTargetId) + { + InsertEightEntities(); + InsertEightEntityAliases(); + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + // Optimize doesn't need to be explicitly called because the setup of the aliases will cause the alias list to be sorted and optimized. + + uint32_t sourceIndex = 0; + uint32_t targetIndex = 0; + for (const AzFramework::Spawnable::EntityAlias& alias : visitor) + { + if (alias.m_sourceIndex != sourceIndex) + { + ASSERT_LE(sourceIndex, alias.m_sourceIndex); + } + else + { + ASSERT_LE(targetIndex, alias.m_targetIndex); + } + sourceIndex = alias.m_sourceIndex; + targetIndex = alias.m_targetIndex; + } + } + + TEST_F( + SpawnableTest, EntityAliasVisitor_Optimize_RemoveUnused_OnlySecondToLastAliasRemains) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(1, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()->m_aliasType); + EXPECT_EQ(6, visitor.begin()->m_targetIndex); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_AddAdditional_ThreeAdditionalAliasesAreAdded) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 0, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional, + Spawnable::EntityAliasType::Additional, Spawnable::EntityAliasType::Additional }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(11, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[5].m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[7].m_aliasType); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_OriginalsOnly_AliasListIsEmpty) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 0, 0, 0, 0, 0 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MixedOriginals_AllOriginalsRemoved) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 0, 1, 1, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Disabled, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(2, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Disabled, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[1].m_aliasType); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_Optimize_MergeAfterOriginal_NoAdditionalOriginalIsInserted) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 0, 1, 1, 2, 2, 2, 2 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Merge, Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original, + Spawnable::EntityAliasType::Original, Spawnable::EntityAliasType::Original }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_EQ(4, AZStd::distance(visitor.begin(), visitor.end())); + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()->m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[1].m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Replace, visitor.begin()[2].m_aliasType); + EXPECT_EQ(Spawnable::EntityAliasType::Merge, visitor.begin()[3].m_aliasType); + } + + + // + // UpdateAliasType + // + + TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliasType_AllToOriginal_NoAliasesAfterOptimization) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + for (uint32_t i = 0; i < 8; ++i) + { + visitor.UpdateAliasType(i, Spawnable::EntityAliasType::Original); + } + + for (const Spawnable::EntityAlias& alias : visitor) + { + EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType); + } + + visitor.Optimize(); + + EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); + } + + + // + // UpdateAliases + // + + TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_AllToOriginal_NoAliasesAfterOptimization) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + auto callback = + [](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, const AZ::Data::Asset& /*aliasedSpawnable*/, + const AZ::Crc32 /*tag*/, const uint32_t /*sourceIndex*/, const uint32_t /*targetIndex*/) + { + aliasType = Spawnable::EntityAliasType::Original; + }; + visitor.UpdateAliases(AZStd::move(callback)); + + for (const Spawnable::EntityAlias& alias : visitor) + { + EXPECT_EQ(Spawnable::EntityAliasType::Original, alias.m_aliasType); + } + + visitor.Optimize(); + + EXPECT_EQ(0, AZStd::distance(visitor.begin(), visitor.end())); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_UpdateAliases_FilterByTag_OnlyOneAliasUpdated) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases( + { 0, 1, 2, 3, 4, 5, 6, 7 }, { 0, 1, 2, 3, 4, 5, 6, 7 }, + { Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace, + Spawnable::EntityAliasType::Replace, Spawnable::EntityAliasType::Replace }); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + bool correctTag = false; + size_t numberOfUpdates = 0; + auto callback = [&correctTag, &numberOfUpdates](Spawnable::EntityAliasType& aliasType, bool& /*queueLoad*/, + const AZ::Data::Asset& /*aliasedSpawnable*/, const AZ::Crc32 tag, const uint32_t /*sourceIndex*/, + const uint32_t /*targetIndex*/) + { + correctTag = (tag == AZ::Crc32(3)); + numberOfUpdates++; + aliasType = Spawnable::EntityAliasType::Original; + }; + visitor.UpdateAliases(AZ::Crc32(3), AZStd::move(callback)); + + EXPECT_EQ(Spawnable::EntityAliasType::Original, visitor.begin()[3].m_aliasType); + } + + + // + // AreAllSpawnablesReady + // + + TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeLoadedAssets_ReturnsTrue) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_TRUE(visitor.AreAllSpawnablesReady()); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_AreAllSpawnablesReady_CheckFakeNotLoadedAssets_ReturnsFalse) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(true); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + EXPECT_FALSE(visitor.AreAllSpawnablesReady()); + } + + + // + // ListTargetSpawnables + // + + TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListAllTargetAssets_AllTargetsListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + bool correctAssets = true; + auto callback = [&count, &correctAssets](const AZ::Data::Asset& targetSpawnable) + { + correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count); + count++; + }; + visitor.ListTargetSpawnables(callback); + + EXPECT_EQ(8, count); + EXPECT_TRUE(correctAssets); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_ListTargetSpawnables_ListTaggedTargetAssets_OneAssetListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + bool correctAsset = false; + auto callback = [&count, &correctAsset](const AZ::Data::Asset& targetSpawnable) + { + correctAsset = (targetSpawnable.GetId().m_subId == 3); + count++; + }; + visitor.ListTargetSpawnables(AZ::Crc32(3), callback); + + EXPECT_EQ(1, count); + EXPECT_TRUE(correctAsset); + } + + + // + // ListSpawnablesRequiringLoad + // + + TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToLoaded_AllTargetsListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(true); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + bool correctAssets = true; + auto callback = [&count, &correctAssets](const AZ::Data::Asset& targetSpawnable) + { + correctAssets = correctAssets && (targetSpawnable.GetId().m_subId == count); + count++; + }; + visitor.ListSpawnablesRequiringLoad(callback); + + EXPECT_EQ(8, count); + EXPECT_TRUE(correctAssets); + } + + TEST_F(SpawnableTest, EntityAliasVisitor_ListSpawnablesRequiringLoad_AllSetToNotLoaded_NoTargetsListed) + { + using namespace AzFramework; + InsertEightEntities(); + InsertEightEntityAliases(false); + + AzFramework::Spawnable::EntityAliasVisitor visitor = m_spawnable->TryGetAliases(); + ASSERT_TRUE(visitor.IsSet()); + + size_t count = 0; + auto callback = [&count](const AZ::Data::Asset& /*targetSpawnable*/) + { + count++; + }; + visitor.ListSpawnablesRequiringLoad(callback); + + EXPECT_EQ(0, count); + } +} // namespace UnitTest diff --git a/Code/Framework/AzFramework/Tests/frameworktests_files.cmake b/Code/Framework/AzFramework/Tests/frameworktests_files.cmake index 6c4f611352..e4877e34a9 100644 --- a/Code/Framework/AzFramework/Tests/frameworktests_files.cmake +++ b/Code/Framework/AzFramework/Tests/frameworktests_files.cmake @@ -10,6 +10,7 @@ set(FILES Main.cpp Spawnable/SpawnableEntitiesInterfaceTests.cpp Spawnable/SpawnableEntitiesManagerTests.cpp + Spawnable/SpawnableTests.cpp ArchiveCompressionTests.cpp ArchiveTests.cpp BehaviorEntityTests.cpp diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp index e12ddb616f..21fa3db52b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.cpp @@ -220,7 +220,7 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils if (it == aliasVisitors.end()) { AzFramework::Spawnable::EntityAliasVisitor visitor = source->m_spawnable.TryGetAliases(); - AZ_Assert(visitor.HasLock(), "Unable to obtain lock for a newly create spawnable."); + AZ_Assert(visitor.IsSet(), "Unable to obtain lock for a newly create spawnable."); it = aliasVisitors.emplace(source->m_spawnable.GetId(), AZStd::move(visitor)).first; } it->second.AddAlias( diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h index 7fc01367b2..c937b974e9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/Spawnable/PrefabProcessorContext.h @@ -42,7 +42,8 @@ namespace AzToolsFramework::Prefab::PrefabConversionUtils { NoLoad, //!< Don't load the spawnable referenced in the entity alias. Loading will be up to the caller. QueueLoad, //!< Queue the spawnable referenced in the entity alias for loading. This will be an async load because asset - //!< handlers aren't allowed to start a blocking load as this can lead to deadlocks. + //!< handlers aren't allowed to start a blocking load as this can lead to deadlocks. This option will allow + //!< to disable loading the referenced spawnable through the event fired from the spawnables asset handler. DependentLoad //!< The spawnable referenced in the entity alias is made a dependency of the spawnable that holds the entity //!< alias. This will cause the spawnable to be automatically loaded along with the owning spawnable. };