Merge branch 'main' of https://github.com/aws-lumberyard/o3de into Spawnable/Instantiation/EntityIdReferenceFix
This commit is contained in:
@@ -115,7 +115,6 @@
|
||||
*.wav filter=lfs diff=lfs merge=lfs -text
|
||||
*.webm filter=lfs diff=lfs merge=lfs -text
|
||||
*.wem filter=lfs diff=lfs merge=lfs -text
|
||||
*.wxs filter=lfs diff=lfs merge=lfs -text
|
||||
*.zip filter=lfs diff=lfs merge=lfs -text
|
||||
*.tbscene filter=lfs diff=lfs merge=lfs -text
|
||||
*.spp filter=lfs diff=lfs merge=lfs -text
|
||||
|
||||
@@ -84,6 +84,7 @@ namespace AzFramework
|
||||
};
|
||||
|
||||
using EntitySpawnCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
|
||||
using EntityPreInsertionCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableEntityContainerView)>;
|
||||
using EntityDespawnCallback = AZStd::function<void(EntitySpawnTicket&)>;
|
||||
using ReloadSpawnableCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
|
||||
using ListEntitiesCallback = AZStd::function<void(EntitySpawnTicket&, SpawnableConstEntityContainerView)>;
|
||||
@@ -110,7 +111,8 @@ namespace AzFramework
|
||||
//! @param completionCallback Optional callback that's called when spawning entities has completed. This can be called from
|
||||
//! a different thread than the one that made the function call. The returned list of entities contains all the newly
|
||||
//! created entities.
|
||||
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback = {}) = 0;
|
||||
virtual void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {},
|
||||
EntitySpawnCallback completionCallback = {}) = 0;
|
||||
//! Spawn instances of some entities in the spawnable.
|
||||
//! @param ticket Stores the results of the call. Use this ticket to spawn additional entities or to despawn them.
|
||||
//! @param entityIndices The indices into the template entities stored in the spawnable that will be used to spawn entities from.
|
||||
@@ -118,7 +120,7 @@ namespace AzFramework
|
||||
//! a different thread than the one that made this function call. The returned list of entities contains all the newly
|
||||
//! created entities.
|
||||
virtual void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
|
||||
EntitySpawnCallback completionCallback = {}) = 0;
|
||||
EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) = 0;
|
||||
//! Removes all entities in the provided list from the environment.
|
||||
//! @param ticket The ticket previously used to spawn entities with.
|
||||
//! @param completionCallback Optional callback that's called when despawning entities has completed. This can be called from
|
||||
|
||||
@@ -15,17 +15,20 @@
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <AzCore/std/parallel/scoped_lock.h>
|
||||
#include <AzCore/std/smart_ptr/make_shared.h>
|
||||
#include <AzFramework/Components/TransformComponent.h>
|
||||
#include <AzFramework/Entity/GameEntityContextBus.h>
|
||||
#include <AzFramework/Spawnable/Spawnable.h>
|
||||
#include <AzFramework/Spawnable/SpawnableEntitiesManager.h>
|
||||
|
||||
namespace AzFramework
|
||||
{
|
||||
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback)
|
||||
void SpawnableEntitiesManager::SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback,
|
||||
EntitySpawnCallback completionCallback)
|
||||
{
|
||||
SpawnAllEntitiesCommand queueEntry;
|
||||
queueEntry.m_ticket = &ticket;
|
||||
queueEntry.m_completionCallback = AZStd::move(completionCallback);
|
||||
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
|
||||
{
|
||||
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
|
||||
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
|
||||
@@ -33,13 +36,15 @@ namespace AzFramework
|
||||
}
|
||||
}
|
||||
|
||||
void SpawnableEntitiesManager::SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
|
||||
EntitySpawnCallback completionCallback)
|
||||
void SpawnableEntitiesManager::SpawnEntities(
|
||||
EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
|
||||
EntityPreInsertionCallback preInsertionCallback, EntitySpawnCallback completionCallback)
|
||||
{
|
||||
SpawnEntitiesCommand queueEntry;
|
||||
queueEntry.m_ticket = &ticket;
|
||||
queueEntry.m_entityIndices = AZStd::move(entityIndices);
|
||||
queueEntry.m_completionCallback = AZStd::move(completionCallback);
|
||||
queueEntry.m_preInsertionCallback = AZStd::move(preInsertionCallback);
|
||||
{
|
||||
AZStd::scoped_lock queueLock(m_pendingRequestQueueMutex);
|
||||
queueEntry.m_ticketId = GetTicketPayload<Ticket>(ticket).m_nextTicketId++;
|
||||
@@ -201,13 +206,12 @@ namespace AzFramework
|
||||
}
|
||||
}
|
||||
|
||||
AZ::Entity* SpawnableEntitiesManager::SpawnSingleEntity(const AZ::Entity& entityTemplate, EntityIdMap& spawnableToInstanceEntityIdMap,
|
||||
AZ::SerializeContext& serializeContext)
|
||||
AZ::Entity* SpawnableEntitiesManager::SpawnSingleEntity(const AZ::Entity& entityTemplate, AZ::SerializeContext& serializeContext)
|
||||
{
|
||||
AZ::Entity* clone = AZ::IdUtils::Remapper<AZ::EntityId>::CloneObjectAndGenerateNewIdsAndFixRefs(
|
||||
&entityTemplate, spawnableToInstanceEntityIdMap, &serializeContext);
|
||||
|
||||
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
clone->SetId(AZ::Entity::MakeId());
|
||||
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, clone);
|
||||
return clone;
|
||||
}
|
||||
@@ -217,26 +221,79 @@ namespace AzFramework
|
||||
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
|
||||
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
|
||||
{
|
||||
size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size();
|
||||
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
|
||||
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
|
||||
|
||||
const Spawnable::EntityList& entities = ticket.m_spawnable->GetEntities();
|
||||
EntityIdMap& spawnableToInstanceEntityIdMap = ticket.m_spawnableToInstanceEntityIdMap;
|
||||
// Keep track how many entities there were in the array initially
|
||||
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
|
||||
|
||||
size_t entitiesSize = entities.size();
|
||||
ticket.m_spawnedEntities.reserve(ticket.m_spawnedEntities.size() + entitiesSize);
|
||||
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesSize);
|
||||
spawnableToInstanceEntityIdMap.reserve(entitiesSize);
|
||||
// These are 'template' entities we'll be cloning from
|
||||
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
|
||||
size_t entitiesToSpawnSize = entitiesToSpawn.size();
|
||||
|
||||
for(size_t i=0; i<entitiesSize; ++i)
|
||||
// Reserve buffers
|
||||
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
|
||||
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesToSpawnSize);
|
||||
|
||||
// TEMP: To be replaced by IdUtils::Remapper
|
||||
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
|
||||
EntityIdMap templateToCloneIdMap;
|
||||
// \TEMP
|
||||
|
||||
// Clone the entities from Spawnable
|
||||
for (size_t i = 0; i < entitiesToSpawnSize; ++i)
|
||||
{
|
||||
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], spawnableToInstanceEntityIdMap, serializeContext));
|
||||
ticket.m_spawnedEntityIndices.push_back(i);
|
||||
const AZ::Entity& entityTemplate = *entitiesToSpawn[i];
|
||||
|
||||
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
clone->SetId(AZ::Entity::MakeId());
|
||||
|
||||
spawnedEntities.push_back(clone);
|
||||
spawnedEntityIndices.push_back(i);
|
||||
|
||||
// TEMP: To be replaced by IdUtils::Remapper
|
||||
templateToCloneIdMap[entityTemplate.GetId()] = clone->GetId();
|
||||
|
||||
// Update TransformComponent parent Id. It is guaranteed for the entities array to be sorted from parent->child here.
|
||||
auto* transformComponent = clone->FindComponent<AzFramework::TransformComponent>();
|
||||
AZ::EntityId parentId = transformComponent->GetParentId();
|
||||
if (parentId.IsValid())
|
||||
{
|
||||
auto it = templateToCloneIdMap.find(parentId);
|
||||
if (it != templateToCloneIdMap.end())
|
||||
{
|
||||
transformComponent->SetParentRelative(it->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Warning(
|
||||
"SpawnableEntitiesManager", false, "Entity %s doesn't have the parent entity %s present in the spawnable",
|
||||
clone->GetName().c_str(), parentId.ToString().data());
|
||||
}
|
||||
}
|
||||
// \TEMP
|
||||
}
|
||||
|
||||
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
|
||||
if (request.m_preInsertionCallback)
|
||||
{
|
||||
request.m_preInsertionCallback(*request.m_ticket, SpawnableEntityContainerView(
|
||||
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
|
||||
}
|
||||
|
||||
// Add to the game context, now the entities are active
|
||||
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
|
||||
[](AZ::Entity* entity)
|
||||
{
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
|
||||
});
|
||||
|
||||
// Let other systems know about newly spawned entities for any post-processing after adding to the scene/game context.
|
||||
if (request.m_completionCallback)
|
||||
{
|
||||
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
|
||||
ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end()));
|
||||
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
|
||||
}
|
||||
|
||||
m_onSpawnedEvent.Signal(ticket.m_spawnable);
|
||||
@@ -255,27 +312,56 @@ namespace AzFramework
|
||||
Ticket& ticket = GetTicketPayload<Ticket>(*request.m_ticket);
|
||||
if (ticket.m_spawnable.IsReady() && request.m_ticketId == ticket.m_currentTicketId)
|
||||
{
|
||||
size_t spawnedEntitiesCount = ticket.m_spawnedEntities.size();
|
||||
AZStd::vector<AZ::Entity*>& spawnedEntities = ticket.m_spawnedEntities;
|
||||
AZStd::vector<size_t>& spawnedEntityIndices = ticket.m_spawnedEntityIndices;
|
||||
|
||||
const Spawnable::EntityList& entities = ticket.m_spawnable->GetEntities();
|
||||
EntityIdMap& spawnableToInstanceEntityIdMap = ticket.m_spawnableToInstanceEntityIdMap;
|
||||
// Keep track how many entities there were in the array initially
|
||||
size_t spawnedEntitiesInitialCount = spawnedEntities.size();
|
||||
|
||||
size_t entitiesSize = entities.size();
|
||||
ticket.m_spawnedEntities.reserve(ticket.m_spawnedEntities.size() + entitiesSize);
|
||||
ticket.m_spawnedEntityIndices.reserve(ticket.m_spawnedEntityIndices.size() + entitiesSize);
|
||||
spawnableToInstanceEntityIdMap.reserve(entitiesSize);
|
||||
// These are 'template' entities we'll be cloning from
|
||||
const Spawnable::EntityList& entitiesToSpawn = ticket.m_spawnable->GetEntities();
|
||||
size_t entitiesToSpawnSize = request.m_entityIndices.size();
|
||||
|
||||
spawnedEntities.reserve(spawnedEntities.size() + entitiesToSpawnSize);
|
||||
spawnedEntityIndices.reserve(spawnedEntityIndices.size() + entitiesToSpawnSize);
|
||||
|
||||
for (size_t index : request.m_entityIndices)
|
||||
{
|
||||
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[index], spawnableToInstanceEntityIdMap, serializeContext));
|
||||
ticket.m_spawnedEntityIndices.push_back(index);
|
||||
if (index < entitiesToSpawn.size())
|
||||
{
|
||||
const AZ::Entity& entityTemplate = *entitiesToSpawn[index];
|
||||
|
||||
AZ::Entity* clone = serializeContext.CloneObject(&entityTemplate);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
clone->SetId(AZ::Entity::MakeId());
|
||||
|
||||
spawnedEntities.push_back(clone);
|
||||
spawnedEntityIndices.push_back(index);
|
||||
|
||||
}
|
||||
}
|
||||
ticket.m_loadAll = false;
|
||||
|
||||
// Let other systems know about newly spawned entities for any pre-processing before adding to the scene/game context.
|
||||
if (request.m_preInsertionCallback)
|
||||
{
|
||||
request.m_preInsertionCallback(
|
||||
*request.m_ticket,
|
||||
SpawnableEntityContainerView(
|
||||
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
|
||||
}
|
||||
|
||||
// Add to the game context, now the entities are active
|
||||
AZStd::for_each(ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end(),
|
||||
[](AZ::Entity* entity)
|
||||
{
|
||||
GameEntityContextRequestBus::Broadcast(&GameEntityContextRequestBus::Events::AddGameEntity, entity);
|
||||
});
|
||||
|
||||
if (request.m_completionCallback)
|
||||
{
|
||||
request.m_completionCallback(*request.m_ticket, SpawnableConstEntityContainerView(
|
||||
ticket.m_spawnedEntities.begin() + spawnedEntitiesCount, ticket.m_spawnedEntities.end()));
|
||||
ticket.m_spawnedEntities.begin() + spawnedEntitiesInitialCount, ticket.m_spawnedEntities.end()));
|
||||
}
|
||||
|
||||
m_onSpawnedEvent.Signal(ticket.m_spawnable);
|
||||
@@ -346,8 +432,6 @@ namespace AzFramework
|
||||
// Rebuild the list of entities.
|
||||
ticket.m_spawnedEntities.clear();
|
||||
const Spawnable::EntityList& entities = request.m_spawnable->GetEntities();
|
||||
EntityIdMap& spawnableToInstanceEntityIdMap = ticket.m_spawnableToInstanceEntityIdMap;
|
||||
|
||||
if (ticket.m_loadAll)
|
||||
{
|
||||
// The new spawnable may have a different number of entities and since the intent of the user was
|
||||
@@ -357,9 +441,7 @@ namespace AzFramework
|
||||
size_t entitiesSize = entities.size();
|
||||
for (size_t i = 0; i < entitiesSize; ++i)
|
||||
{
|
||||
ticket.m_spawnedEntities.push_back(
|
||||
SpawnSingleEntity(*entities[i], spawnableToInstanceEntityIdMap, serializeContext));
|
||||
|
||||
ticket.m_spawnedEntities.push_back(SpawnSingleEntity(*entities[i], serializeContext));
|
||||
ticket.m_spawnedEntityIndices.push_back(i);
|
||||
}
|
||||
}
|
||||
@@ -369,8 +451,7 @@ namespace AzFramework
|
||||
for (size_t index : ticket.m_spawnedEntityIndices)
|
||||
{
|
||||
ticket.m_spawnedEntities.push_back(
|
||||
index < entitiesSize ?
|
||||
SpawnSingleEntity(*entities[index], spawnableToInstanceEntityIdMap, serializeContext) : nullptr);
|
||||
index < entitiesSize ? SpawnSingleEntity(*entities[index], serializeContext) : nullptr);
|
||||
}
|
||||
}
|
||||
ticket.m_spawnable = AZStd::move(request.m_spawnable);
|
||||
|
||||
@@ -49,8 +49,8 @@ namespace AzFramework
|
||||
// The following functions are thread safe
|
||||
//
|
||||
|
||||
void SpawnAllEntities(EntitySpawnTicket& ticket, EntitySpawnCallback completionCallback = {}) override;
|
||||
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices,
|
||||
void SpawnAllEntities(EntitySpawnTicket& ticket, EntityPreInsertionCallback preInsertionCallback = {}, EntitySpawnCallback completionCallback = {}) override;
|
||||
void SpawnEntities(EntitySpawnTicket& ticket, AZStd::vector<size_t> entityIndices, EntityPreInsertionCallback preInsertionCallback = {},
|
||||
EntitySpawnCallback completionCallback = {}) override;
|
||||
void DespawnAllEntities(EntitySpawnTicket& ticket, EntityDespawnCallback completionCallback = {}) override;
|
||||
|
||||
@@ -94,6 +94,7 @@ namespace AzFramework
|
||||
struct SpawnAllEntitiesCommand
|
||||
{
|
||||
EntitySpawnCallback m_completionCallback;
|
||||
EntityPreInsertionCallback m_preInsertionCallback;
|
||||
EntitySpawnTicket* m_ticket;
|
||||
uint32_t m_ticketId;
|
||||
};
|
||||
@@ -101,6 +102,7 @@ namespace AzFramework
|
||||
{
|
||||
AZStd::vector<size_t> m_entityIndices;
|
||||
EntitySpawnCallback m_completionCallback;
|
||||
EntityPreInsertionCallback m_preInsertionCallback;
|
||||
EntitySpawnTicket* m_ticket;
|
||||
uint32_t m_ticketId;
|
||||
};
|
||||
|
||||
+6
-2
@@ -134,9 +134,13 @@ namespace AzToolsFramework
|
||||
}
|
||||
}
|
||||
|
||||
auto findInstancesResult = m_templateInstanceMapperInterface->FindInstancesOwnedByTemplate(instanceTemplateId)->get();
|
||||
auto findInstancesResult = m_templateInstanceMapperInterface->FindInstancesOwnedByTemplate(instanceTemplateId);
|
||||
AZ_Assert(
|
||||
findInstancesResult.has_value(), "Prefab Instances corresponding to template with id %llu couldn't be found.",
|
||||
instanceTemplateId);
|
||||
|
||||
if (findInstancesResult.find(instanceToUpdate) == findInstancesResult.end())
|
||||
if (findInstancesResult == AZStd::nullopt ||
|
||||
findInstancesResult->get().find(instanceToUpdate) == findInstancesResult->get().end())
|
||||
{
|
||||
// Since nested instances get reconstructed during propagation, remove any nested instance that no longer
|
||||
// maps to a template.
|
||||
|
||||
@@ -182,16 +182,16 @@ namespace AzToolsFramework
|
||||
else
|
||||
{
|
||||
AZ::JsonSerializationResult::ResultCode applyPatchResult = AZ::JsonSerialization::ApplyPatch(
|
||||
linkedInstanceDom,
|
||||
sourceTemplateDomCopy,
|
||||
targetTemplatePrefabDom.GetAllocator(),
|
||||
sourceTemplatePrefabDom,
|
||||
patchesReference->get(),
|
||||
AZ::JsonMergeApproach::JsonPatch);
|
||||
linkedInstanceDom.CopyFrom(sourceTemplateDomCopy, targetTemplatePrefabDom.GetAllocator());
|
||||
if (applyPatchResult.GetProcessing() != AZ::JsonSerializationResult::Processing::Completed)
|
||||
{
|
||||
AZ_Error("Prefab", false,
|
||||
"Link::UpdateTarget - "
|
||||
"ApplyPatches failed for Prefab DOM from source Template '%u' and target Template '%u'.",
|
||||
AZ_Error(
|
||||
"Prefab", false,
|
||||
"Link::UpdateTarget - ApplyPatches failed for Prefab DOM from source Template '%u' and target Template '%u'.",
|
||||
m_sourceTemplateId, m_targetTemplateId);
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -122,11 +122,14 @@ namespace AzToolsFramework
|
||||
|
||||
AZ::EntityId containerEntityId = instanceToCreate->get().GetContainerEntityId();
|
||||
|
||||
// Parent the entities to the container entity. Parenting the container entities of the instances passed to createPrefab
|
||||
// will be done during the creation of links below.
|
||||
for (AZ::Entity* topLevelEntity : entities)
|
||||
// Parent the non-container top level entities to the container entity.
|
||||
// Parenting the top level container entities will be done during the creation of links.
|
||||
for (AZ::Entity* topLevelEntity : topLevelEntities)
|
||||
{
|
||||
AZ::TransformBus::Event(topLevelEntity->GetId(), &AZ::TransformBus::Events::SetParent, containerEntityId);
|
||||
if (!IsInstanceContainerEntity(topLevelEntity->GetId()))
|
||||
{
|
||||
AZ::TransformBus::Event(topLevelEntity->GetId(), &AZ::TransformBus::Events::SetParent, containerEntityId);
|
||||
}
|
||||
}
|
||||
|
||||
// Update the template of the instance since the entities are modified since the template creation.
|
||||
@@ -142,11 +145,25 @@ namespace AzToolsFramework
|
||||
AZ_Assert(
|
||||
nestedInstanceContainerEntity, "Invalid container entity found for the nested instance used in prefab creation.");
|
||||
|
||||
AZ::EntityId parentId;
|
||||
AZ::TransformBus::EventResult(
|
||||
parentId, nestedInstanceContainerEntity->get().GetId(), &AZ::TransformBus::Events::GetParentId);
|
||||
|
||||
auto entityIterator = AZStd::find_if(
|
||||
entities.begin(), entities.end(), [parentId](AZ::Entity* entity) { return entity->GetId() == parentId; });
|
||||
|
||||
// If the previous parent entity of the nested instance is not part of the entities of the newly created prefab,
|
||||
// then set the parent of the nested prefab as the container entity of the newly created prefab.
|
||||
if (entityIterator == entities.end())
|
||||
{
|
||||
parentId = containerEntityId;
|
||||
}
|
||||
|
||||
// These link creations shouldn't be undone because that would put the template in a non-usable state if a user
|
||||
// chooses to instantiate the template after undoing the creation.
|
||||
CreateLink(
|
||||
{&nestedInstanceContainerEntity->get()}, *nestedInstance, instanceToCreate->get().GetTemplateId(),
|
||||
undoBatch.GetUndoBatch(), containerEntityId, false);
|
||||
undoBatch.GetUndoBatch(), parentId, false);
|
||||
});
|
||||
|
||||
// Create a link between the templates of the newly created instance and the instance it's being parented under.
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
|
||||
#include <MorphTargets/MorphTargetComputePass.h>
|
||||
#include <SkinnedMesh/SkinnedMeshFeatureProcessor.h>
|
||||
#include <Atom/Feature/SkinnedMesh/SkinnedMeshOutputStreamManagerInterface.h>
|
||||
|
||||
#include <Atom/RPI.Public/Shader/Shader.h>
|
||||
@@ -38,6 +39,11 @@ namespace AZ
|
||||
return m_shader;
|
||||
}
|
||||
|
||||
void MorphTargetComputePass::SetFeatureProcessor(SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor)
|
||||
{
|
||||
m_skinnedMeshFeatureProcessor = skinnedMeshFeatureProcessor;
|
||||
}
|
||||
|
||||
void MorphTargetComputePass::BuildAttachmentsInternal()
|
||||
{
|
||||
// The same buffer that skinning writes to is used to manage the computed vertex deltas that are passed from the
|
||||
@@ -45,30 +51,16 @@ namespace AZ
|
||||
AttachBufferToSlot(Name{ "MorphTargetDeltaOutput" }, SkinnedMeshOutputStreamManagerInterface::Get()->GetBuffer());
|
||||
}
|
||||
|
||||
void MorphTargetComputePass::AddDispatchItem(const RHI::DispatchItem* dispatchItem)
|
||||
{
|
||||
AZ_Assert(dispatchItem != nullptr, "invalid dispatchItem");
|
||||
|
||||
AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
|
||||
//using an unordered_set here to prevent redundantly adding the same dispatchItem to the submission queue
|
||||
//(i.e. if the same morph target exists in multiple views, it can call AddDispatchItem multiple times with the same item)
|
||||
m_dispatches.insert(dispatchItem);
|
||||
}
|
||||
|
||||
void MorphTargetComputePass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context)
|
||||
{
|
||||
RHI::CommandList* commandList = context.GetCommandList();
|
||||
|
||||
SetSrgsForDispatch(commandList);
|
||||
|
||||
AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
|
||||
for (const RHI::DispatchItem* dispatchItem : m_dispatches)
|
||||
if (m_skinnedMeshFeatureProcessor)
|
||||
{
|
||||
commandList->Submit(*dispatchItem);
|
||||
}
|
||||
RHI::CommandList* commandList = context.GetCommandList();
|
||||
|
||||
// Clear the dispatch items. They will need to be re-populated next frame
|
||||
m_dispatches.clear();
|
||||
SetSrgsForDispatch(commandList);
|
||||
|
||||
m_skinnedMeshFeatureProcessor->SubmitMorphTargetDispatchItems(commandList);
|
||||
}
|
||||
}
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
@@ -18,6 +18,8 @@ namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
class SkinnedMeshFeatureProcessor;
|
||||
|
||||
//! The morph target compute pass submits dispatch items for morph targets. The dispatch items are cleared every frame, so it needs to be re-populated.
|
||||
class MorphTargetComputePass
|
||||
: public RPI::ComputePass
|
||||
@@ -31,16 +33,14 @@ namespace AZ
|
||||
|
||||
static RPI::Ptr<MorphTargetComputePass> Create(const RPI::PassDescriptor& descriptor);
|
||||
|
||||
//! Thread-safe function for adding a dispatch item to the current frame.
|
||||
void AddDispatchItem(const RHI::DispatchItem* dispatchItem);
|
||||
Data::Instance<RPI::Shader> GetShader() const;
|
||||
|
||||
void SetFeatureProcessor(SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor);
|
||||
private:
|
||||
void BuildAttachmentsInternal() override;
|
||||
void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override;
|
||||
|
||||
AZStd::mutex m_mutex;
|
||||
AZStd::unordered_set<const RHI::DispatchItem*> m_dispatches;
|
||||
SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor = nullptr;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
*/
|
||||
|
||||
#include <MorphTargets/MorphTargetDispatchItem.h>
|
||||
#include <MorphTargets/MorphTargetComputePass.h>
|
||||
#include <SkinnedMesh/SkinnedMeshFeatureProcessor.h>
|
||||
|
||||
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
|
||||
#include <Atom/RPI.Public/Shader/Shader.h>
|
||||
@@ -30,7 +30,7 @@ namespace AZ
|
||||
MorphTargetDispatchItem::MorphTargetDispatchItem(
|
||||
const AZStd::intrusive_ptr<MorphTargetInputBuffers> inputBuffers,
|
||||
const MorphTargetMetaData& morphTargetMetaData,
|
||||
RPI::Ptr<MorphTargetComputePass> morphTargetComputePass,
|
||||
SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
|
||||
MorphTargetInstanceMetaData morphInstanceMetaData,
|
||||
float morphDeltaIntegerEncoding)
|
||||
: m_inputBuffers(inputBuffers)
|
||||
@@ -38,7 +38,7 @@ namespace AZ
|
||||
, m_morphInstanceMetaData(morphInstanceMetaData)
|
||||
, m_accumulatedDeltaIntegerEncoding(morphDeltaIntegerEncoding)
|
||||
{
|
||||
m_morphTargetShader = morphTargetComputePass->GetShader();
|
||||
m_morphTargetShader = skinnedMeshFeatureProcessor->GetMorphTargetShader();
|
||||
RPI::ShaderReloadNotificationBus::Handler::BusConnect(m_morphTargetShader->GetAssetId());
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ namespace AZ
|
||||
|
||||
namespace Render
|
||||
{
|
||||
class MorphTargetComputePass;
|
||||
class SkinnedMeshFeatureProcessor;
|
||||
|
||||
//! Holds and manages an RHI DispatchItem for a specific morph target, and the resources that are needed to build and maintain it.
|
||||
class MorphTargetDispatchItem
|
||||
@@ -51,7 +51,7 @@ namespace AZ
|
||||
explicit MorphTargetDispatchItem(
|
||||
const AZStd::intrusive_ptr<MorphTargetInputBuffers> inputBuffers,
|
||||
const MorphTargetMetaData& morphTargetMetaData,
|
||||
RPI::Ptr<MorphTargetComputePass> morphTargetComputePass,
|
||||
SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
|
||||
MorphTargetInstanceMetaData morphInstanceMetaData,
|
||||
float accumulatedDeltaRange
|
||||
);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
|
||||
|
||||
#include <SkinnedMesh/SkinnedMeshComputePass.h>
|
||||
#include <SkinnedMesh/SkinnedMeshFeatureProcessor.h>
|
||||
#include <Atom/Feature/SkinnedMesh/SkinnedMeshOutputStreamManagerInterface.h>
|
||||
|
||||
#include <Atom/RPI.Public/Shader/Shader.h>
|
||||
@@ -22,11 +23,9 @@ namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
|
||||
SkinnedMeshComputePass::SkinnedMeshComputePass(const RPI::PassDescriptor& descriptor)
|
||||
: RPI::ComputePass(descriptor)
|
||||
{
|
||||
m_cachedShaderOptions.SetShader(m_shader);
|
||||
}
|
||||
|
||||
RPI::Ptr<SkinnedMeshComputePass> SkinnedMeshComputePass::Create(const RPI::PassDescriptor& descriptor)
|
||||
@@ -40,42 +39,30 @@ namespace AZ
|
||||
return m_shader;
|
||||
}
|
||||
|
||||
RPI::ShaderOptionGroup SkinnedMeshComputePass::CreateShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler)
|
||||
void SkinnedMeshComputePass::SetFeatureProcessor(SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor)
|
||||
{
|
||||
m_cachedShaderOptions.ConnectToShaderReinitializedEvent(shaderReinitializedHandler);
|
||||
return m_cachedShaderOptions.CreateShaderOptionGroup(shaderOptions);
|
||||
}
|
||||
|
||||
void SkinnedMeshComputePass::AddDispatchItem(const RHI::DispatchItem* dispatchItem)
|
||||
{
|
||||
AZ_Assert(dispatchItem != nullptr, "invalid dispatchItem");
|
||||
|
||||
AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
|
||||
//using an unordered_set here to prevent redundantly adding the same dispatchItem to the submission queue
|
||||
//(i.e. if the same skinnedMesh exists in multiple views, it can call AddDispatchItem multiple times with the same item)
|
||||
m_dispatches.insert(dispatchItem);
|
||||
m_skinnedMeshFeatureProcessor = skinnedMeshFeatureProcessor;
|
||||
}
|
||||
|
||||
void SkinnedMeshComputePass::BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context)
|
||||
{
|
||||
RHI::CommandList* commandList = context.GetCommandList();
|
||||
|
||||
SetSrgsForDispatch(commandList);
|
||||
|
||||
AZStd::lock_guard<AZStd::mutex> lock(m_mutex);
|
||||
for (const RHI::DispatchItem* dispatchItem : m_dispatches)
|
||||
if (m_skinnedMeshFeatureProcessor)
|
||||
{
|
||||
commandList->Submit(*dispatchItem);
|
||||
}
|
||||
RHI::CommandList* commandList = context.GetCommandList();
|
||||
|
||||
// Clear the dispatch items. They will need to be re-populated next frame
|
||||
m_dispatches.clear();
|
||||
SetSrgsForDispatch(commandList);
|
||||
|
||||
m_skinnedMeshFeatureProcessor->SubmitSkinningDispatchItems(commandList);
|
||||
}
|
||||
}
|
||||
|
||||
void SkinnedMeshComputePass::OnShaderReinitialized(const RPI::Shader& shader)
|
||||
{
|
||||
ComputePass::OnShaderReinitialized(shader);
|
||||
m_cachedShaderOptions.SetShader(m_shader);
|
||||
if (m_skinnedMeshFeatureProcessor)
|
||||
{
|
||||
m_skinnedMeshFeatureProcessor->OnSkinningShaderReinitialized(m_shader);
|
||||
}
|
||||
}
|
||||
|
||||
void SkinnedMeshComputePass::OnShaderVariantReinitialized(const RPI::Shader& shader, const RPI::ShaderVariantId&, RPI::ShaderVariantStableId)
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace AZ
|
||||
{
|
||||
namespace Render
|
||||
{
|
||||
class SkinnedMeshFeatureProcessor;
|
||||
|
||||
//! The skinned mesh compute pass submits dispatch items for skinning. The dispatch items are cleared every frame, so it needs to be re-populated.
|
||||
class SkinnedMeshComputePass
|
||||
: public RPI::ComputePass
|
||||
@@ -33,10 +35,9 @@ namespace AZ
|
||||
|
||||
static RPI::Ptr<SkinnedMeshComputePass> Create(const RPI::PassDescriptor& descriptor);
|
||||
|
||||
//! Thread-safe function for adding a dispatch item to the current frame.
|
||||
void AddDispatchItem(const RHI::DispatchItem* dispatchItem);
|
||||
Data::Instance<RPI::Shader> GetShader() const;
|
||||
RPI::ShaderOptionGroup CreateShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler);
|
||||
|
||||
void SetFeatureProcessor(SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor);
|
||||
|
||||
private:
|
||||
void BuildCommandListInternal(const RHI::FrameGraphExecuteContext& context) override;
|
||||
@@ -45,9 +46,7 @@ namespace AZ
|
||||
void OnShaderReinitialized(const RPI::Shader& shader) override;
|
||||
void OnShaderVariantReinitialized(const RPI::Shader& shader, const RPI::ShaderVariantId& shaderVariantId, RPI::ShaderVariantStableId shaderVariantStableId) override;
|
||||
|
||||
AZStd::mutex m_mutex;
|
||||
AZStd::unordered_set<const RHI::DispatchItem*> m_dispatches;
|
||||
CachedSkinnedMeshShaderOptions m_cachedShaderOptions;
|
||||
SkinnedMeshFeatureProcessor* m_skinnedMeshFeatureProcessor = nullptr;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
#include <SkinnedMesh/SkinnedMeshDispatchItem.h>
|
||||
#include <SkinnedMesh/SkinnedMeshOutputStreamManager.h>
|
||||
#include <SkinnedMesh/SkinnedMeshComputePass.h>
|
||||
#include <SkinnedMesh/SkinnedMeshFeatureProcessor.h>
|
||||
|
||||
#include <Atom/RPI.Public/Shader/ShaderResourceGroup.h>
|
||||
#include <Atom/RPI.Public/Shader/Shader.h>
|
||||
@@ -34,7 +34,7 @@ namespace AZ
|
||||
size_t lodIndex,
|
||||
Data::Instance<RPI::Buffer> boneTransforms,
|
||||
const SkinnedMeshShaderOptions& shaderOptions,
|
||||
RPI::Ptr<SkinnedMeshComputePass> skinnedMeshComputePass,
|
||||
SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
|
||||
MorphTargetInstanceMetaData morphTargetInstanceMetaData,
|
||||
float morphTargetDeltaIntegerEncoding)
|
||||
: m_inputBuffers(inputBuffers)
|
||||
@@ -45,7 +45,7 @@ namespace AZ
|
||||
, m_morphTargetInstanceMetaData(morphTargetInstanceMetaData)
|
||||
, m_morphTargetDeltaIntegerEncoding(morphTargetDeltaIntegerEncoding)
|
||||
{
|
||||
m_skinningShader = skinnedMeshComputePass->GetShader();
|
||||
m_skinningShader = skinnedMeshFeatureProcessor->GetSkinningShader();
|
||||
|
||||
// Shader options are generally set per-skinned mesh instance, but morph targets may only exist on some lods. Override the option for applying morph targets here
|
||||
if (m_morphTargetInstanceMetaData.m_accumulatedPositionDeltaOffsetInBytes != MorphTargetConstants::s_invalidDeltaOffset)
|
||||
@@ -58,7 +58,7 @@ namespace AZ
|
||||
}
|
||||
|
||||
// CreateShaderOptionGroup will also connect to the SkinnedMeshShaderOptionNotificationBus
|
||||
m_shaderOptionGroup = skinnedMeshComputePass->CreateShaderOptionGroup(m_shaderOptions, *this);
|
||||
m_shaderOptionGroup = skinnedMeshFeatureProcessor->CreateSkinningShaderOptionGroup(m_shaderOptions, *this);
|
||||
}
|
||||
|
||||
SkinnedMeshDispatchItem::~SkinnedMeshDispatchItem()
|
||||
|
||||
@@ -38,7 +38,7 @@ namespace AZ
|
||||
|
||||
namespace Render
|
||||
{
|
||||
class SkinnedMeshComputePass;
|
||||
class SkinnedMeshFeatureProcessor;
|
||||
|
||||
//! Holds and manages an RHI DispatchItem for a specific skinned mesh, and the resources that are needed to build and maintain it.
|
||||
class SkinnedMeshDispatchItem
|
||||
@@ -55,7 +55,7 @@ namespace AZ
|
||||
size_t lodIndex,
|
||||
Data::Instance<RPI::Buffer> skinningMatrices,
|
||||
const SkinnedMeshShaderOptions& shaderOptions,
|
||||
RPI::Ptr<SkinnedMeshComputePass> skinnedMeshComputePass,
|
||||
SkinnedMeshFeatureProcessor* skinnedMeshFeatureProcessor,
|
||||
MorphTargetInstanceMetaData morphTargetInstanceMetaData,
|
||||
float morphTargetDeltaIntegerEncoding
|
||||
);
|
||||
|
||||
@@ -24,8 +24,10 @@
|
||||
#include <Atom/RPI.Public/Pass/PassSystemInterface.h>
|
||||
#include <Atom/RPI.Public/RPIUtils.h>
|
||||
#include <Atom/RPI.Public/Shader/Shader.h>
|
||||
#include <Atom/RPI.Public/RenderPipeline.h>
|
||||
|
||||
#include <Atom/RHI/CpuProfiler.h>
|
||||
#include <Atom/RHI/CommandList.h>
|
||||
|
||||
#include <AzCore/Debug/EventTrace.h>
|
||||
#include <AzCore/Jobs/JobCompletion.h>
|
||||
@@ -84,11 +86,6 @@ namespace AZ
|
||||
AZ_PROFILE_FUNCTION(Debug::ProfileCategory::AzRender);
|
||||
AZ_ATOM_PROFILE_FUNCTION("SkinnedMesh", "SkinnedMeshFeatureProcessor: Render");
|
||||
|
||||
if (!m_skinningPass)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
#if 0 //[GFX_TODO][ATOM-13564] Temporarily disable skinning culling until we figure out how to hook up visibility & lod selection with skinning:
|
||||
//Setup the culling workgroup (it will be re-used for each view)
|
||||
{
|
||||
@@ -132,7 +129,7 @@ namespace AZ
|
||||
//Dispatch the workgroup to each view
|
||||
for (const RPI::ViewPtr& viewPtr : packet.m_views)
|
||||
{
|
||||
Job *processWorkgroupJob = AZ::CreateJobFunction(
|
||||
Job* processWorkgroupJob = AZ::CreateJobFunction(
|
||||
[this, cullingSystem, viewPtr](AZ::Job& thisJob)
|
||||
{
|
||||
AZ_PROFILE_SCOPE_DYNAMIC(Debug::ProfileCategory::AzRender, "skinningMeshFP processWorkgroupJob - View: %s", viewPtr->GetName().GetCStr());
|
||||
@@ -167,7 +164,16 @@ namespace AZ
|
||||
float maxScreenPercentage(lod.m_range.m_max);
|
||||
if (approxScreenPercentage >= minScreenPercentage && approxScreenPercentage <= maxScreenPercentage)
|
||||
{
|
||||
m_skinningPass->AddDispatchItem(&renderProxy->m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
|
||||
AZStd::lock_guard lock(m_dispatchItemMutex);
|
||||
m_skinningDispatches.insert(&renderProxy->m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
|
||||
for (size_t morphTargetIndex = 0; morphTargetIndex < renderProxy->m_morphTargetDispatchItemsByLod[lodIndex].size(); morphTargetIndex++)
|
||||
{
|
||||
const MorphTargetDispatchItem* dispatchItem = renderProxy->m_morphTargetDispatchItemsByLod[lodIndex][morphTargetIndex].get();
|
||||
if (dispatchItem && dispatchItem->GetWeight() > AZ::Constants::FloatEpsilon)
|
||||
{
|
||||
m_morphTargetDispatches.insert(&dispatchItem->GetRHIDispatchItem());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,13 +238,14 @@ namespace AZ
|
||||
//Note that this supports overlapping lod ranges (to support cross-fading lods, for example)
|
||||
if (approxScreenPercentage >= lod.m_screenCoverageMin && approxScreenPercentage <= lod.m_screenCoverageMax)
|
||||
{
|
||||
m_skinningPass->AddDispatchItem(&renderProxy.m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
|
||||
AZStd::lock_guard lock(m_dispatchItemMutex);
|
||||
m_skinningDispatches.insert(&renderProxy.m_dispatchItemsByLod[lodIndex]->GetRHIDispatchItem());
|
||||
for (size_t morphTargetIndex = 0; morphTargetIndex < renderProxy.m_morphTargetDispatchItemsByLod[lodIndex].size(); morphTargetIndex++)
|
||||
{
|
||||
const MorphTargetDispatchItem* dispatchItem = renderProxy.m_morphTargetDispatchItemsByLod[lodIndex][morphTargetIndex].get();
|
||||
if (dispatchItem && dispatchItem->GetWeight() > AZ::Constants::FloatEpsilon)
|
||||
{
|
||||
m_morphTargetPass->AddDispatchItem(&dispatchItem->GetRHIDispatchItem());
|
||||
m_morphTargetDispatches.insert(&dispatchItem->GetRHIDispatchItem());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -248,19 +255,14 @@ namespace AZ
|
||||
#endif
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::OnRenderPipelineAdded([[maybe_unused]] RPI::RenderPipelinePtr pipeline)
|
||||
void SkinnedMeshFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline)
|
||||
{
|
||||
InitSkinningAndMorphPass();
|
||||
InitSkinningAndMorphPass(pipeline->GetRootPass());
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::OnRenderPipelineRemoved([[maybe_unused]] RPI::RenderPipeline* pipeline)
|
||||
void SkinnedMeshFeatureProcessor::OnRenderPipelinePassesChanged(RPI::RenderPipeline* renderPipeline)
|
||||
{
|
||||
InitSkinningAndMorphPass();
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::OnRenderPipelinePassesChanged([[maybe_unused]] RPI::RenderPipeline* renderPipeline)
|
||||
{
|
||||
InitSkinningAndMorphPass();
|
||||
InitSkinningAndMorphPass(renderPipeline->GetRootPass());
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::OnBeginPrepareRender()
|
||||
@@ -268,9 +270,15 @@ namespace AZ
|
||||
m_renderProxiesChecker.soft_lock();
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::OnEndPrepareRender()
|
||||
void SkinnedMeshFeatureProcessor::OnRenderEnd()
|
||||
{
|
||||
m_renderProxiesChecker.soft_unlock();
|
||||
|
||||
// Clear any dispatch items that were added but never submitted
|
||||
// in case there were no passes that submitted this frame
|
||||
// because they execute at a lower frequency
|
||||
m_skinningDispatches.clear();
|
||||
m_morphTargetDispatches.clear();
|
||||
}
|
||||
|
||||
SkinnedMeshRenderProxyHandle SkinnedMeshFeatureProcessor::AcquireRenderProxy(const SkinnedMeshRenderProxyDesc& desc)
|
||||
@@ -295,61 +303,73 @@ namespace AZ
|
||||
return false;
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::InitSkinningAndMorphPass()
|
||||
void SkinnedMeshFeatureProcessor::InitSkinningAndMorphPass(const RPI::Ptr<RPI::ParentPass> pipelineRootPass)
|
||||
{
|
||||
m_skinningPass = nullptr; //reset it to null, just in case it fails to load the assets properly
|
||||
m_morphTargetPass = nullptr;
|
||||
|
||||
RPI::PassSystemInterface* passSystem = RPI::PassSystemInterface::Get();
|
||||
if (passSystem->HasPassesForTemplateName(AZ::Name{ "SkinningPassTemplate" }))
|
||||
RPI::Ptr<RPI::Pass> skinningPass = pipelineRootPass->FindPassByNameRecursive(AZ::Name{ "SkinningPass" });
|
||||
if (skinningPass)
|
||||
{
|
||||
auto& skinningPasses = passSystem->GetPassesForTemplateName(AZ::Name{ "SkinningPassTemplate" });
|
||||
SkinnedMeshComputePass* skinnedMeshComputePass = azdynamic_cast<SkinnedMeshComputePass*>(skinningPass.get());
|
||||
skinnedMeshComputePass->SetFeatureProcessor(this);
|
||||
|
||||
// For now, assume one skinning pass
|
||||
if (!skinningPasses.empty() && skinningPasses[0])
|
||||
// There may be multiple skinning passes in the scene due to multiple pipelines, but there is only one skinning shader
|
||||
m_skinningShader = skinnedMeshComputePass->GetShader();
|
||||
|
||||
if (!m_skinningShader)
|
||||
{
|
||||
m_skinningPass = static_cast<SkinnedMeshComputePass*>(skinningPasses[0]);
|
||||
const Data::Instance<RPI::Shader> shader = m_skinningPass->GetShader();
|
||||
|
||||
if (!shader)
|
||||
{
|
||||
AZ_Error(s_featureProcessorName, false, "Failed to get skinning pass shader. It may need to finish processing.");
|
||||
}
|
||||
AZ_Error(s_featureProcessorName, false, "Failed to get skinning pass shader. It may need to finish processing.");
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Error(s_featureProcessorName, false, "\"SkinningPassTemplate\" does not have any valid passes. Check your game project's .pass assets.");
|
||||
m_cachedSkinningShaderOptions.SetShader(m_skinningShader);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Error(s_featureProcessorName, false, "Failed to find passes for \"SkinningPassTemplate\". Check your game project's .pass assets.");
|
||||
}
|
||||
|
||||
if (passSystem->HasPassesForTemplateName(AZ::Name{ "MorphTargetPassTemplate" }))
|
||||
RPI::Ptr<RPI::Pass> morphTargetPass = pipelineRootPass->FindPassByNameRecursive(AZ::Name{ "MorphTargetPass" });
|
||||
if (morphTargetPass)
|
||||
{
|
||||
auto& morphTargetPasses = passSystem->GetPassesForTemplateName(AZ::Name{ "MorphTargetPassTemplate" });
|
||||
MorphTargetComputePass* morphTargetComputePass = azdynamic_cast<MorphTargetComputePass*>(morphTargetPass.get());
|
||||
morphTargetComputePass->SetFeatureProcessor(this);
|
||||
|
||||
// For now, assume one skinning pass
|
||||
if (!morphTargetPasses.empty() && morphTargetPasses[0])
|
||||
// There may be multiple morph target passes in the scene due to multiple pipelines, but there is only one morph target shader
|
||||
m_morphTargetShader = morphTargetComputePass->GetShader();
|
||||
|
||||
if (!m_morphTargetShader)
|
||||
{
|
||||
m_morphTargetPass = static_cast<MorphTargetComputePass*>(morphTargetPasses[0]);
|
||||
const Data::Instance<RPI::Shader> shader = m_morphTargetPass->GetShader();
|
||||
AZ_Error(s_featureProcessorName, false, "Failed to get morph target pass shader. It may need to finish processing.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!shader)
|
||||
{
|
||||
AZ_Error(s_featureProcessorName, false, "Failed to get morph target pass shader. It may need to finish processing.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Error(s_featureProcessorName, false, "\"MorphTargetPassTemplate\" does not have any valid passes. Check your game project's .pass assets.");
|
||||
}
|
||||
}
|
||||
else
|
||||
RPI::ShaderOptionGroup SkinnedMeshFeatureProcessor::CreateSkinningShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler)
|
||||
{
|
||||
m_cachedSkinningShaderOptions.ConnectToShaderReinitializedEvent(shaderReinitializedHandler);
|
||||
return m_cachedSkinningShaderOptions.CreateShaderOptionGroup(shaderOptions);
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::OnSkinningShaderReinitialized(const Data::Instance<RPI::Shader> skinningShader)
|
||||
{
|
||||
m_skinningShader = skinningShader;
|
||||
m_cachedSkinningShaderOptions.SetShader(m_skinningShader);
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::SubmitSkinningDispatchItems(RHI::CommandList* commandList)
|
||||
{
|
||||
AZStd::lock_guard lock(m_dispatchItemMutex);
|
||||
for (const RHI::DispatchItem* dispatchItem : m_skinningDispatches)
|
||||
{
|
||||
AZ_Error(s_featureProcessorName, false, "Failed to find passes for \"MorphTargetPassTemplate\". Check your game project's .pass assets.");
|
||||
commandList->Submit(*dispatchItem);
|
||||
}
|
||||
m_skinningDispatches.clear();
|
||||
}
|
||||
|
||||
void SkinnedMeshFeatureProcessor::SubmitMorphTargetDispatchItems(RHI::CommandList* commandList)
|
||||
{
|
||||
AZStd::lock_guard lock(m_dispatchItemMutex);
|
||||
for (const RHI::DispatchItem* dispatchItem : m_morphTargetDispatches)
|
||||
{
|
||||
commandList->Submit(*dispatchItem);
|
||||
}
|
||||
m_morphTargetDispatches.clear();
|
||||
}
|
||||
|
||||
SkinnedMeshRenderProxyInterfaceHandle SkinnedMeshFeatureProcessor::AcquireRenderProxyInterface(const SkinnedMeshRenderProxyDesc& desc)
|
||||
@@ -363,14 +383,14 @@ namespace AZ
|
||||
return ReleaseRenderProxy(handle);
|
||||
}
|
||||
|
||||
RPI::Ptr<SkinnedMeshComputePass> SkinnedMeshFeatureProcessor::GetSkinningPass() const
|
||||
Data::Instance<RPI::Shader> SkinnedMeshFeatureProcessor::GetSkinningShader() const
|
||||
{
|
||||
return m_skinningPass;
|
||||
return m_skinningShader;
|
||||
}
|
||||
|
||||
RPI::Ptr<MorphTargetComputePass> SkinnedMeshFeatureProcessor::GetMorphTargetPass() const
|
||||
Data::Instance<RPI::Shader> SkinnedMeshFeatureProcessor::GetMorphTargetShader() const
|
||||
{
|
||||
return m_morphTargetPass;
|
||||
return m_morphTargetShader;
|
||||
}
|
||||
} // namespace Render
|
||||
} // namespace AZ
|
||||
|
||||
@@ -51,35 +51,46 @@ namespace AZ
|
||||
void Deactivate() override;
|
||||
void Simulate(const FeatureProcessor::SimulatePacket& packet) override;
|
||||
void Render(const FeatureProcessor::RenderPacket& packet) override;
|
||||
void OnRenderEnd() override;
|
||||
|
||||
// RPI::SceneNotificationBus overrides ...
|
||||
void OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) override;
|
||||
void OnRenderPipelineRemoved(RPI::RenderPipeline* pipeline) override;
|
||||
void OnRenderPipelinePassesChanged(RPI::RenderPipeline* renderPipeline) override;
|
||||
void OnBeginPrepareRender() override;
|
||||
void OnEndPrepareRender() override;
|
||||
|
||||
SkinnedMeshRenderProxyHandle AcquireRenderProxy(const SkinnedMeshRenderProxyDesc& desc);
|
||||
bool ReleaseRenderProxy(SkinnedMeshRenderProxyHandle& handle);
|
||||
|
||||
RPI::Ptr<SkinnedMeshComputePass> GetSkinningPass() const;
|
||||
RPI::Ptr<MorphTargetComputePass> GetMorphTargetPass() const;
|
||||
Data::Instance<RPI::Shader> GetSkinningShader() const;
|
||||
RPI::ShaderOptionGroup CreateSkinningShaderOptionGroup(const SkinnedMeshShaderOptions shaderOptions, SkinnedMeshShaderOptionNotificationBus::Handler& shaderReinitializedHandler);
|
||||
void OnSkinningShaderReinitialized(const Data::Instance<RPI::Shader> skinningShader);
|
||||
void SubmitSkinningDispatchItems(RHI::CommandList* commandList);
|
||||
|
||||
Data::Instance<RPI::Shader> GetMorphTargetShader() const;
|
||||
void SubmitMorphTargetDispatchItems(RHI::CommandList* commandList);
|
||||
private:
|
||||
AZ_DISABLE_COPY_MOVE(SkinnedMeshFeatureProcessor);
|
||||
|
||||
void InitSkinningAndMorphPass();
|
||||
void InitSkinningAndMorphPass(const RPI::Ptr<RPI::ParentPass> pipelineRootPass);
|
||||
|
||||
SkinnedMeshRenderProxyInterfaceHandle AcquireRenderProxyInterface(const SkinnedMeshRenderProxyDesc& desc) override;
|
||||
bool ReleaseRenderProxyInterface(SkinnedMeshRenderProxyInterfaceHandle& handle) override;
|
||||
|
||||
static const char* s_featureProcessorName;
|
||||
RPI::Ptr<SkinnedMeshComputePass> m_skinningPass;
|
||||
RPI::Ptr<MorphTargetComputePass> m_morphTargetPass;
|
||||
|
||||
Data::Instance<RPI::Shader> m_skinningShader;
|
||||
CachedSkinnedMeshShaderOptions m_cachedSkinningShaderOptions;
|
||||
|
||||
Data::Instance<RPI::Shader> m_morphTargetShader;
|
||||
|
||||
AZStd::concurrency_checker m_renderProxiesChecker;
|
||||
StableDynamicArray<SkinnedMeshRenderProxy> m_renderProxies;
|
||||
AZStd::unique_ptr<SkinnedMeshStatsCollector> m_statsCollector;
|
||||
|
||||
MeshFeatureProcessor* m_meshFeatureProcessor = nullptr;
|
||||
AZStd::unordered_set<const RHI::DispatchItem*> m_skinningDispatches;
|
||||
AZStd::unordered_set<const RHI::DispatchItem*> m_morphTargetDispatches;
|
||||
AZStd::mutex m_dispatchItemMutex;
|
||||
|
||||
};
|
||||
} // namespace Render
|
||||
|
||||
@@ -60,13 +60,7 @@ namespace AZ
|
||||
|
||||
bool SkinnedMeshRenderProxy::BuildDispatchItem([[maybe_unused]] const RPI::Scene& scene, size_t modelLodIndex, [[maybe_unused]] const SkinnedMeshShaderOptions& shaderOptions)
|
||||
{
|
||||
if (!m_featureProcessor->GetSkinningPass())
|
||||
{
|
||||
AZ_Error("Skinned Mesh Feature Processor", false, "Failed to get Skinning Pass. Make sure the project has a skinning pass.");
|
||||
return false;
|
||||
}
|
||||
|
||||
Data::Instance<RPI::Shader> skinningShader = m_featureProcessor->GetSkinningPass()->GetShader();
|
||||
Data::Instance<RPI::Shader> skinningShader = m_featureProcessor->GetSkinningShader();
|
||||
if (!skinningShader)
|
||||
{
|
||||
AZ_Error("Skinned Mesh Feature Processor", false, "Failed to get skinning shader from skinning pass");
|
||||
@@ -89,7 +83,7 @@ namespace AZ
|
||||
m_instance->m_outputStreamOffsetsInBytes[modelLodIndex],
|
||||
modelLodIndex, m_boneTransforms,
|
||||
m_shaderOptions,
|
||||
m_featureProcessor->GetSkinningPass(),
|
||||
m_featureProcessor,
|
||||
m_instance->m_morphTargetInstanceMetaData[modelLodIndex],
|
||||
morphDeltaIntegerEncoding });
|
||||
|
||||
@@ -100,7 +94,7 @@ namespace AZ
|
||||
}
|
||||
|
||||
// Get the data needed to create a morph target dispatch item
|
||||
Data::Instance<RPI::Shader> morphTargetShader = m_featureProcessor->GetMorphTargetPass()->GetShader();
|
||||
Data::Instance<RPI::Shader> morphTargetShader = m_featureProcessor->GetMorphTargetShader();
|
||||
const AZStd::vector<AZStd::intrusive_ptr<MorphTargetInputBuffers>>& morphTargetInputBuffersVector = m_inputBuffers->GetMorphTargetInputBuffers(modelLodIndex);
|
||||
AZ_Assert(morphTargetMetaDatas.size() == morphTargetInputBuffersVector.size(), "Skinned Mesh Feature Processor - Mismatch in morph target metadata count and morph target input buffer count");
|
||||
|
||||
@@ -118,7 +112,7 @@ namespace AZ
|
||||
aznew MorphTargetDispatchItem{
|
||||
morphTargetInputBuffersVector[morphTargetIndex],
|
||||
morphTargetMetaDatas[morphTargetIndex],
|
||||
m_featureProcessor->GetMorphTargetPass(),
|
||||
m_featureProcessor,
|
||||
m_instance->m_morphTargetInstanceMetaData[modelLodIndex],
|
||||
morphDeltaIntegerEncoding });
|
||||
|
||||
|
||||
@@ -59,6 +59,26 @@ ly_add_target(
|
||||
)
|
||||
|
||||
if (PAL_TRAIT_BUILD_HOST_TOOLS)
|
||||
ly_add_target(
|
||||
NAME Multiplayer.Tools.Static STATIC
|
||||
NAMESPACE Gem
|
||||
FILES_CMAKE
|
||||
multiplayer_tools_files.cmake
|
||||
COMPILE_DEFINITIONS
|
||||
PUBLIC
|
||||
MULTIPLAYER_TOOLS
|
||||
INCLUDE_DIRECTORIES
|
||||
PRIVATE
|
||||
.
|
||||
Source
|
||||
${pal_source_dir}
|
||||
PUBLIC
|
||||
Include
|
||||
BUILD_DEPENDENCIES
|
||||
PUBLIC
|
||||
AZ::AzToolsFramework
|
||||
Gem::Multiplayer.Static
|
||||
)
|
||||
|
||||
ly_add_target(
|
||||
NAME Multiplayer.Tools MODULE
|
||||
@@ -74,8 +94,7 @@ if (PAL_TRAIT_BUILD_HOST_TOOLS)
|
||||
Include
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
AZ::AzToolsFramework
|
||||
Gem::Multiplayer.Static
|
||||
Gem::Multiplayer.Tools.Static
|
||||
)
|
||||
|
||||
ly_add_target(
|
||||
@@ -145,6 +164,30 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED)
|
||||
ly_add_googletest(
|
||||
NAME Gem::Multiplayer.Tests
|
||||
)
|
||||
|
||||
if (PAL_TRAIT_BUILD_HOST_TOOLS)
|
||||
ly_add_target(
|
||||
NAME Multiplayer.Tools.Tests ${PAL_TRAIT_TEST_TARGET_TYPE}
|
||||
NAMESPACE Gem
|
||||
FILES_CMAKE
|
||||
multiplayer_tools_tests_files.cmake
|
||||
INCLUDE_DIRECTORIES
|
||||
PRIVATE
|
||||
Tests
|
||||
Source
|
||||
.
|
||||
BUILD_DEPENDENCIES
|
||||
PRIVATE
|
||||
AZ::AzTest
|
||||
AZ::AzTestShared
|
||||
AZ::AzToolsFrameworkTestCommon
|
||||
Gem::Multiplayer.Tools.Static
|
||||
)
|
||||
ly_add_googletest(
|
||||
NAME Gem::Multiplayer.Tools.Tests
|
||||
)
|
||||
endif()
|
||||
|
||||
endif()
|
||||
|
||||
ly_add_target(
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 <AzCore/Asset/AssetCommon.h>
|
||||
#include <AzCore/Name/Name.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
//! @class INetworkSpawnableLibrary
|
||||
//! @brief The interface for managing network spawnables.
|
||||
class INetworkSpawnableLibrary
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(INetworkSpawnableLibrary, "{A3CF809C-6C1D-4B43-B2C4-3901B5DE1ABE}");
|
||||
|
||||
virtual ~INetworkSpawnableLibrary() = default;
|
||||
virtual void BuildSpawnablesList() = 0;
|
||||
virtual void ProcessSpawnableAsset(const AZStd::string& relativePath, AZ::Data::AssetId id) = 0;
|
||||
virtual AZ::Name GetSpawnableNameFromAssetId(AZ::Data::AssetId assetId) = 0;
|
||||
virtual AZ::Data::AssetId GetAssetIdByName(AZ::Name name) = 0;
|
||||
};
|
||||
}
|
||||
@@ -78,6 +78,12 @@ namespace Multiplayer
|
||||
const AZ::Transform& transform
|
||||
) = 0;
|
||||
|
||||
//! Configures new networked entity
|
||||
//! @param netEntity the entity to setup
|
||||
//! @param prefabEntryId the name of the spawnable the entity originated from
|
||||
//! @param netEntityRole the net role the entity should be setup for
|
||||
virtual void SetupNetEntity(AZ::Entity* netEntity, PrefabEntityId prefabEntityId, NetEntityRole netEntityRole) = 0;
|
||||
|
||||
//! Returns an ConstEntityPtr for the provided entityId.
|
||||
//! @param netEntityId the netEntityId to get an ConstEntityPtr for
|
||||
//! @return the requested ConstEntityPtr
|
||||
|
||||
+3
-3
@@ -555,10 +555,10 @@ namespace Multiplayer
|
||||
{
|
||||
replicatorEntity = entityList[0];
|
||||
}
|
||||
|
||||
AZ_Assert(replicatorEntity != nullptr, "Failed to create entity from prefab %s", prefabEntityId.m_prefabName.GetCStr());
|
||||
if (replicatorEntity == nullptr)
|
||||
else
|
||||
{
|
||||
AZ_Assert(false, "There should be exactly one created entity out of prefab %s, index %d. Got: %d",
|
||||
prefabEntityId.m_prefabName.GetCStr(), prefabEntityId.m_entityOffset, entityList.size());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,18 +34,15 @@ namespace Multiplayer
|
||||
: m_networkEntityAuthorityTracker(*this)
|
||||
, m_removeEntitiesEvent([this] { RemoveEntities(); }, AZ::Name("NetworkEntityManager remove entities event"))
|
||||
, m_updateEntityDomainEvent([this] { UpdateEntityDomain(); }, AZ::Name("NetworkEntityManager update entity domain event"))
|
||||
, m_onSpawnedHandler([this](AZ::Data::Asset<AzFramework::Spawnable> spawnable) { this->OnSpawned(spawnable); })
|
||||
, m_onDespawnedHandler([this](AZ::Data::Asset<AzFramework::Spawnable> spawnable) { this->OnDespawned(spawnable); })
|
||||
{
|
||||
AZ::Interface<INetworkEntityManager>::Register(this);
|
||||
AzFramework::RootSpawnableNotificationBus::Handler::BusConnect();
|
||||
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->AddOnSpawnedHandler(m_onSpawnedHandler);
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->AddOnDespawnedHandler(m_onDespawnedHandler);
|
||||
}
|
||||
|
||||
NetworkEntityManager::~NetworkEntityManager()
|
||||
{
|
||||
AzFramework::RootSpawnableNotificationBus::Handler::BusDisconnect();
|
||||
AZ::Interface<INetworkEntityManager>::Unregister(this);
|
||||
}
|
||||
|
||||
void NetworkEntityManager::Initialize(HostId hostId, AZStd::unique_ptr<IEntityDomain> entityDomain)
|
||||
@@ -331,17 +328,41 @@ namespace Multiplayer
|
||||
const AzFramework::Spawnable::EntityList& entities = spawnable.GetEntities();
|
||||
size_t entitiesSize = entities.size();
|
||||
|
||||
using EntityIdMap = AZStd::unordered_map<AZ::EntityId, AZ::EntityId>;
|
||||
EntityIdMap originalToCloneIdMap;
|
||||
|
||||
for (size_t i = 0; i < entitiesSize; ++i)
|
||||
{
|
||||
AZ::Entity* clone = serializeContext->CloneObject(entities[i].get());
|
||||
AZ::Entity* originalEntity = entities[i].get();
|
||||
AZ::Entity* clone = serializeContext->CloneObject(originalEntity);
|
||||
AZ_Assert(clone != nullptr, "Failed to clone spawnable entity.");
|
||||
|
||||
clone->SetId(AZ::Entity::MakeId());
|
||||
|
||||
originalToCloneIdMap[originalEntity->GetId()] = clone->GetId();
|
||||
|
||||
NetBindComponent* netBindComponent = clone->FindComponent<NetBindComponent>();
|
||||
if (netBindComponent != nullptr)
|
||||
{
|
||||
// Update TransformComponent parent Id. It is guaranteed for the entities array to be sorted from parent->child here.
|
||||
auto* transformComponent = clone->FindComponent<AzFramework::TransformComponent>();
|
||||
AZ::EntityId parentId = transformComponent->GetParentId();
|
||||
if (parentId.IsValid())
|
||||
{
|
||||
auto it = originalToCloneIdMap.find(parentId);
|
||||
if (it != originalToCloneIdMap.end())
|
||||
{
|
||||
transformComponent->SetParentRelative(it->second);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Warning("NetworkEntityManager", false, "Entity %s doesn't have the parent entity %s present in network.spawnable",
|
||||
clone->GetName().c_str(), parentId.ToString().data());
|
||||
}
|
||||
}
|
||||
|
||||
PrefabEntityId prefabEntityId;
|
||||
prefabEntityId.m_prefabName = m_networkPrefabLibrary.GetPrefabNameFromAssetId(spawnable.GetId());
|
||||
prefabEntityId.m_prefabName = m_networkPrefabLibrary.GetSpawnableNameFromAssetId(spawnable.GetId());
|
||||
prefabEntityId.m_entityOffset = aznumeric_cast<uint32_t>(i);
|
||||
|
||||
const NetEntityId netEntityId = NextId();
|
||||
@@ -469,57 +490,19 @@ namespace Multiplayer
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkEntityManager::OnSpawned(AZ::Data::Asset<AzFramework::Spawnable> spawnable)
|
||||
void NetworkEntityManager::SetupNetEntity(AZ::Entity* netEntity, PrefabEntityId prefabEntityId, NetEntityRole netEntityRole)
|
||||
{
|
||||
AzFramework::Spawnable* spawnableData = spawnable.GetAs<AzFramework::Spawnable>();
|
||||
const auto& entityList = spawnableData->GetEntities();
|
||||
if (entityList.size() == 0)
|
||||
auto* netBindComponent = netEntity->FindComponent<NetBindComponent>();
|
||||
|
||||
if (netBindComponent)
|
||||
{
|
||||
AZ_Error("NetworkEntityManager", false, "OnSpawned: Spawnable %s doesn't have any entities.",
|
||||
spawnable.GetHint().c_str());
|
||||
return;
|
||||
const NetEntityId netEntityId = NextId();
|
||||
netBindComponent->PreInit(netEntity, prefabEntityId, netEntityId, netEntityRole);
|
||||
}
|
||||
|
||||
const auto& rootEntity = entityList[0];
|
||||
auto* spawnableHolder = rootEntity->FindComponent<NetworkSpawnableHolderComponent>();
|
||||
if (!spawnableHolder)
|
||||
else
|
||||
{
|
||||
// Root entity doesn't have NetworkSpawnableHolderComponent. It means there's no corresponding network spawnable.
|
||||
return;
|
||||
AZ_Error("NetworkEntityManager", false, "SetupNetEntity called for an entity with no NetBindComponent. Entity: %s",
|
||||
netEntity->GetName().c_str());
|
||||
}
|
||||
|
||||
AZ::Data::Asset<AzFramework::Spawnable> netSpawnableAsset = spawnableHolder->GetNetworkSpawnableAsset();
|
||||
AzFramework::Spawnable* netSpawnable = netSpawnableAsset.GetAs<AzFramework::Spawnable>();
|
||||
if (!netSpawnable)
|
||||
{
|
||||
// TODO: Temp sync load until JsonSerialization of loadBehavior is fixed.
|
||||
netSpawnableAsset = AZ::Data::AssetManager::Instance().GetAsset<AzFramework::Spawnable>(
|
||||
netSpawnableAsset.GetId(), AZ::Data::AssetLoadBehavior::PreLoad);
|
||||
AZ::Data::AssetManager::Instance().BlockUntilLoadComplete(netSpawnableAsset);
|
||||
|
||||
netSpawnable = netSpawnableAsset.GetAs<AzFramework::Spawnable>();
|
||||
}
|
||||
|
||||
if (!netSpawnable)
|
||||
{
|
||||
AZ_Error("NetworkEntityManager", false, "OnRootSpawnableAssigned: Net spawnable doesn't have any data.");
|
||||
return;
|
||||
}
|
||||
|
||||
auto* multiplayer = GetMultiplayer();
|
||||
|
||||
const auto agentType = multiplayer->GetAgentType();
|
||||
const bool spawnImmediately =
|
||||
(agentType == MultiplayerAgentType::ClientServer || agentType == MultiplayerAgentType::DedicatedServer);
|
||||
|
||||
if (spawnImmediately)
|
||||
{
|
||||
CreateEntitiesImmediate(*netSpawnable, NetEntityRole::Authority);
|
||||
}
|
||||
}
|
||||
|
||||
void NetworkEntityManager::OnDespawned([[maybe_unused]]AZ::Data::Asset<AzFramework::Spawnable> spawnable)
|
||||
{
|
||||
// TODO: Remove entities instantiated from the spawnable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,8 @@ namespace Multiplayer
|
||||
const AZ::Transform& transform
|
||||
) override;
|
||||
|
||||
void SetupNetEntity(AZ::Entity* netEntity, PrefabEntityId prefabEntityId, NetEntityRole netEntityRole) override;
|
||||
|
||||
uint32_t GetEntityCount() const override;
|
||||
NetworkEntityHandle AddEntityToEntityMap(NetEntityId netEntityId, AZ::Entity* entity) override;
|
||||
void MarkForRemoval(const ConstNetworkEntityHandle& entityHandle) override;
|
||||
@@ -93,9 +95,6 @@ namespace Multiplayer
|
||||
void RemoveEntities();
|
||||
NetEntityId NextId();
|
||||
|
||||
void OnSpawned(AZ::Data::Asset<AzFramework::Spawnable> spawnable);
|
||||
void OnDespawned(AZ::Data::Asset<AzFramework::Spawnable> spawnable);
|
||||
|
||||
NetworkEntityTracker m_networkEntityTracker;
|
||||
NetworkEntityAuthorityTracker m_networkEntityAuthorityTracker;
|
||||
MultiplayerComponentRegistry m_multiplayerComponentRegistry;
|
||||
@@ -123,8 +122,5 @@ namespace Multiplayer
|
||||
DeferredRpcMessages m_localDeferredRpcMessages;
|
||||
|
||||
NetworkSpawnableLibrary m_networkPrefabLibrary;
|
||||
|
||||
AZ::Event<AZ::Data::Asset<AzFramework::Spawnable>>::Handler m_onSpawnedHandler;
|
||||
AZ::Event<AZ::Data::Asset<AzFramework::Spawnable>>::Handler m_onDespawnedHandler;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -14,20 +14,23 @@
|
||||
#include <AzCore/Asset/AssetManagerBus.h>
|
||||
#include <AzFramework/Spawnable/Spawnable.h>
|
||||
#include <AzCore/StringFunc/StringFunc.h>
|
||||
#include <AzCore/Interface/Interface.h>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
NetworkSpawnableLibrary::NetworkSpawnableLibrary()
|
||||
{
|
||||
AZ::Interface<INetworkSpawnableLibrary>::Register(this);
|
||||
AzFramework::AssetCatalogEventBus::Handler::BusConnect();
|
||||
}
|
||||
|
||||
NetworkSpawnableLibrary::~NetworkSpawnableLibrary()
|
||||
{
|
||||
AzFramework::AssetCatalogEventBus::Handler::BusDisconnect();
|
||||
AZ::Interface<INetworkSpawnableLibrary>::Unregister(this);
|
||||
}
|
||||
|
||||
void NetworkSpawnableLibrary::BuildPrefabsList()
|
||||
void NetworkSpawnableLibrary::BuildSpawnablesList()
|
||||
{
|
||||
auto enumerateCallback = [this](const AZ::Data::AssetId id, const AZ::Data::AssetInfo& info)
|
||||
{
|
||||
@@ -50,10 +53,10 @@ namespace Multiplayer
|
||||
|
||||
void NetworkSpawnableLibrary::OnCatalogLoaded([[maybe_unused]] const char* catalogFile)
|
||||
{
|
||||
BuildPrefabsList();
|
||||
BuildSpawnablesList();
|
||||
}
|
||||
|
||||
AZ::Name NetworkSpawnableLibrary::GetPrefabNameFromAssetId(AZ::Data::AssetId assetId)
|
||||
AZ::Name NetworkSpawnableLibrary::GetSpawnableNameFromAssetId(AZ::Data::AssetId assetId)
|
||||
{
|
||||
if (assetId.IsValid())
|
||||
{
|
||||
|
||||
@@ -12,30 +12,31 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Asset/AssetCommon.h>
|
||||
#include <AzCore/std/string/string.h>
|
||||
#include <Multiplayer/INetworkSpawnableLibrary.h>
|
||||
#include <AzFramework/Asset/AssetCatalogBus.h>
|
||||
#include <AzCore/Name/Name.h>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
/// Implementation of the network prefab library interface.
|
||||
class NetworkSpawnableLibrary final
|
||||
: private AzFramework::AssetCatalogEventBus::Handler
|
||||
: public INetworkSpawnableLibrary
|
||||
, private AzFramework::AssetCatalogEventBus::Handler
|
||||
{
|
||||
public:
|
||||
AZ_RTTI(NetworkSpawnableLibrary, "{65E15F33-E893-49C2-A8E2-B6A8A6EF31E0}", INetworkSpawnableLibrary);
|
||||
|
||||
NetworkSpawnableLibrary();
|
||||
~NetworkSpawnableLibrary();
|
||||
|
||||
void BuildPrefabsList();
|
||||
void ProcessSpawnableAsset(const AZStd::string& relativePath, AZ::Data::AssetId id);
|
||||
/// INetworkSpawnableLibrary overrides.
|
||||
void BuildSpawnablesList() override;
|
||||
void ProcessSpawnableAsset(const AZStd::string& relativePath, AZ::Data::AssetId id) override;
|
||||
AZ::Name GetSpawnableNameFromAssetId(AZ::Data::AssetId assetId) override;
|
||||
AZ::Data::AssetId GetAssetIdByName(AZ::Name name) override;
|
||||
|
||||
/// AssetCatalogEventBus overrides.
|
||||
void OnCatalogLoaded(const char* catalogFile) override;
|
||||
|
||||
AZ::Name GetPrefabNameFromAssetId(AZ::Data::AssetId assetId);
|
||||
AZ::Data::AssetId GetAssetIdByName(AZ::Name name);
|
||||
|
||||
private:
|
||||
AZStd::unordered_map<AZ::Name, AZ::Data::AssetId> m_spawnables;
|
||||
AZStd::unordered_map<AZ::Data::AssetId, AZ::Name> m_spawnablesReverseLookup;
|
||||
|
||||
@@ -11,7 +11,12 @@
|
||||
*/
|
||||
|
||||
#include <Source/Pipeline/NetBindMarkerComponent.h>
|
||||
#include <AzCore/Asset/AssetManager.h>
|
||||
#include <AzCore/Serialization/SerializeContext.h>
|
||||
#include <Multiplayer/IMultiplayer.h>
|
||||
#include <Multiplayer/INetworkSpawnableLibrary.h>
|
||||
#include <AzCore/Component/TransformBus.h>
|
||||
#include <AzFramework/Components/TransformComponent.h>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
@@ -21,15 +26,91 @@ namespace Multiplayer
|
||||
if (serializeContext)
|
||||
{
|
||||
serializeContext->Class<NetBindMarkerComponent, AZ::Component>()
|
||||
->Version(1);
|
||||
->Version(1)
|
||||
->Field("NetEntityIndex", &NetBindMarkerComponent::m_netEntityIndex)
|
||||
->Field("NetSpawnableAsset", &NetBindMarkerComponent::m_networkSpawnableAsset);
|
||||
}
|
||||
}
|
||||
|
||||
AzFramework::Spawnable* GetSpawnableFromAsset(AZ::Data::Asset<AzFramework::Spawnable>& asset)
|
||||
{
|
||||
AzFramework::Spawnable* spawnable = asset.GetAs<AzFramework::Spawnable>();
|
||||
if (!spawnable)
|
||||
{
|
||||
asset =
|
||||
AZ::Data::AssetManager::Instance().GetAsset<AzFramework::Spawnable>(asset.GetId(), AZ::Data::AssetLoadBehavior::PreLoad);
|
||||
AZ::Data::AssetManager::Instance().BlockUntilLoadComplete(asset);
|
||||
|
||||
spawnable = asset.GetAs<AzFramework::Spawnable>();
|
||||
}
|
||||
|
||||
return spawnable;
|
||||
}
|
||||
|
||||
|
||||
void NetBindMarkerComponent::Activate()
|
||||
{
|
||||
const auto agentType = AZ::Interface<IMultiplayer>::Get()->GetAgentType();
|
||||
const bool spawnImmediately =
|
||||
(agentType == MultiplayerAgentType::ClientServer || agentType == MultiplayerAgentType::DedicatedServer);
|
||||
|
||||
if (spawnImmediately && m_networkSpawnableAsset.GetId().IsValid())
|
||||
{
|
||||
AZ::Transform worldTm = GetEntity()->FindComponent<AzFramework::TransformComponent>()->GetWorldTM();
|
||||
auto preInsertionCallback =
|
||||
[worldTm = AZStd::move(worldTm), netEntityIndex = m_netEntityIndex, spawnableAssetId = m_networkSpawnableAsset.GetId()]
|
||||
(AzFramework::EntitySpawnTicket&, AzFramework::SpawnableEntityContainerView entities)
|
||||
{
|
||||
if (entities.size() == 1)
|
||||
{
|
||||
AZ::Entity* netEntity = *entities.begin();
|
||||
|
||||
auto* transformComponent = netEntity->FindComponent<AzFramework::TransformComponent>();
|
||||
transformComponent->SetWorldTM(worldTm);
|
||||
|
||||
AZ::Name spawnableName = AZ::Interface<INetworkSpawnableLibrary>::Get()->GetSpawnableNameFromAssetId(spawnableAssetId);
|
||||
PrefabEntityId prefabEntityId;
|
||||
prefabEntityId.m_prefabName = spawnableName;
|
||||
prefabEntityId.m_entityOffset = netEntityIndex;
|
||||
AZ::Interface<INetworkEntityManager>::Get()->SetupNetEntity(netEntity, prefabEntityId, NetEntityRole::Authority);
|
||||
}
|
||||
else
|
||||
{
|
||||
AZ_Error("NetBindMarkerComponent", false, "Requested to spawn 1 entity, but received %d", entities.size());
|
||||
}
|
||||
};
|
||||
|
||||
m_netSpawnTicket = AzFramework::EntitySpawnTicket(m_networkSpawnableAsset);
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->SpawnEntities(m_netSpawnTicket, {m_netEntityIndex}, preInsertionCallback);
|
||||
}
|
||||
}
|
||||
|
||||
void NetBindMarkerComponent::Deactivate()
|
||||
{
|
||||
if(m_netSpawnTicket.IsValid())
|
||||
{
|
||||
AzFramework::SpawnableEntitiesInterface::Get()->DespawnAllEntities(m_netSpawnTicket);
|
||||
}
|
||||
}
|
||||
|
||||
size_t NetBindMarkerComponent::GetNetEntityIndex() const
|
||||
{
|
||||
return m_netEntityIndex;
|
||||
}
|
||||
|
||||
void NetBindMarkerComponent::SetNetEntityIndex(size_t netEntityIndex)
|
||||
{
|
||||
m_netEntityIndex = netEntityIndex;
|
||||
}
|
||||
|
||||
void NetBindMarkerComponent::SetNetworkSpawnableAsset(AZ::Data::Asset<AzFramework::Spawnable> networkSpawnableAsset)
|
||||
{
|
||||
m_networkSpawnableAsset = networkSpawnableAsset;
|
||||
}
|
||||
|
||||
AZ::Data::Asset<AzFramework::Spawnable> NetBindMarkerComponent::GetNetworkSpawnableAsset() const
|
||||
{
|
||||
return m_networkSpawnableAsset;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@
|
||||
#pragma once
|
||||
|
||||
#include <AzCore/Component/Component.h>
|
||||
#include <AzCore/Asset/AssetCommon.h>
|
||||
#include <AzFramework/Spawnable/Spawnable.h>
|
||||
#include <AzFramework/Spawnable/SpawnableEntitiesInterface.h>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
@@ -34,6 +37,15 @@ namespace Multiplayer
|
||||
void Deactivate() override;
|
||||
//! @}
|
||||
|
||||
size_t GetNetEntityIndex() const;
|
||||
void SetNetEntityIndex(size_t val);
|
||||
|
||||
void SetNetworkSpawnableAsset(AZ::Data::Asset<AzFramework::Spawnable> networkSpawnableAsset);
|
||||
AZ::Data::Asset<AzFramework::Spawnable> GetNetworkSpawnableAsset() const;
|
||||
|
||||
private:
|
||||
AZ::Data::Asset<AzFramework::Spawnable> m_networkSpawnableAsset{AZ::Data::AssetLoadBehavior::PreLoad};
|
||||
size_t m_netEntityIndex = 0;
|
||||
AzFramework::EntitySpawnTicket m_netSpawnTicket;
|
||||
};
|
||||
} // namespace Multiplayer
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace Multiplayer
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void NetworkPrefabProcessor::ProcessPrefab(PrefabProcessorContext& context, AZStd::string_view prefabName, PrefabDom& prefab)
|
||||
{
|
||||
using namespace AzToolsFramework::Prefab;
|
||||
@@ -113,29 +114,35 @@ namespace Multiplayer
|
||||
|
||||
AZStd::unique_ptr<Instance> networkInstance(aznew Instance());
|
||||
|
||||
for (auto entityId : networkedEntityIds)
|
||||
{
|
||||
AZ::Entity* netEntity = sourceInstance->DetachEntity(entityId).release();
|
||||
AZ::Data::Asset<AzFramework::Spawnable> networkSpawnableAsset;
|
||||
networkSpawnableAsset.Create(networkSpawnable->GetId());
|
||||
networkSpawnableAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::PreLoad);
|
||||
|
||||
for (size_t entityIndex = 0; entityIndex < networkedEntityIds.size(); ++entityIndex)
|
||||
{
|
||||
AZ::EntityId entityId = networkedEntityIds[entityIndex];
|
||||
|
||||
AZ::Entity* netEntity = sourceInstance->DetachEntity(entityId).release();
|
||||
// Net entity will need a new ID to avoid IDs collision
|
||||
netEntity->SetId(AZ::Entity::MakeId());
|
||||
networkInstance->AddEntity(*netEntity);
|
||||
|
||||
AZ::Entity* breadcrumbEntity = aznew AZ::Entity(netEntity->GetName());
|
||||
// Use the old ID for the breadcrumb entity to keep parent-child relationship in the original spawnable
|
||||
AZ::Entity* breadcrumbEntity = aznew AZ::Entity(entityId, netEntity->GetName());
|
||||
breadcrumbEntity->SetRuntimeActiveByDefault(netEntity->IsRuntimeActiveByDefault());
|
||||
breadcrumbEntity->CreateComponent<NetBindMarkerComponent>();
|
||||
|
||||
NetBindMarkerComponent* netBindMarkerComponent = breadcrumbEntity->CreateComponent<NetBindMarkerComponent>();
|
||||
// Each spawnable has a root meta-data entity at position 0, so starting net indices from 1
|
||||
netBindMarkerComponent->SetNetEntityIndex(entityIndex + 1);
|
||||
netBindMarkerComponent->SetNetworkSpawnableAsset(networkSpawnableAsset);
|
||||
AzFramework::TransformComponent* transformComponent = netEntity->FindComponent<AzFramework::TransformComponent>();
|
||||
breadcrumbEntity->CreateComponent<AzFramework::TransformComponent>(*transformComponent);
|
||||
|
||||
// TODO: Configure NetBindMarkerComponent to refer to the net entity
|
||||
|
||||
sourceInstance->AddEntity(*breadcrumbEntity);
|
||||
}
|
||||
|
||||
// Add net spawnable asset holder
|
||||
{
|
||||
AZ::Data::AssetId assetId = networkSpawnable->GetId();
|
||||
AZ::Data::Asset<AzFramework::Spawnable> networkSpawnableAsset;
|
||||
networkSpawnableAsset.Create(assetId);
|
||||
networkSpawnableAsset.SetAutoLoadBehavior(AZ::Data::AssetLoadBehavior::PreLoad);
|
||||
|
||||
EntityOptionalReference containerEntityRef = sourceInstance->GetContainerEntity();
|
||||
if (containerEntityRef.has_value())
|
||||
{
|
||||
@@ -175,6 +182,9 @@ namespace Multiplayer
|
||||
(*it)->InvalidateDependencies();
|
||||
(*it)->EvaluateDependencies();
|
||||
}
|
||||
|
||||
SpawnableUtils::SortEntitiesByTransformHierarchy(*networkSpawnable);
|
||||
|
||||
context.GetProcessedObjects().push_back(AZStd::move(object));
|
||||
}
|
||||
else
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzCore/UnitTest/UnitTest.h>
|
||||
#include <AzQtComponents/Utilities/QtPluginPaths.h>
|
||||
#include <AzTest/AzTest.h>
|
||||
#include <AzTest/GemTestEnvironment.h>
|
||||
#include <QApplication>
|
||||
#include <Multiplayer/Components/NetBindComponent.h>
|
||||
#include <Source/Pipeline/NetBindMarkerComponent.h>
|
||||
#include <Source/Pipeline/NetworkSpawnableHolderComponent.h>
|
||||
#include <UnitTest/ToolsTestApplication.h>
|
||||
|
||||
namespace Multiplayer
|
||||
{
|
||||
class MultiplayerToolsTestEnvironment : public AZ::Test::GemTestEnvironment
|
||||
{
|
||||
AZ::ComponentApplication* CreateApplicationInstance() override
|
||||
{
|
||||
return aznew UnitTest::ToolsTestApplication("MultiplayerToolsTest");
|
||||
}
|
||||
|
||||
void AddGemsAndComponents() override
|
||||
{
|
||||
AZStd::vector<AZ::ComponentDescriptor*> descriptors({
|
||||
NetBindComponent::CreateDescriptor(),
|
||||
NetBindMarkerComponent::CreateDescriptor(),
|
||||
NetworkSpawnableHolderComponent::CreateDescriptor()
|
||||
});
|
||||
|
||||
AddComponentDescriptors(descriptors);
|
||||
}
|
||||
};
|
||||
} // namespace UnitTest
|
||||
|
||||
// Required to support running integration tests with Qt
|
||||
AZTEST_EXPORT int AZ_UNIT_TEST_HOOK_NAME(int argc, char** argv)
|
||||
{
|
||||
::testing::InitGoogleMock(&argc, argv);
|
||||
AzQtComponents::PrepareQtPaths();
|
||||
QApplication app(argc, argv);
|
||||
AZ::Test::printUnusedParametersWarning(argc, argv);
|
||||
AZ::Test::addTestEnvironments({new Multiplayer::MultiplayerToolsTestEnvironment});
|
||||
int result = RUN_ALL_TESTS();
|
||||
return result;
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
|
||||
* its licensors.
|
||||
*
|
||||
* For complete copyright and license terms please see the LICENSE at the root of this
|
||||
* distribution (the "License"). All use of this software is governed by the License,
|
||||
* or, if provided, by the license below or the license accompanying this file. Do not
|
||||
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
*
|
||||
*/
|
||||
|
||||
#include <AzFramework/Components/TransformComponent.h>
|
||||
#include <AzFramework/Spawnable/Spawnable.h>
|
||||
#include <AzToolsFramework/Prefab/PrefabSystemComponentInterface.h>
|
||||
#include <AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h>
|
||||
#include <Prefab/PrefabDomTypes.h>
|
||||
#include <Prefab/Spawnable/PrefabProcessorContext.h>
|
||||
#include <Multiplayer/Components/NetBindComponent.h>
|
||||
#include <Source/Pipeline/NetworkPrefabProcessor.h>
|
||||
|
||||
namespace UnitTest
|
||||
{
|
||||
class PrefabProcessingTestFixture : public ::testing::Test
|
||||
{
|
||||
public:
|
||||
static void ConvertEntitiesToPrefab(const AZStd::vector<AZ::Entity*>& entities, AzToolsFramework::Prefab::PrefabDom& prefabDom)
|
||||
{
|
||||
auto* prefabSystem = AZ::Interface<AzToolsFramework::Prefab::PrefabSystemComponentInterface>::Get();
|
||||
AZStd::unique_ptr<AzToolsFramework::Prefab::Instance> sourceInstance(prefabSystem->CreatePrefab(entities, {}, "test/path"));
|
||||
ASSERT_TRUE(sourceInstance);
|
||||
|
||||
auto& prefabTemplateDom = prefabSystem->FindTemplateDom(sourceInstance->GetTemplateId());
|
||||
prefabDom.CopyFrom(prefabTemplateDom, prefabDom.GetAllocator());
|
||||
}
|
||||
|
||||
static AZ::Entity* CreateSourceEntity(const char* name, bool networked, const AZ::Transform& tm, AZ::Entity* parent = nullptr)
|
||||
{
|
||||
AZ::Entity* entity = aznew AZ::Entity(name);
|
||||
auto* transformComponent = entity->CreateComponent<AzFramework::TransformComponent>();
|
||||
|
||||
if (parent)
|
||||
{
|
||||
transformComponent->SetParent(parent->GetId());
|
||||
transformComponent->SetLocalTM(tm);
|
||||
}
|
||||
else
|
||||
{
|
||||
transformComponent->SetWorldTM(tm);
|
||||
}
|
||||
|
||||
if(networked)
|
||||
{
|
||||
entity->CreateComponent<Multiplayer::NetBindComponent>();
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PrefabProcessingTestFixture, NetworkPrefabProcessor_ProcessPrefabTwoEntities_NetEntityGoesToNetSpawnable)
|
||||
{
|
||||
using AzToolsFramework::Prefab::PrefabConversionUtils::PrefabProcessorContext;
|
||||
|
||||
AZStd::vector<AZ::Entity*> entities;
|
||||
|
||||
// Create test entities: 1 networked and 1 static
|
||||
const AZStd::string staticEntityName = "static_floor";
|
||||
entities.emplace_back(CreateSourceEntity(staticEntityName.c_str(), false, AZ::Transform::CreateIdentity()));
|
||||
|
||||
const AZStd::string netEntityName = "networked_entity";
|
||||
entities.emplace_back(CreateSourceEntity(netEntityName.c_str(), true, AZ::Transform::CreateIdentity()));
|
||||
|
||||
// Convert the entities into prefab. Note: This will transfer the ownership of AZ::Entity* into Prefab
|
||||
AzToolsFramework::Prefab::PrefabDom prefabDom;
|
||||
ConvertEntitiesToPrefab(entities, prefabDom);
|
||||
|
||||
// Add the prefab into the Prefab Processor Context
|
||||
const AZStd::string prefabName = "testPrefab";
|
||||
PrefabProcessorContext prefabProcessorContext{AZ::Uuid::CreateRandom()};
|
||||
prefabProcessorContext.AddPrefab(prefabName, AZStd::move(prefabDom));
|
||||
|
||||
// Request NetworkPrefabProcessor to process the prefab
|
||||
Multiplayer::NetworkPrefabProcessor processor;
|
||||
processor.Process(prefabProcessorContext);
|
||||
|
||||
// Validate results
|
||||
EXPECT_TRUE(prefabProcessorContext.HasCompletedSuccessfully());
|
||||
|
||||
// Should be 1 networked spawnable
|
||||
const auto& processedObjects = prefabProcessorContext.GetProcessedObjects();
|
||||
EXPECT_EQ(processedObjects.size(), 1);
|
||||
|
||||
// Verify the name and the type of the spawnable asset
|
||||
const AZ::Data::AssetData& spawnableAsset = processedObjects[0].GetAsset();
|
||||
EXPECT_EQ(prefabName + ".network.spawnable", processedObjects[0].GetId());
|
||||
EXPECT_EQ(spawnableAsset.GetType(), azrtti_typeid<AzFramework::Spawnable>());
|
||||
|
||||
// Verify we have only the networked entity in the network spawnable and not the static one
|
||||
const AzFramework::Spawnable* netSpawnable = azrtti_cast<const AzFramework::Spawnable*>(&spawnableAsset);
|
||||
const AzFramework::Spawnable::EntityList& entityList = netSpawnable->GetEntities();
|
||||
auto countEntityCallback = [](const auto& name)
|
||||
{
|
||||
return [name](const auto& entity)
|
||||
{
|
||||
return entity->GetName() == name;
|
||||
};
|
||||
};
|
||||
|
||||
EXPECT_EQ(0, AZStd::count_if(entityList.begin(), entityList.end(), countEntityCallback(staticEntityName)));
|
||||
EXPECT_EQ(1, AZStd::count_if(entityList.begin(), entityList.end(), countEntityCallback(netEntityName)));
|
||||
}
|
||||
|
||||
} // namespace UnitTest
|
||||
@@ -22,6 +22,7 @@ set(FILES
|
||||
Include/Multiplayer/ConnectionData/IConnectionData.h
|
||||
Include/Multiplayer/EntityDomains/IEntityDomain.h
|
||||
Include/Multiplayer/NetworkEntity/INetworkEntityManager.h
|
||||
Include/Multiplayer/INetworkSpawnableLibrary.h
|
||||
Include/Multiplayer/NetworkEntity/NetworkEntityRpcMessage.h
|
||||
Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h
|
||||
Include/Multiplayer/NetworkEntity/NetworkEntityHandle.h
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
set(FILES
|
||||
Tests/MainTools.cpp
|
||||
Tests/PrefabProcessingTests.cpp
|
||||
)
|
||||
+37
-15
@@ -13,7 +13,34 @@ if(NOT PAL_TRAIT_BUILD_CPACK_SUPPORTED)
|
||||
return()
|
||||
endif()
|
||||
|
||||
ly_get_absolute_pal_filename(pal_dir ${CMAKE_SOURCE_DIR}/cmake/Platform/${PAL_HOST_PLATFORM_NAME})
|
||||
# public facing options will be used for conversion into cpack specific ones below.
|
||||
set(LY_INSTALLER_DOWNLOAD_URL "" CACHE STRING "URL embedded into the installer to download additional artifacts")
|
||||
set(LY_INSTALLER_LICENSE_URL "" CACHE STRING "Optionally embed a link to the license instead of raw text")
|
||||
|
||||
# set all common cpack variable overrides first so they can be accessible via configure_file
|
||||
# when the platform specific settings are applied below. additionally, any variable with
|
||||
# the "CPACK_" prefix will automatically be cached for use in any phase of cpack namely
|
||||
# pre/post build
|
||||
set(CPACK_PACKAGE_VENDOR "${PROJECT_NAME}")
|
||||
set(CPACK_PACKAGE_VERSION "${LY_VERSION_STRING}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Installation Tool")
|
||||
|
||||
string(TOLOWER ${PROJECT_NAME} _project_name_lower)
|
||||
set(CPACK_PACKAGE_FILE_NAME "${_project_name_lower}_${LY_VERSION_STRING}_installer")
|
||||
|
||||
set(DEFAULT_LICENSE_NAME "Apache-2.0")
|
||||
set(DEFAULT_LICENSE_FILE "${CMAKE_SOURCE_DIR}/LICENSE.txt")
|
||||
|
||||
set(CPACK_RESOURCE_FILE_LICENSE ${DEFAULT_LICENSE_FILE})
|
||||
set(CPACK_LICENSE_URL ${LY_INSTALLER_LICENSE_URL})
|
||||
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_VENDOR}/${CPACK_PACKAGE_VERSION}")
|
||||
|
||||
# CMAKE_SOURCE_DIR doesn't equate to anything during execution of pre/post build scripts
|
||||
set(CPACK_SOURCE_DIR ${CMAKE_SOURCE_DIR}/cmake)
|
||||
|
||||
# attempt to apply platform specific settings
|
||||
ly_get_absolute_pal_filename(pal_dir ${CPACK_SOURCE_DIR}/Platform/${PAL_HOST_PLATFORM_NAME})
|
||||
include(${pal_dir}/Packaging_${PAL_HOST_PLATFORM_NAME_LOWERCASE}.cmake)
|
||||
|
||||
# if we get here and the generator hasn't been set, then a non fatal error occurred disabling packaging support
|
||||
@@ -21,20 +48,6 @@ if(NOT CPACK_GENERATOR)
|
||||
return()
|
||||
endif()
|
||||
|
||||
set(CPACK_PACKAGE_VENDOR "${PROJECT_NAME}")
|
||||
set(CPACK_PACKAGE_VERSION "${LY_VERSION_STRING}")
|
||||
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Installation Tool")
|
||||
|
||||
string(TOLOWER ${PROJECT_NAME} _project_name_lower)
|
||||
set(CPACK_PACKAGE_FILE_NAME "${_project_name_lower}_installer")
|
||||
|
||||
set(DEFAULT_LICENSE_NAME "Apache-2.0")
|
||||
set(DEFAULT_LICENSE_FILE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.txt")
|
||||
|
||||
set(CPACK_RESOURCE_FILE_LICENSE ${DEFAULT_LICENSE_FILE})
|
||||
|
||||
set(CPACK_PACKAGE_INSTALL_DIRECTORY "${CPACK_PACKAGE_VENDOR}/${CPACK_PACKAGE_VERSION}")
|
||||
|
||||
# IMPORTANT: required to be included AFTER setting all property overrides
|
||||
include(CPack REQUIRED)
|
||||
|
||||
@@ -76,3 +89,12 @@ ly_configure_cpack_component(
|
||||
DISPLAY_NAME "${PROJECT_NAME} Core"
|
||||
DESCRIPTION "${PROJECT_NAME} Headers, Libraries and Tools"
|
||||
)
|
||||
|
||||
if(LY_INSTALLER_DOWNLOAD_URL)
|
||||
# this will set the following variables: CPACK_DOWNLOAD_SITE, CPACK_DOWNLOAD_ALL, and CPACK_UPLOAD_DIRECTORY
|
||||
cpack_configure_downloads(
|
||||
${LY_INSTALLER_DOWNLOAD_URL}
|
||||
UPLOAD_DIRECTORY ${CMAKE_BINARY_DIR}/_CPack_Uploads # to match the _CPack_Packages directory
|
||||
ALL
|
||||
)
|
||||
endif()
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<?include "cpack_variables.wxi"?>
|
||||
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
|
||||
xmlns:bal="http://schemas.microsoft.com/wix/BalExtension">
|
||||
|
||||
<Bundle Name="$(var.CPACK_PACKAGE_NAME) $(var.CPACK_PACKAGE_VERSION)"
|
||||
Version="$(var.CPACK_PACKAGE_VERSION)"
|
||||
Manufacturer="$(var.CPACK_PACKAGE_VENDOR)"
|
||||
UpgradeCode="$(var.CPACK_BOOTSTRAP_UPGRADE_GUID)"
|
||||
DisableModify="yes">
|
||||
|
||||
<Variable Name="InstallFolder"
|
||||
Type="string"
|
||||
Value="[ProgramFiles64Folder]$(var.CPACK_PACKAGE_INSTALL_DIRECTORY)"
|
||||
bal:Overridable="yes"/>
|
||||
|
||||
<?ifdef CPACK_LICENSE_URL?>
|
||||
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.HyperlinkLicense">
|
||||
<bal:WixStandardBootstrapperApplication
|
||||
LicenseUrl="$(var.CPACK_LICENSE_URL)"
|
||||
ShowVersion="yes" />
|
||||
</BootstrapperApplicationRef>
|
||||
<?else?>
|
||||
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense">
|
||||
<bal:WixStandardBootstrapperApplication
|
||||
LicenseFile="$(var.CPACK_WIX_LICENSE_RTF)"
|
||||
ShowVersion="yes" />
|
||||
</BootstrapperApplicationRef>
|
||||
<?endif?>
|
||||
|
||||
<Chain>
|
||||
<MsiPackage Id="$(var.CPACK_PACKAGE_FILE_NAME)"
|
||||
SourceFile="$(var.CPACK_LOCAL_INSTALLER_DIR)/$(var.CPACK_PACKAGE_FILE_NAME).msi"
|
||||
DownloadUrl="$(var.CPACK_DOWNLOAD_SITE)/{2}"
|
||||
Vital="yes"
|
||||
Compressed="no">
|
||||
|
||||
<MsiProperty Name="INSTALL_ROOT" Value="[InstallFolder]" />
|
||||
</MsiPackage>
|
||||
</Chain>
|
||||
</Bundle>
|
||||
</Wix>
|
||||
@@ -0,0 +1,88 @@
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
|
||||
# convert the path to a windows style path using string replace because TO_NATIVE_PATH
|
||||
# only works on real paths
|
||||
string(REPLACE "/" "\\" _fixed_package_install_dir ${CPACK_PACKAGE_INSTALL_DIRECTORY})
|
||||
|
||||
# directory where the auto generated files live e.g <build>/_CPack_Package/win64/WIX
|
||||
set(_cpack_wix_out_dir ${CPACK_TOPLEVEL_DIRECTORY})
|
||||
set(_bootstrap_out_dir "${CPACK_TOPLEVEL_DIRECTORY}/bootstrap")
|
||||
|
||||
set(_bootstrap_filename "${CPACK_PACKAGE_FILE_NAME}.exe")
|
||||
set(_bootstrap_output_file ${_cpack_wix_out_dir}/${_bootstrap_filename})
|
||||
|
||||
set(_ext_flags
|
||||
-ext WixBalExtension
|
||||
)
|
||||
|
||||
set(_addtional_defines
|
||||
-dCPACK_BOOTSTRAP_UPGRADE_GUID=${CPACK_WIX_BOOTSTRAP_UPGRADE_GUID}
|
||||
-dCPACK_DOWNLOAD_SITE=${CPACK_DOWNLOAD_SITE}
|
||||
-dCPACK_LOCAL_INSTALLER_DIR=${_cpack_wix_out_dir}
|
||||
-dCPACK_PACKAGE_FILE_NAME=${CPACK_PACKAGE_FILE_NAME}
|
||||
-dCPACK_PACKAGE_INSTALL_DIRECTORY=${_fixed_package_install_dir}
|
||||
)
|
||||
|
||||
if(CPACK_LICENSE_URL)
|
||||
list(APPEND _addtional_defines -dCPACK_LICENSE_URL=${CPACK_LICENSE_URL})
|
||||
endif()
|
||||
|
||||
set(_candle_command
|
||||
${CPACK_WIX_CANDLE_EXECUTABLE}
|
||||
-nologo
|
||||
-arch x64
|
||||
"-I${_cpack_wix_out_dir}" # to include cpack_variables.wxi
|
||||
${_addtional_defines}
|
||||
${_ext_flags}
|
||||
"${CPACK_SOURCE_DIR}/Platform/Windows/PackagingBootstrapper.wxs"
|
||||
-o "${_bootstrap_out_dir}/"
|
||||
)
|
||||
|
||||
set(_light_command
|
||||
${CPACK_WIX_LIGHT_EXECUTABLE}
|
||||
-nologo
|
||||
${_ext_flags}
|
||||
${_bootstrap_out_dir}/*.wixobj
|
||||
-o "${_bootstrap_output_file}"
|
||||
)
|
||||
|
||||
message(STATUS "Creating Bootstrap Installer...")
|
||||
execute_process(
|
||||
COMMAND ${_candle_command}
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
execute_process(
|
||||
COMMAND ${_light_command}
|
||||
COMMAND_ERROR_IS_FATAL ANY
|
||||
)
|
||||
|
||||
file(COPY ${_bootstrap_output_file}
|
||||
DESTINATION ${CPACK_PACKAGE_DIRECTORY}
|
||||
)
|
||||
|
||||
message(STATUS "Bootstrap installer generated to ${CPACK_PACKAGE_DIRECTORY}/${_bootstrap_filename}")
|
||||
|
||||
# use the internal default path if somehow not specified from cpack_configure_downloads
|
||||
if(NOT CPACK_UPLOAD_DIRECTORY)
|
||||
set(CPACK_UPLOAD_DIRECTORY ${CPACK_PACKAGE_DIRECTORY}/CPackUploads)
|
||||
endif()
|
||||
|
||||
# copy the artifacts intended to be uploaded to a remote server into the folder specified
|
||||
# through cpack_configure_downloads. this mimics the same process cpack does natively for
|
||||
# some other frameworks that have built-in online installer support.
|
||||
message(STATUS "Copying installer artifacts to upload directory...")
|
||||
file(REMOVE_RECURSE ${CPACK_UPLOAD_DIRECTORY})
|
||||
file(GLOB _artifacts "${_cpack_wix_out_dir}/*.msi" "${_cpack_wix_out_dir}/*.cab")
|
||||
file(COPY ${_artifacts}
|
||||
DESTINATION ${CPACK_UPLOAD_DIRECTORY}
|
||||
)
|
||||
message(STATUS "Artifacts copied to ${CPACK_UPLOAD_DIRECTORY}")
|
||||
@@ -12,10 +12,10 @@
|
||||
Manufacturer="$(var.CPACK_PACKAGE_VENDOR)"
|
||||
UpgradeCode="$(var.CPACK_WIX_UPGRADE_GUID)">
|
||||
|
||||
<Package InstallerVersion="301" Compressed="yes"/>
|
||||
<Package InstallerVersion="301" Compressed="yes" InstallPrivileges="elevated" InstallScope="perMachine"/>
|
||||
|
||||
<!-- auto distribute the installable files across N cab files and embed them in the final MSI -->
|
||||
<MediaTemplate EmbedCab="yes"/>
|
||||
<!-- auto distribute the installable files across N cab files -->
|
||||
<MediaTemplate EmbedCab="$(var.CPACK_EMBED_ARTIFACTS)"/>
|
||||
|
||||
<MajorUpgrade
|
||||
Schedule="afterInstallInitialize"
|
||||
|
||||
@@ -30,34 +30,23 @@ set(CPACK_GENERATOR "WIX")
|
||||
|
||||
# CPack will generate the WiX product/upgrade GUIDs further down the chain if they weren't supplied
|
||||
# however, they are unique for each run. instead, let's do the auto generation here and add it to
|
||||
# the cache for run persistence. an additional cache file will be used to store the information on
|
||||
# the original generation so we still have the ability to detect if they are still being used.
|
||||
set(_guid_cache_file "${CMAKE_BINARY_DIR}/installer/wix_guid_cache.cmake")
|
||||
if(NOT EXISTS ${_guid_cache_file})
|
||||
set(_wix_guid_namespace "6D43F57A-2917-4AD9-B758-1F13CDB08593")
|
||||
# the cache for run persistence and have the ability to detect if they are still being used.
|
||||
set(_wix_guid_namespace "6D43F57A-2917-4AD9-B758-1F13CDB08593")
|
||||
|
||||
# based the ISO-8601 standard (YYYY-MM-DDTHH-mm-ssTZD) e.g., 20210506145533
|
||||
string(TIMESTAMP _guid_gen_timestamp "%Y%m%d%H%M%S")
|
||||
|
||||
file(WRITE ${_guid_cache_file} "set(_wix_guid_gen_timestamp ${_guid_gen_timestamp})\n")
|
||||
|
||||
string(UUID _default_product_guid
|
||||
function(generate_wix_guid out_value seed)
|
||||
string(UUID _guid
|
||||
NAMESPACE ${_wix_guid_namespace}
|
||||
NAME "ProductID_${_guid_gen_timestamp}"
|
||||
NAME ${seed}
|
||||
TYPE SHA1
|
||||
UPPER
|
||||
)
|
||||
file(APPEND ${_guid_cache_file} "set(_wix_default_product_guid ${_default_product_guid})\n")
|
||||
|
||||
string(UUID _default_upgrade_guid
|
||||
NAMESPACE ${_wix_guid_namespace}
|
||||
NAME "UpgradeCode_${_guid_gen_timestamp}"
|
||||
TYPE SHA1
|
||||
UPPER
|
||||
)
|
||||
file(APPEND ${_guid_cache_file} "set(_wix_default_upgrade_guid ${_default_upgrade_guid})\n")
|
||||
endif()
|
||||
include(${_guid_cache_file})
|
||||
set(${out_value} ${_guid} PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
set(_guid_seed_base "${PROJECT_NAME}_${LY_VERSION_STRING}")
|
||||
generate_wix_guid(_wix_default_product_guid "${_guid_seed_base}_ProductID" )
|
||||
generate_wix_guid(_wix_default_upgrade_guid "${_guid_seed_base}_UpgradeCode")
|
||||
|
||||
set(LY_WIX_PRODUCT_GUID "${_wix_default_product_guid}" CACHE STRING "GUID for the Product ID field. Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
|
||||
set(LY_WIX_UPGRADE_GUID "${_wix_default_upgrade_guid}" CACHE STRING "GUID for the Upgrade Code field. Format: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX")
|
||||
@@ -89,4 +78,21 @@ endif()
|
||||
set(CPACK_WIX_PRODUCT_GUID ${LY_WIX_PRODUCT_GUID})
|
||||
set(CPACK_WIX_UPGRADE_GUID ${LY_WIX_UPGRADE_GUID})
|
||||
|
||||
set(CPACK_WIX_TEMPLATE "${CMAKE_SOURCE_DIR}/cmake/Platform/Windows/PackagingTemplate.wxs.in")
|
||||
set(CPACK_WIX_TEMPLATE "${CPACK_SOURCE_DIR}/Platform/Windows/PackagingTemplate.wxs.in")
|
||||
|
||||
set(_embed_artifacts "yes")
|
||||
|
||||
if(LY_INSTALLER_DOWNLOAD_URL)
|
||||
set(_embed_artifacts "no")
|
||||
|
||||
# the bootstrapper will at the very least need a different upgrade guid
|
||||
generate_wix_guid(CPACK_WIX_BOOTSTRAP_UPGRADE_GUID "${_guid_seed_base}_Bootstrap_UpgradeCode")
|
||||
|
||||
set(CPACK_POST_BUILD_SCRIPTS
|
||||
${CPACK_SOURCE_DIR}/Platform/Windows/PackagingPostBuild.cmake
|
||||
)
|
||||
endif()
|
||||
|
||||
set(CPACK_WIX_CANDLE_EXTRA_FLAGS
|
||||
-dCPACK_EMBED_ARTIFACTS=${_embed_artifacts}
|
||||
)
|
||||
|
||||
@@ -24,5 +24,7 @@ set(FILES
|
||||
PALDetection_windows.cmake
|
||||
Install_windows.cmake
|
||||
Packaging_windows.cmake
|
||||
PackagingBootstrapper.wxs
|
||||
PackagingPostBuild.cmake
|
||||
PackagingTemplate.wxs.in
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user