Merge branch 'main' into jckand/FoundationAuto

This commit is contained in:
jckand-amzn
2021-05-24 10:06:09 -05:00
58 changed files with 1501 additions and 342 deletions
@@ -86,4 +86,94 @@ namespace AZ
Normal,
UniformReal
};
//! Halton sequences are deterministic, quasi-random sequences with low discrepancy. They
//! are useful for generating evenly distributed points.
//! See https://en.wikipedia.org/wiki/Halton_sequence for more information.
//! Returns a single halton number.
//! @param index The index of the number. Indices start at 1. Using index 0 will return 0.
//! @param base The numerical base of the halton number.
inline float GetHaltonNumber(uint32_t index, uint32_t base)
{
float fraction = 1.0f;
float result = 0.0f;
while (index > 0)
{
fraction = fraction / base;
result += fraction * (index % base);
index = aznumeric_cast<uint32_t>(index / base);
}
return result;
}
//! A helper class for generating arrays of Halton sequences in n dimensions.
//! The class holds the state of which bases to use, the starting offset
//! of each dimension and how much to increment between each index for each
//! dimension.
template <uint8_t Dimensions>
class HaltonSequence
{
public:
//! Initializes a Halton sequence with some bases. By default there is no
//! offset and the index increments by 1 between each number.
HaltonSequence(AZStd::array<uint32_t, Dimensions> bases)
: m_bases(bases)
{
m_offsets.fill(1); // Halton sequences start at index 1.
m_increments.fill(1); // By default increment by 1 between each number.
}
//! Returns a Halton sequence in an array of N length
template<uint32_t N>
AZStd::array<AZStd::array<float, Dimensions>, N> GetHaltonSequence()
{
AZStd::array<AZStd::array<float, Dimensions>, N> result;
AZStd::array<uint32_t, Dimensions> indices = m_offsets;
// Generator that returns the Halton number for all bases for a single entry.
auto f = [&] ()
{
AZStd::array<float, Dimensions> item;
for (auto d = 0; d < Dimensions; ++d)
{
item[d] = GetHaltonNumber(indices[d], m_bases[d]);
indices[d] += m_increments[d];
}
return item;
};
AZStd::generate(result.begin(), result.end(), f);
return result;
}
//! Sets the offsets per dimension to start generating a sequence from.
//! By default, there is no offset (offset of 0 corresponds to starting at index 1)
void SetOffsets(AZStd::array<uint32_t, Dimensions> offsets)
{
m_offsets = offsets;
// Halton sequences start at index 1, so increment all the indices.
AZStd::for_each(m_offsets.begin(), m_offsets.end(), [](uint32_t &n){ n++; });
}
//! Sets the increment between numbers in the halton sequence per dimension
//! By default this is 1, meaning that no numbers are skipped. Can be negative
//! to generate numbers in reverse order.
void SetIncrements(AZStd::array<int32_t, Dimensions> increments)
{
m_increments = increments;
}
private:
AZStd::array<uint32_t, Dimensions> m_bases;
AZStd::array<uint32_t, Dimensions> m_offsets;
AZStd::array<int32_t, Dimensions> m_increments;
};
}
@@ -0,0 +1,74 @@
/*
* 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/Math/Random.h>
#include <AzCore/UnitTest/TestTypes.h>
using namespace AZ;
namespace UnitTest
{
TEST(MATH_Random, GetHaltonNumber)
{
EXPECT_FLOAT_EQ(0.5, GetHaltonNumber(1, 2));
EXPECT_FLOAT_EQ(898.0f / 2187.0f, GetHaltonNumber(1234, 3));
EXPECT_FLOAT_EQ(5981.0f / 15625.0f, GetHaltonNumber(4321, 5));
}
TEST(MATH_Random, HaltonSequence)
{
HaltonSequence<3> sequence({ 2, 3, 5 });
auto regularSequence = sequence.GetHaltonSequence<5>();
EXPECT_FLOAT_EQ(1.0f / 2.0f, regularSequence[0][0]);
EXPECT_FLOAT_EQ(1.0f / 3.0f, regularSequence[0][1]);
EXPECT_FLOAT_EQ(1.0f / 5.0f, regularSequence[0][2]);
EXPECT_FLOAT_EQ(1.0f / 4.0f, regularSequence[1][0]);
EXPECT_FLOAT_EQ(2.0f / 3.0f, regularSequence[1][1]);
EXPECT_FLOAT_EQ(2.0f / 5.0f, regularSequence[1][2]);
EXPECT_FLOAT_EQ(3.0f / 4.0f, regularSequence[2][0]);
EXPECT_FLOAT_EQ(1.0f / 9.0f, regularSequence[2][1]);
EXPECT_FLOAT_EQ(3.0f / 5.0f, regularSequence[2][2]);
EXPECT_FLOAT_EQ(1.0f / 8.0f, regularSequence[3][0]);
EXPECT_FLOAT_EQ(4.0f / 9.0f, regularSequence[3][1]);
EXPECT_FLOAT_EQ(4.0f / 5.0f, regularSequence[3][2]);
EXPECT_FLOAT_EQ(5.0f / 8.0f, regularSequence[4][0]);
EXPECT_FLOAT_EQ(7.0f / 9.0f, regularSequence[4][1]);
EXPECT_FLOAT_EQ(1.0f / 25.0f, regularSequence[4][2]);
sequence.SetOffsets({ 1, 2, 3 });
auto offsetSequence = sequence.GetHaltonSequence<2>();
EXPECT_FLOAT_EQ(1.0f / 4.0f, offsetSequence[0][0]);
EXPECT_FLOAT_EQ(1.0f / 9.0f, offsetSequence[0][1]);
EXPECT_FLOAT_EQ(4.0f / 5.0f, offsetSequence[0][2]);
EXPECT_FLOAT_EQ(3.0f / 4.0f, offsetSequence[1][0]);
EXPECT_FLOAT_EQ(4.0f / 9.0f, offsetSequence[1][1]);
EXPECT_FLOAT_EQ(1.0f / 25.0f, offsetSequence[1][2]);
sequence.SetIncrements({ 1, 2, 3 });
auto incrementedSequence = sequence.GetHaltonSequence<2>();
EXPECT_FLOAT_EQ(1.0f / 4.0f, incrementedSequence[0][0]);
EXPECT_FLOAT_EQ(1.0f / 9.0f, incrementedSequence[0][1]);
EXPECT_FLOAT_EQ(4.0f / 5.0f, incrementedSequence[0][2]);
EXPECT_FLOAT_EQ(3.0f / 4.0f, incrementedSequence[1][0]);
EXPECT_FLOAT_EQ(7.0f / 9.0f, incrementedSequence[1][1]);
EXPECT_FLOAT_EQ(11.0f / 25.0f, incrementedSequence[1][2]);
}
}
@@ -152,6 +152,7 @@ set(FILES
Math/PlaneTests.cpp
Math/QuaternionPerformanceTests.cpp
Math/QuaternionTests.cpp
Math/RandomTests.cpp
Math/ShapeIntersectionPerformanceTests.cpp
Math/ShapeIntersectionTests.cpp
Math/SfmtTests.cpp
@@ -52,6 +52,21 @@ namespace AzToolsFramework
* Deletes all entities in the provided list, as well as their transform descendants.
*/
virtual void DeleteEntitiesAndAllDescendants(const EntityIdList& entities) = 0;
/**
* Duplicate all currently-selected entities.
*/
virtual void DuplicateSelected() = 0;
/**
* Duplicates the specified entity.
*/
virtual void DuplicateEntityById(AZ::EntityId entityId) = 0;
/**
* Duplicates all specified entities.
*/
virtual void DuplicateEntities(const EntityIdList& entities) = 0;
};
} // namespace AzToolsFramework
@@ -43,7 +43,7 @@ namespace AzToolsFramework
void EditorEntityManager::DeleteEntityById(AZ::EntityId entityId)
{
DeleteEntities({entityId});
DeleteEntities(EntityIdList{ entityId });
}
void EditorEntityManager::DeleteEntities(const EntityIdList& entities)
@@ -53,12 +53,30 @@ namespace AzToolsFramework
void EditorEntityManager::DeleteEntityAndAllDescendants(AZ::EntityId entityId)
{
DeleteEntitiesAndAllDescendants({entityId});
DeleteEntitiesAndAllDescendants(EntityIdList{ entityId });
}
void EditorEntityManager::DeleteEntitiesAndAllDescendants(const EntityIdList& entities)
{
m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entities);
}
void EditorEntityManager::DuplicateSelected()
{
EntityIdList selectedEntities;
ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities);
m_prefabPublicInterface->DuplicateEntitiesInInstance(selectedEntities);
}
void EditorEntityManager::DuplicateEntityById(AZ::EntityId entityId)
{
DuplicateEntities(EntityIdList{ entityId });
}
void EditorEntityManager::DuplicateEntities(const EntityIdList& entities)
{
m_prefabPublicInterface->DuplicateEntitiesInInstance(entities);
}
}
@@ -31,6 +31,9 @@ namespace AzToolsFramework
void DeleteEntities(const EntityIdList& entities) override;
void DeleteEntityAndAllDescendants(AZ::EntityId entityId) override;
void DeleteEntitiesAndAllDescendants(const EntityIdList& entities) override;
void DuplicateSelected() override;
void DuplicateEntityById(AZ::EntityId entityId) override;
void DuplicateEntities(const EntityIdList& entities) override;
private:
Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr;
@@ -28,6 +28,7 @@ namespace AzToolsFramework
inline static const char* PatchesName = "Patches";
inline static const char* SourceName = "Source";
inline static const char* LinkIdName = "LinkId";
inline static const char* EntityIdName = "Id";
inline static const char* EntitiesName = "Entities";
inline static const char* ContainerEntityName = "ContainerEntity";
@@ -13,6 +13,8 @@
#include <AzToolsFramework/Prefab/PrefabPublicHandler.h>
#include <AzCore/Component/TransformBus.h>
#include <AzCore/JSON/stringbuffer.h>
#include <AzCore/JSON/writer.h>
#include <AzCore/Utils/TypeHash.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
@@ -31,6 +33,8 @@
#include <AzToolsFramework/Prefab/PrefabUndoHelpers.h>
#include <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <QString>
namespace AzToolsFramework
{
namespace Prefab
@@ -83,7 +87,8 @@ namespace AzToolsFramework
commonRootInstanceDomBeforeCreate, commonRootEntityOwningInstance->get());
AZStd::vector<AZ::Entity*> entities;
AZStd::vector<AZStd::unique_ptr<Instance>> instances;
AZStd::vector<AZStd::unique_ptr<Instance>> instancePtrs;
AZStd::vector<Instance*> instances;
AZStd::unordered_map<Instance*, PrefabDom> nestedInstanceLinkPatchesMap;
// Retrieve all entities affected and identify Instances
@@ -93,10 +98,18 @@ namespace AzToolsFramework
AZStd::string("Could not create a new prefab out of the entities provided - invalid selection."));
}
// Detach the retrieved entities
for (AZ::Entity* entity : entities)
{
commonRootEntityOwningInstance->get().DetachEntity(entity->GetId()).release();
}
// When we create a prefab with other prefab instances, we have to remove the existing links between the source and
// target templates of the other instances.
for (auto& nestedInstance : instances)
{
AZStd::unique_ptr<Instance> outInstance = commonRootEntityOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
auto linkRef = m_prefabSystemComponentInterface->FindLink(nestedInstance->GetLinkId());
if (linkRef.has_value())
@@ -104,10 +117,12 @@ namespace AzToolsFramework
PrefabDom oldLinkPatches;
oldLinkPatches.CopyFrom(linkRef->get().GetLinkDom(), oldLinkPatches.GetAllocator());
nestedInstanceLinkPatchesMap.emplace(nestedInstance.get(), AZStd::move(oldLinkPatches));
nestedInstanceLinkPatchesMap.emplace(nestedInstance, AZStd::move(oldLinkPatches));
}
RemoveLink(nestedInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
RemoveLink(outInstance, commonRootEntityOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
instancePtrs.emplace_back(AZStd::move(outInstance));
}
PrefabUndoHelpers::UpdatePrefabInstance(
@@ -123,7 +138,7 @@ namespace AzToolsFramework
// Create the Prefab
instanceToCreate = prefabEditorEntityOwnershipInterface->CreatePrefab(
entities, AZStd::move(instances), filePath, commonRootEntityOwningInstance);
entities, AZStd::move(instancePtrs), filePath, commonRootEntityOwningInstance);
if (!instanceToCreate)
{
@@ -388,7 +403,7 @@ namespace AzToolsFramework
// Find common root and top level entities
bool entitiesHaveCommonRoot = false;
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(
AzToolsFramework::ToolsApplicationRequestBus::BroadcastResult(
entitiesHaveCommonRoot, &AzToolsFramework::ToolsApplicationRequests::FindCommonRootInactive, inputEntityList,
commonRootEntityId, &topLevelEntities);
@@ -710,6 +725,151 @@ namespace AzToolsFramework
return DeleteFromInstance(entityIds, true);
}
PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds)
{
if (entityIds.empty())
{
return AZ::Failure(AZStd::string("No entities to duplicate."));
}
if (!EntitiesBelongToSameInstance(entityIds))
{
return AZ::Failure(AZStd::string("Cannot duplicate multiple "
"entities belonging to different instances with one operation."));
}
// We've already verified the entities are all owned by the same instance,
// so we can just retrieve our instance from the first entity in the list.
InstanceOptionalReference commonEntityOwningInstance = GetOwnerInstanceByEntityId(entityIds[0]);
AZ_Assert(
commonEntityOwningInstance.has_value(),
"Failed to duplicate : Couldn't get a valid owning instance for the common root entity of the entities provided");
// This will cull out any entities that have ancestors in the list, since we will end up duplicating
// the full nested hierarchy with what is returned from RetrieveAndSortPrefabEntitiesAndInstances
AzToolsFramework::EntityIdSet duplicationSet = AzToolsFramework::GetCulledEntityHierarchy(entityIds);
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
ScopedUndoBatch undoBatch("Duplicate Entities");
{
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities");
// Take a snapshot of the instance DOM before we manipulate it
Prefab::PrefabDom instanceDomBefore;
m_instanceToTemplateInterface->GenerateDomForInstance(instanceDomBefore, commonEntityOwningInstance->get());
AZStd::vector<AZ::Entity*> entities;
AZStd::vector<Instance*> instances;
// Gather all entities/instances in the hierarchy, but don't detach them because we are duplicating not deleting.
EntityList inputEntityList = EntityIdSetToEntityList(duplicationSet);
bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonEntityOwningInstance->get(), entities, instances);
if (!success)
{
return AZ::Failure(AZStd::string("Failed to retrieve entities and instances from the given list of entity ids for duplication"));
}
// Make a copy of our before instance DOM where we will add our duplicated entities
Prefab::PrefabDom instanceDomAfter;
instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator());
AZStd::unordered_map<EntityAlias, EntityAlias> oldAliasToNewAliasMap;
AZStd::unordered_map<EntityAlias, QString> aliasToEntityDomMap;
for (AZ::Entity* entity : entities)
{
EntityAliasOptionalReference oldAliasRef = commonEntityOwningInstance->get().GetEntityAlias(entity->GetId());
AZ_Assert(oldAliasRef.has_value(), "No alias found for Entity in the DOM");
EntityAlias oldAlias = oldAliasRef.value();
// Give this the outer allocator so that the memory reference will be valid when
// it gets used for AddMember
Prefab::PrefabDom entityDomBefore(&instanceDomAfter.GetAllocator());
m_instanceToTemplateInterface->GenerateDomForEntity(entityDomBefore, *entity);
// Keep track of the old alias <-> new alias mapping for this duplicated entity
// so we can fixup references later
EntityAlias newEntityAlias = Instance::GenerateEntityAlias();
oldAliasToNewAliasMap.insert(AZStd::make_pair(oldAlias, newEntityAlias));
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
entityDomBefore.Accept(writer);
// Store our duplicated Entity DOM with its new alias as a string
// so that we can fixup entity alias references before adding it
// to the Entities member of our instance DOM
QString entityDomString(buffer.GetString());
aliasToEntityDomMap.insert(AZStd::make_pair(newEntityAlias, entityDomString));
}
auto entitiesIter = instanceDomAfter.FindMember(PrefabDomUtils::EntitiesName);
AZ_Assert(entitiesIter != instanceDomAfter.MemberEnd(), "Instance DOM missing the Entities member.");
// Now that all the duplicated Entity DOMs have been created, we need to iterate
// through them and replace any previous EntityAlias references with the new ones.
// These are more than just parent entity references for nested entities, this will
// also cover any EntityId references that were made in the components between them.
for (auto aliasEntityPair : aliasToEntityDomMap)
{
EntityAlias newEntityAlias = aliasEntityPair.first;
QString newEntityDomString = aliasEntityPair.second;
// Replace all of the old alias references with the new ones
// We bookend the aliases with \" and also with a / as an extra precaution to prevent
// inadvertently replacing a matching string vs. where an actual EntityId is expected
// This will cover both cases where an alias could be used in a normal entity vs. an instance
for (auto aliasMapIter : oldAliasToNewAliasMap)
{
QString oldAliasQuotes = QString("\"%1\"").arg(aliasMapIter.first.c_str());
QString newAliasQuotes = QString("\"%1\"").arg(aliasMapIter.second.c_str());
newEntityDomString.replace(oldAliasQuotes, newAliasQuotes);
QString oldAliasPathRef = QString("/%1").arg(aliasMapIter.first.c_str());
QString newAliasPathRef = QString("/%1").arg(aliasMapIter.second.c_str());
newEntityDomString.replace(oldAliasPathRef, newAliasPathRef);
}
// Create the new Entity DOM from parsing the JSON string
Prefab::PrefabDom entityDomAfter(&instanceDomAfter.GetAllocator());
entityDomAfter.Parse(newEntityDomString.toUtf8().constData());
// Add the new Entity DOM to the Entities member of the instance
rapidjson::Value aliasName(newEntityAlias.c_str(), newEntityAlias.length(), instanceDomAfter.GetAllocator());
entitiesIter->value.AddMember(AZStd::move(aliasName), entityDomAfter, instanceDomAfter.GetAllocator());
}
PrefabUndoInstance* command = aznew PrefabUndoInstance("Entity duplication");
command->SetParent(undoBatch.GetUndoBatch());
command->Capture(instanceDomBefore, instanceDomAfter, commonEntityOwningInstance->get().GetTemplateId());
command->RunRedo();
EntityIdList duplicatedEntityIds;
for (auto aliasMapIter : oldAliasToNewAliasMap)
{
EntityAlias newEntityAlias = aliasMapIter.second;
AliasPath absoluteEntityPath = commonEntityOwningInstance->get().GetAbsoluteInstanceAliasPath();
absoluteEntityPath.Append(newEntityAlias);
AZ::EntityId newEntityId = InstanceEntityIdMapper::GenerateEntityIdForAliasPath(absoluteEntityPath);
duplicatedEntityIds.push_back(newEntityId);
}
// Select the duplicated entities
auto selectionUndo = aznew SelectionCommand(duplicatedEntityIds, "Select Duplicated Entities");
selectionUndo->SetParent(undoBatch.GetUndoBatch());
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::RunRedoSeparately, selectionUndo);
}
return AZ::Success();
}
PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants)
{
if (entityIds.empty())
@@ -737,17 +897,7 @@ namespace AzToolsFramework
AZ_PROFILE_FUNCTION(AZ::Debug::ProfileCategory::AzToolsFramework);
UndoSystem::URSequencePoint* currentUndoBatch = nullptr;
ToolsApplicationRequests::Bus::BroadcastResult(currentUndoBatch, &ToolsApplicationRequests::Bus::Events::GetCurrentUndoBatch);
bool createdUndo = false;
if (!currentUndoBatch)
{
createdUndo = true;
ToolsApplicationRequests::Bus::BroadcastResult(
currentUndoBatch, &ToolsApplicationRequests::Bus::Events::BeginUndoBatch, "Delete Selected");
AZ_Assert(currentUndoBatch, "Failed to create new undo batch.");
}
ScopedUndoBatch undoBatch("Delete Selected");
// In order to undo DeleteSelected, we have to create a selection command which selects the current selection
// and then add the deletion as children.
@@ -775,7 +925,7 @@ namespace AzToolsFramework
if (deleteDescendants)
{
AZStd::vector<AZ::Entity*> entities;
AZStd::vector<AZStd::unique_ptr<Instance>> instances;
AZStd::vector<Instance*> instances;
bool success = RetrieveAndSortPrefabEntitiesAndInstances(inputEntityList, commonOwningInstance->get(), entities, instances);
@@ -786,13 +936,15 @@ namespace AzToolsFramework
for (AZ::Entity* entity : entities)
{
commonOwningInstance->get().DetachEntity(entity->GetId()).release();
AZ::ComponentApplicationBus::Broadcast(&AZ::ComponentApplicationRequests::DeleteEntity, entity->GetId());
}
for (auto& nestedInstance : instances)
{
RemoveLink(nestedInstance, commonOwningInstance->get().GetTemplateId(), currentUndoBatch);
nestedInstance.reset();
AZStd::unique_ptr<Instance> outInstance = commonOwningInstance->get().DetachNestedInstance(nestedInstance->GetInstanceAlias());
RemoveLink(outInstance, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
outInstance.reset();
}
}
else
@@ -804,7 +956,7 @@ namespace AzToolsFramework
if (owningInstance->get().GetContainerEntityId() == entityId)
{
auto instancePtr = commonOwningInstance->get().DetachNestedInstance(owningInstance->get().GetInstanceAlias());
RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), currentUndoBatch);
RemoveLink(instancePtr, commonOwningInstance->get().GetTemplateId(), undoBatch.GetUndoBatch());
}
else
{
@@ -822,17 +974,12 @@ namespace AzToolsFramework
command->SetParent(selCommand);
}
selCommand->SetParent(currentUndoBatch);
selCommand->SetParent(undoBatch.GetUndoBatch());
{
AZ_PROFILE_SCOPE(AZ::Debug::ProfileCategory::AzToolsFramework, "Internal::DeleteEntities:RunRedo");
selCommand->RunRedo();
}
if (createdUndo)
{
ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequests::EndUndoBatch);
}
return AZ::Success();
}
@@ -944,7 +1091,7 @@ namespace AzToolsFramework
bool PrefabPublicHandler::RetrieveAndSortPrefabEntitiesAndInstances(
const EntityList& inputEntities, Instance& commonRootEntityOwningInstance,
EntityList& outEntities, AZStd::vector<AZStd::unique_ptr<Instance>>& outInstances) const
EntityList& outEntities, AZStd::vector<Instance*>& outInstances) const
{
if (inputEntities.size() == 0)
{
@@ -1028,14 +1175,14 @@ namespace AzToolsFramework
for (AZ::Entity* entity : entities)
{
outEntities.emplace_back(commonRootEntityOwningInstance.DetachEntity(entity->GetId()).release());
outEntities.emplace_back(entity);
}
outInstances.clear();
outInstances.reserve(instances.size());
for (Instance* instancePtr : instances)
{
outInstances.push_back(AZStd::move(commonRootEntityOwningInstance.DetachNestedInstance(instancePtr->GetInstanceAlias())));
outInstances.push_back(instancePtr);
}
return (outEntities.size() + outInstances.size()) > 0;
@@ -60,11 +60,12 @@ namespace AzToolsFramework
PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override;
PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override;
private:
PrefabOperationResult DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants);
bool RetrieveAndSortPrefabEntitiesAndInstances(const EntityList& inputEntities, Instance& commonRootEntityOwningInstance,
EntityList& outEntities, AZStd::vector<AZStd::unique_ptr<Instance>>& outInstances) const;
EntityList& outEntities, AZStd::vector<Instance*>& outInstances) const;
InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const;
bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const;
@@ -143,6 +143,13 @@ namespace AzToolsFramework
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0;
/**
* Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance.
* @param entities The entities to duplicate.
* @return An outcome object; on failure, it comes with an error message detailing the cause of the error.
*/
virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0;
};
} // namespace Prefab
@@ -63,6 +63,7 @@
#include <AzToolsFramework/UI/Outliner/EntityOutlinerDisplayOptionsMenu.h>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerSortFilterProxyModel.hxx>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx>
#include <AzToolsFramework/UI/Outliner/EntityOutlinerCacheBus.h>
#include <AzToolsFramework/UI/UICore/WidgetHelpers.h>
////////////////////////////////////////////////////////////////////////////
@@ -1409,6 +1410,16 @@ namespace AzToolsFramework
{
(void)name;
QueueEntityUpdate(entityId);
bool isSelected = false;
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(
isSelected, &AzToolsFramework::ToolsApplicationRequests::IsSelected, entityId);
if (isSelected)
{
// Ask the system to scroll to the entity in case it is off screen after the rename
EntityOutlinerModelNotificationBus::Broadcast(&EntityOutlinerModelNotifications::QueueScrollToNewContent, entityId);
}
}
void EntityOutlinerListModel::OnEntityInfoUpdatedUnsavedChanges(AZ::EntityId entityId)
@@ -138,7 +138,7 @@ namespace UnitTest
if (!GetApplication())
{
// Create & Start a new ToolsApplication if there's no existing one
m_app = AZStd::make_unique<ToolsTestApplication>("ToolsApplication");
m_app = CreateTestApplication();
m_app->Start(AzFramework::Application::Descriptor());
}
@@ -216,6 +216,12 @@ namespace UnitTest
TestEditorActions m_editorActions;
ToolsApplicationMessageHandler m_messageHandler; // used to suppress trace messages in test output
// Override this if your test fixture needs to use a custom TestApplication
virtual AZStd::unique_ptr<ToolsTestApplication> CreateTestApplication()
{
return AZStd::make_unique<ToolsTestApplication>("ToolsApplication");
}
private:
AZStd::unique_ptr<ToolsTestApplication> m_app;
};
@@ -0,0 +1,129 @@
/*
* 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 <AzToolsFramework/ToolsComponents/TransformComponent.h>
#include <Prefab/PrefabTestComponent.h>
#include <Prefab/PrefabTestDomUtils.h>
#include <Prefab/PrefabTestFixture.h>
namespace UnitTest
{
using PrefabDuplicateTest = PrefabTestFixture;
TEST_F(PrefabDuplicateTest, PrefabDuplicate_DuplicateSingleEntitySucceeds)
{
AZStd::string entityName("Same Name");
AZ::Entity* entity1 = CreateEntity(entityName.c_str());
entity1->Deactivate();
entity1->CreateComponent<PrefabTestComponent>();
entity1->Activate();
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
&AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ entity1 });
AZStd::unique_ptr<Instance> newInstance = m_prefabSystemComponent->CreatePrefab(
{ entity1 },
{},
PrefabMockFilePath);
// We've created a prefab with a single Entity, so there should only be one EntityAlias in our instance
EXPECT_EQ(newInstance->GetEntityAliases().size(), 1);
// Duplicate the Entity and trigger the UpdateTemplateInstancesInQueue so the changes get propagated
m_prefabPublicInterface->DuplicateEntitiesInInstance(AzToolsFramework::EntityIdList{ entity1->GetId() });
m_instanceUpdateExecutorInterface->UpdateTemplateInstancesInQueue();
// We duplicated a single Entity, so there should now be two EntityAliases
EXPECT_EQ(newInstance->GetEntityAliases().size(), 2);
newInstance->GetConstEntities([&](const AZ::Entity& entity)
{
// Both of the entities should have the same name
EXPECT_EQ(entity.GetName(), entityName);
// Both of the entities should have the PrefabTestComponent we added
auto testComponent = entity.FindComponent<PrefabTestComponent>();
EXPECT_NE(nullptr, testComponent);
return true;
});
}
TEST_F(PrefabDuplicateTest, PrefabDuplicate_DuplicateMultipleEntitiesAndFixesReferences)
{
AZ::Entity* parentEntity = CreateEntity("Parent Entity");
AZ::Entity* childEntity = CreateEntity("Child Entity");
childEntity->Deactivate();
auto newComponent = childEntity->CreateComponent<PrefabTestComponent>();
childEntity->Activate();
// Set the EntityId reference property on our PrefabTestComponent so we can
// verify that arbitrary EntityId's are fixed up properly
newComponent->m_entityIdProperty = parentEntity->GetId();
AzToolsFramework::EditorEntityContextRequestBus::Broadcast(
&AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ parentEntity, childEntity });
AZStd::unique_ptr<Instance> newInstance = m_prefabSystemComponent->CreatePrefab(
{ parentEntity, childEntity },
{},
PrefabMockFilePath);
// We've created a prefab with two entities, so there should be two EntityAliases in our instance
EXPECT_EQ(newInstance->GetEntityAliases().size(), 2);
// Duplicate the entities and trigger the UpdateTemplateInstancesInQueue so the changes get propagated
m_prefabPublicInterface->DuplicateEntitiesInInstance(AzToolsFramework::EntityIdList{ parentEntity->GetId(), childEntity->GetId() });
m_instanceUpdateExecutorInterface->UpdateTemplateInstancesInQueue();
// We duplicated two entities, so there should now be four EntityAliases
EXPECT_EQ(newInstance->GetEntityAliases().size(), 4);
AzToolsFramework::EntityIdList parentEntityIds;
newInstance->GetConstEntities([&](const AZ::Entity& entity)
{
// Gather the parent EntityIds by tracking which entities don't have a PrefabTestComponent
auto testComponent = entity.FindComponent<PrefabTestComponent>();
if (!testComponent)
{
parentEntityIds.push_back(entity.GetId());
}
return true;
});
// There should only be two parents
EXPECT_EQ(parentEntityIds.size(), 2);
// Verify that the EntityId reference on the PrefabTestComponent on the children correspond
// to unique entities, which will verify that the EntityIds are fixed up on duplicate
newInstance->GetConstEntities([&](const AZ::Entity& entity)
{
// Only the child entities have a PrefabTestComponent
auto testComponent = entity.FindComponent<PrefabTestComponent>();
if (testComponent)
{
auto it = AZStd::find(parentEntityIds.begin(), parentEntityIds.end(), testComponent->m_entityIdProperty);
EXPECT_NE(it, parentEntityIds.end());
// Erase when we find it so that the matches will be unique
parentEntityIds.erase(it);
}
return true;
});
// Verify we matched each of the parent EntityIds
EXPECT_EQ(parentEntityIds.size(), 0);
}
}
@@ -20,6 +20,17 @@
namespace UnitTest
{
PrefabTestToolsApplication::PrefabTestToolsApplication(AZStd::string appName)
: ToolsTestApplication(AZStd::move(appName))
{
}
bool PrefabTestToolsApplication::IsPrefabSystemEnabled() const
{
// Make sure our prefab tests always run with prefabs enabled
return true;
}
void PrefabTestFixture::SetUpEditorFixtureImpl()
{
// Acquire the system entity
@@ -32,6 +43,9 @@ namespace UnitTest
m_prefabLoaderInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabLoaderInterface>::Get();
EXPECT_TRUE(m_prefabLoaderInterface);
m_prefabPublicInterface = AZ::Interface<AzToolsFramework::Prefab::PrefabPublicInterface>::Get();
EXPECT_TRUE(m_prefabPublicInterface);
m_instanceUpdateExecutorInterface = AZ::Interface<AzToolsFramework::Prefab::InstanceUpdateExecutorInterface>::Get();
EXPECT_TRUE(m_instanceUpdateExecutorInterface);
@@ -41,6 +55,11 @@ namespace UnitTest
GetApplication()->RegisterComponentDescriptor(PrefabTestComponent::CreateDescriptor());
}
AZStd::unique_ptr<ToolsTestApplication> PrefabTestFixture::CreateTestApplication()
{
return AZStd::make_unique<PrefabTestToolsApplication>("PrefabTestApplication");
}
AZ::Entity* PrefabTestFixture::CreateEntity(const char* entityName, const bool shouldActivate)
{
// Circumvent the EntityContext system and generate a new entity with a transformcomponent
@@ -31,6 +31,16 @@ namespace UnitTest
using namespace AzToolsFramework::Prefab;
using namespace PrefabTestUtils;
class PrefabTestToolsApplication
: public ToolsTestApplication
{
public:
PrefabTestToolsApplication(AZStd::string appName);
// Make sure our prefab tests always run with prefabs enabled
bool IsPrefabSystemEnabled() const override;
};
class PrefabTestFixture
: public ToolsApplicationFixture,
public UnitTest::TraceBusRedirector
@@ -45,6 +55,8 @@ namespace UnitTest
void SetUpEditorFixtureImpl() override;
AZStd::unique_ptr<ToolsTestApplication> CreateTestApplication() override;
AZ::Entity* CreateEntity(const char* entityName, const bool shouldActivate = true);
void CompareInstances(const Instance& instanceA, const Instance& instanceB, bool shouldCompareLinkIds = true,
@@ -57,6 +69,7 @@ namespace UnitTest
PrefabSystemComponent* m_prefabSystemComponent = nullptr;
PrefabLoaderInterface* m_prefabLoaderInterface = nullptr;
PrefabPublicInterface* m_prefabPublicInterface = nullptr;
InstanceUpdateExecutorInterface* m_instanceUpdateExecutorInterface = nullptr;
InstanceToTemplateInterface* m_instanceToTemplateInterface = nullptr;
};
@@ -54,6 +54,7 @@ set(FILES
Prefab/Spawnable/SpawnableMetaDataTests.cpp
Prefab/MockPrefabFileIOActionValidator.cpp
Prefab/MockPrefabFileIOActionValidator.h
Prefab/PrefabDuplicateTests.cpp
Prefab/PrefabEntityAliasTests.cpp
Prefab/PrefabInstanceToTemplatePropagatorTests.cpp
Prefab/PrefabInstantiateTests.cpp
@@ -35,6 +35,7 @@
#include <AzFramework/StringFunc/StringFunc.h>
#include <AzFramework/Visibility/BoundsBus.h>
#include <AzToolsFramework/API/EditorAssetSystemAPI.h>
#include <AzToolsFramework/API/EditorEntityAPI.h>
#include <AzToolsFramework/API/EntityCompositionRequestBus.h>
#include <AzToolsFramework/API/ToolsApplicationAPI.h>
#include <AzToolsFramework/AssetBrowser/AssetBrowserBus.h>
@@ -192,6 +193,9 @@ void SandboxIntegrationManager::Setup()
(m_prefabIntegrationInterface != nullptr),
"SandboxIntegrationManager requires a PrefabIntegrationInterface instance to be present on Setup().");
m_editorEntityAPI = AZ::Interface<AzToolsFramework::EditorEntityAPI>::Get();
AZ_Assert(m_editorEntityAPI, "SandboxIntegrationManager requires an EditorEntityAPI instance to be present on Setup().");
AzToolsFramework::Layers::EditorLayerComponentNotificationBus::Handler::BusConnect();
}
@@ -1215,9 +1219,20 @@ void SandboxIntegrationManager::CloneSelection(bool& handled)
if (!duplicationSet.empty())
{
AZStd::unordered_set<AZ::EntityId> clonedEntities;
handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities);
m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end());
bool prefabSystemEnabled = false;
AzFramework::ApplicationRequests::Bus::BroadcastResult(prefabSystemEnabled, &AzFramework::ApplicationRequests::IsPrefabSystemEnabled);
if (prefabSystemEnabled)
{
m_editorEntityAPI->DuplicateSelected();
handled = true;
}
else
{
AZStd::unordered_set<AZ::EntityId> clonedEntities;
handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities);
m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end());
}
}
else
{
@@ -77,6 +77,7 @@ class CHyperGraph;
namespace AzToolsFramework
{
class EditorEntityAPI;
class EditorEntityUiInterface;
namespace AssetBrowser
@@ -371,6 +372,7 @@ private:
AzToolsFramework::EditorEntityUiInterface* m_editorEntityUiInterface = nullptr;
AzToolsFramework::Prefab::PrefabIntegrationInterface* m_prefabIntegrationInterface = nullptr;
AzToolsFramework::EditorEntityAPI* m_editorEntityAPI = nullptr;
// Overrides UI styling and behavior for Layer Entities
AzToolsFramework::LayerUiHandler m_layerUiOverrideHandler;
@@ -65,6 +65,7 @@
#include "OutlinerTreeView.hxx"
#include "Include/ICommandManager.h"
#include "Include/IObjectManager.h"
#include "OutlinerCacheBus.h"
#include <Editor/CryEditDoc.h>
#include <AzCore/Outcome/Outcome.h>
@@ -1538,6 +1539,16 @@ void OutlinerListModel::OnEntityInfoUpdatedName(AZ::EntityId entityId, const AZS
{
(void)name;
QueueEntityUpdate(entityId);
bool isSelected = false;
AzToolsFramework::ToolsApplicationRequests::Bus::BroadcastResult(
isSelected, &AzToolsFramework::ToolsApplicationRequests::IsSelected, entityId);
if (isSelected)
{
// Ask the system to scroll to the entity in case it is off screen after the rename
OutlinerModelNotificationBus::Broadcast(&OutlinerModelNotifications::QueueScrollToNewContent, entityId);
}
}
void OutlinerListModel::OnEntityInfoUpdatedUnsavedChanges(AZ::EntityId entityId)
@@ -1,4 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="#444444"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M13 3H11V11H3V13H11V21H13V13H21V11H13V3Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 255 B

After

Width:  |  Height:  |  Size: 209 B

@@ -0,0 +1,16 @@
<RCC>
<qresource prefix="/ProjectManager/style">
<file>ProjectManager.qss</file>
</qresource>
<qresource prefix="/">
<file>Add.svg</file>
<file>Select_Folder.svg</file>
<file>o3de_editor.ico</file>
<file>Windows.svg</file>
<file>Android.svg</file>
<file>iOS.svg</file>
<file>Linux.svg</file>
<file>macOS.svg</file>
<file>Backgrounds/FirstTimeBackgroundImage.jpg</file>
</qresource>
</RCC>
@@ -0,0 +1,73 @@
/************** General (MainWindow) **************/
QMainWindow {
background-color: #333333;
}
QPushButton:focus {
outline: none;
border:1px solid #1e70eb;
}
/************** General (Forms) **************/
#formLineEditWidget,
#formBrowseEditWidget {
max-width: 780px;
}
#formFrame {
max-width: 720px;
background-color: #444444;
border:1px solid #dddddd;
border-radius: 4px;
padding: 0px 10px 2px 6px;
margin-top:10px;
margin-left:30px;
}
#formFrame[Focus="true"] {
border:1px solid #1e70eb;
}
#formFrame[Valid="false"] {
border:1px solid red;
}
#formFrame QLabel {
font-size: 13px;
color: #cccccc;
}
#formFrame QPushButton {
background-color: transparent;
background:transparent url(:/Select_Folder.svg) no-repeat center;
qproperty-flat: true;
}
#formFrame QPushButton:focus {
border:none;
}
#formFrame QLineEdit {
background-color: rgba(0,0,0,0);
font-size: 18px;
color: #ffffff;
border:0;
line-height: 30px;
height: 1em;
padding-top: -4px;
}
#formErrorLabel {
color: #ec3030;
font-size: 14px;
margin-left: 40px;
}
#formTitleLabel {
font-size:21px;
color:#ffffff;
margin: 10px 0 10px 30px;
}
@@ -1,4 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="24" height="24" fill="#444444"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.98407 4H2V6V7V19.8145H2.07539L6.02059 7.99123H19V6H11.328L9.98407 4ZM19 19.7473L22.0766 10H7.05436L3.68763 19.8329H18.973L18.9788 19.8145H19V19.7473Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 367 B

After

Width:  |  Height:  |  Size: 321 B

@@ -14,8 +14,16 @@
namespace O3DE::ProjectManager
{
EngineInfo::EngineInfo(const QString& path)
EngineInfo::EngineInfo(const QString& path, const QString& name, const QString& version, const QString& thirdPartyPath)
: m_path(path)
, m_name(name)
, m_version(version)
, m_thirdPartyPath(thirdPartyPath)
{
}
bool EngineInfo::IsValid() const
{
return !m_path.isEmpty();
}
} // namespace O3DE::ProjectManager
+13 -1
View File
@@ -22,8 +22,20 @@ namespace O3DE::ProjectManager
{
public:
EngineInfo() = default;
EngineInfo(const QString& path);
EngineInfo(const QString& path, const QString& name, const QString& version, const QString& thirdPartyPath);
// from engine.json
QString m_version;
QString m_name;
QString m_thirdPartyPath;
// from o3de_manifest.json
QString m_path;
QString m_defaultProjectsFolder;
QString m_defaultGemsFolder;
QString m_defaultTemplatesFolder;
QString m_defaultRestrictedFolder;
bool IsValid() const;
};
} // namespace O3DE::ProjectManager
@@ -11,20 +11,99 @@
*/
#include <EngineSettingsScreen.h>
#include <Source/ui_EngineSettingsScreen.h>
#include <QVBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QMessageBox>
#include <FormLineEditWidget.h>
#include <FormBrowseEditWidget.h>
#include <PythonBindingsInterface.h>
#include <PathValidator.h>
namespace O3DE::ProjectManager
{
EngineSettingsScreen::EngineSettingsScreen(QWidget* parent)
: ScreenWidget(parent)
, m_ui(new Ui::EngineSettingsClass())
{
m_ui->setupUi(this);
auto* layout = new QVBoxLayout(this);
layout->setAlignment(Qt::AlignTop);
setObjectName("engineSettingsScreen");
EngineInfo engineInfo;
AZ::Outcome<EngineInfo> engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
engineInfo = engineInfoResult.GetValue();
}
QLabel* formTitleLabel = new QLabel(tr("O3DE Settings"), this);
formTitleLabel->setObjectName("formTitleLabel");
layout->addWidget(formTitleLabel);
m_engineVersion = new FormLineEditWidget(tr("Engine Version"), engineInfo.m_version, this);
m_engineVersion->lineEdit()->setReadOnly(true);
layout->addWidget(m_engineVersion);
m_thirdParty = new FormBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this);
m_thirdParty->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_thirdParty->lineEdit()->setReadOnly(true);
m_thirdParty->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_thirdParty->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_thirdParty);
m_defaultProjects = new FormBrowseEditWidget(tr("Default Projects Folder"), engineInfo.m_defaultProjectsFolder, this);
m_defaultProjects->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultProjects->lineEdit()->setReadOnly(true);
m_defaultProjects->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultProjects->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultProjects);
m_defaultGems = new FormBrowseEditWidget(tr("Default Gems Folder"), engineInfo.m_defaultGemsFolder, this);
m_defaultGems->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultGems->lineEdit()->setReadOnly(true);
m_defaultGems->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultGems->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultGems);
m_defaultProjectTemplates = new FormBrowseEditWidget(tr("Default Project Templates Folder"), engineInfo.m_defaultTemplatesFolder, this);
m_defaultProjectTemplates->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this));
m_defaultProjectTemplates->lineEdit()->setReadOnly(true);
m_defaultProjectTemplates->setErrorLabelText(tr("Please provide a valid path to a folder that exists"));
connect(m_defaultProjectTemplates->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged);
layout->addWidget(m_defaultProjectTemplates);
setLayout(layout);
}
ProjectManagerScreen EngineSettingsScreen::GetScreenEnum()
{
return ProjectManagerScreen::EngineSettings;
}
void EngineSettingsScreen::OnTextChanged()
{
// save engine settings
auto engineInfoResult = PythonBindingsInterface::Get()->GetEngineInfo();
if (engineInfoResult.IsSuccess())
{
EngineInfo engineInfo;
engineInfo = engineInfoResult.GetValue();
engineInfo.m_thirdPartyPath = m_thirdParty->lineEdit()->text();
engineInfo.m_defaultProjectsFolder = m_defaultProjects->lineEdit()->text();
engineInfo.m_defaultGemsFolder = m_defaultGems->lineEdit()->text();
engineInfo.m_defaultTemplatesFolder = m_defaultProjectTemplates->lineEdit()->text();
bool result = PythonBindingsInterface::Get()->SetEngineInfo(engineInfo);
if (!result)
{
QMessageBox::critical(this, tr("Engine Settings"), tr("Failed to save engine settings."));
}
}
else
{
QMessageBox::critical(this, tr("Engine Settings"), tr("Failed to get engine settings."));
}
}
} // namespace O3DE::ProjectManager
@@ -15,13 +15,11 @@
#include <ScreenWidget.h>
#endif
namespace Ui
{
class EngineSettingsClass;
}
namespace O3DE::ProjectManager
{
QT_FORWARD_DECLARE_CLASS(FormLineEditWidget)
QT_FORWARD_DECLARE_CLASS(FormBrowseEditWidget)
class EngineSettingsScreen
: public ScreenWidget
{
@@ -30,8 +28,15 @@ namespace O3DE::ProjectManager
~EngineSettingsScreen() = default;
ProjectManagerScreen GetScreenEnum() override;
protected slots:
void OnTextChanged();
private:
QScopedPointer<Ui::EngineSettingsClass> m_ui;
FormLineEditWidget* m_engineVersion;
FormBrowseEditWidget* m_thirdParty;
FormBrowseEditWidget* m_defaultProjects;
FormBrowseEditWidget* m_defaultGems;
FormBrowseEditWidget* m_defaultProjectTemplates;
};
} // namespace O3DE::ProjectManager
@@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>EngineSettingsClass</class>
<widget class="QWidget" name="EngineSettingsClass">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>839</width>
<height>597</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>O3DE Settings</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_2">
<property name="text">
<string>Engine Version</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_3">
<property name="text">
<string>v1.01</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>3rd Party Software Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit"/>
</item>
<item>
<widget class="QLabel" name="label_5">
<property name="text">
<string>Restricted Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_2"/>
</item>
<item>
<widget class="QLabel" name="label_6">
<property name="text">
<string>Default Gems Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_3"/>
</item>
<item>
<widget class="QLabel" name="label_7">
<property name="text">
<string>Default Project Templates Folder</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="lineEdit_4"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>
@@ -49,11 +49,11 @@ namespace O3DE::ProjectManager
QHBoxLayout* buttonLayout = new QHBoxLayout();
buttonLayout->setSpacing(s_buttonSpacing);
m_createProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Add.svg"), tr("Create Project"), this);
m_createProjectButton = CreateLargeBoxButton(QIcon(":/Add.svg"), tr("Create Project"), this);
m_createProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
buttonLayout->addWidget(m_createProjectButton);
m_addProjectButton = CreateLargeBoxButton(QIcon(":/Resources/Select_Folder.svg"), tr("Add a Project"), this);
m_addProjectButton = CreateLargeBoxButton(QIcon(":/Select_Folder.svg"), tr("Add a Project"), this);
m_addProjectButton->setIconSize(QSize(s_iconSize, s_iconSize));
buttonLayout->addWidget(m_addProjectButton);
@@ -66,7 +66,7 @@ namespace O3DE::ProjectManager
vLayout->addItem(verticalSpacer);
// Using border-image allows for scaling options background-image does not support
setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Resources/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }");
setStyleSheet("O3DE--ProjectManager--ScreenWidget { border-image: url(:/Backgrounds/FirstTimeBackgroundImage.jpg) repeat repeat; }");
connect(m_createProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleNewProjectButton);
connect(m_addProjectButton, &QPushButton::pressed, this, &FirstTimeUseScreen::HandleAddProjectButton);
@@ -0,0 +1,49 @@
/*
* 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 <FormBrowseEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <QPushButton>
#include <QHBoxLayout>
#include <QFileDialog>
#include <QLineEdit>
#include <QStandardPaths>
#include <QIcon>
namespace O3DE::ProjectManager
{
FormBrowseEditWidget::FormBrowseEditWidget(const QString& labelText, const QString& valueText, QWidget* parent)
: FormLineEditWidget(labelText, valueText, parent)
{
setObjectName("formBrowseEditWidget");
QPushButton* browseButton = new QPushButton(this);
connect(browseButton, &QPushButton::pressed, this, &FormBrowseEditWidget::HandleBrowseButton);
m_frameLayout->addWidget(browseButton);
}
void FormBrowseEditWidget::HandleBrowseButton()
{
QString defaultPath = m_lineEdit->text();
if (defaultPath.isEmpty())
{
defaultPath = QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation);
}
QString directory = QDir::toNativeSeparators(QFileDialog::getExistingDirectory(this, tr("Browse"), defaultPath));
if (!directory.isEmpty())
{
m_lineEdit->setText(directory);
}
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,33 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <FormLineEditWidget.h>
#endif
namespace O3DE::ProjectManager
{
class FormBrowseEditWidget
: public FormLineEditWidget
{
Q_OBJECT
public:
explicit FormBrowseEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
~FormBrowseEditWidget() = default;
private slots:
void HandleBrowseButton();
};
} // namespace O3DE::ProjectManager
@@ -0,0 +1,123 @@
/*
* 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 <FormLineEditWidget.h>
#include <AzQtComponents/Components/StyledLineEdit.h>
#include <AzQtComponents/Components/Widgets/LineEdit.h>
#include <QVBoxLayout>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QLabel>
#include <QFrame>
#include <QValidator>
#include <QStyle>
namespace O3DE::ProjectManager
{
FormLineEditWidget::FormLineEditWidget(const QString& labelText, const QString& valueText, QWidget* parent)
: QWidget(parent)
{
setObjectName("formLineEditWidget");
QVBoxLayout* mainLayout = new QVBoxLayout();
mainLayout->setAlignment(Qt::AlignTop);
{
m_frame = new QFrame(this);
m_frame->setObjectName("formFrame");
// use a horizontal box layout so buttons can be added to the right of the field
m_frameLayout = new QHBoxLayout();
{
QVBoxLayout* fieldLayout = new QVBoxLayout();
QLabel* label = new QLabel(labelText, this);
fieldLayout->addWidget(label);
m_lineEdit = new AzQtComponents::StyledLineEdit(this);
m_lineEdit->setFlavor(AzQtComponents::StyledLineEdit::Question);
AzQtComponents::LineEdit::setErrorIconEnabled(m_lineEdit, false);
m_lineEdit->setText(valueText);
connect(m_lineEdit, &AzQtComponents::StyledLineEdit::flavorChanged, this, &FormLineEditWidget::flavorChanged);
connect(m_lineEdit, &AzQtComponents::StyledLineEdit::onFocus, this, &FormLineEditWidget::onFocus);
connect(m_lineEdit, &AzQtComponents::StyledLineEdit::onFocusOut, this, &FormLineEditWidget::onFocusOut);
m_lineEdit->setFrame(false);
fieldLayout->addWidget(m_lineEdit);
m_frameLayout->addLayout(fieldLayout);
QWidget* emptyWidget = new QWidget(this);
m_frameLayout->addWidget(emptyWidget);
}
m_frame->setLayout(m_frameLayout);
mainLayout->addWidget(m_frame);
m_errorLabel = new QLabel(this);
m_errorLabel->setObjectName("formErrorLabel");
m_errorLabel->setVisible(false);
mainLayout->addWidget(m_errorLabel);
}
setLayout(mainLayout);
}
void FormLineEditWidget::setErrorLabelText(const QString& labelText)
{
m_errorLabel->setText(labelText);
}
QLineEdit* FormLineEditWidget::lineEdit() const
{
return m_lineEdit;
}
void FormLineEditWidget::flavorChanged()
{
if (m_lineEdit->flavor() == AzQtComponents::StyledLineEdit::Flavor::Invalid)
{
m_frame->setProperty("Valid", false);
m_errorLabel->setVisible(true);
}
else
{
m_frame->setProperty("Valid", true);
m_errorLabel->setVisible(false);
}
refreshStyle();
}
void FormLineEditWidget::onFocus()
{
m_frame->setProperty("Focus", true);
refreshStyle();
}
void FormLineEditWidget::onFocusOut()
{
m_frame->setProperty("Focus", false);
refreshStyle();
}
void FormLineEditWidget::refreshStyle()
{
// we must unpolish/polish every child after changing a property
// or else they won't use the correct stylesheet selector
for (auto child : findChildren<QWidget*>())
{
child->style()->unpolish(child);
child->style()->polish(child);
}
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,60 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <QWidget>
#endif
QT_FORWARD_DECLARE_CLASS(QLineEdit)
QT_FORWARD_DECLARE_CLASS(QLabel)
QT_FORWARD_DECLARE_CLASS(QFrame)
QT_FORWARD_DECLARE_CLASS(QHBoxLayout)
namespace AzQtComponents
{
class StyledLineEdit;
}
namespace O3DE::ProjectManager
{
class FormLineEditWidget
: public QWidget
{
Q_OBJECT
public:
explicit FormLineEditWidget(const QString& labelText, const QString& valueText = "", QWidget* parent = nullptr);
~FormLineEditWidget() = default;
//! Set the error message for to display when invalid.
void setErrorLabelText(const QString& labelText);
//! Returns a pointer to the underlying LineEdit.
QLineEdit* lineEdit() const;
protected:
QLabel* m_errorLabel = nullptr;
QFrame* m_frame = nullptr;
QHBoxLayout* m_frameLayout = nullptr;
AzQtComponents::StyledLineEdit* m_lineEdit = nullptr;
private slots:
void flavorChanged();
void onFocus();
void onFocusOut();
private:
void refreshStyle();
};
} // namespace O3DE::ProjectManager
@@ -22,11 +22,11 @@ namespace O3DE::ProjectManager
: QStyledItemDelegate(parent)
, m_gemModel(gemModel)
{
AddPlatformIcon(GemInfo::Android, ":/Resources/Android.svg");
AddPlatformIcon(GemInfo::iOS, ":/Resources/iOS.svg");
AddPlatformIcon(GemInfo::Linux, ":/Resources/Linux.svg");
AddPlatformIcon(GemInfo::macOS, ":/Resources/macOS.svg");
AddPlatformIcon(GemInfo::Windows, ":/Resources/Windows.svg");
AddPlatformIcon(GemInfo::Android, ":/Android.svg");
AddPlatformIcon(GemInfo::iOS, ":/iOS.svg");
AddPlatformIcon(GemInfo::Linux, ":/Linux.svg");
AddPlatformIcon(GemInfo::macOS, ":/macOS.svg");
AddPlatformIcon(GemInfo::Windows, ":/Windows.svg");
}
void GemItemDelegate::AddPlatformIcon(GemInfo::Platform platform, const QString& iconPath)
@@ -0,0 +1,65 @@
/*
* All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
* its licensors.
*
* For complete copyright and license terms please see the LICENSE at the root of this
* distribution (the "License"). All use of this software is governed by the License,
* or, if provided, by the license below or the license accompanying this file. Do not
* remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
*
*/
#include "PathValidator.h"
#include <QWidget>
#include <QFileInfo>
#include <QDir>
namespace O3DE::ProjectManager
{
PathValidator::PathValidator(PathMode pathMode, QWidget* parent)
: QValidator(parent)
, m_pathMode(pathMode)
{
}
void PathValidator::setAllowEmpty(bool allowEmpty)
{
m_allowEmpty = allowEmpty;
}
void PathValidator::setPathMode(PathMode pathMode)
{
m_pathMode = pathMode;
}
QValidator::State PathValidator::validate(QString &text, int &) const
{
if(text.isEmpty())
{
return m_allowEmpty ? QValidator::Acceptable : QValidator::Intermediate;
}
QFileInfo pathInfo(text);
if(!pathInfo.dir().exists())
{
return QValidator::Intermediate;
}
switch(m_pathMode)
{
case PathMode::AnyFile://acceptable, as long as it's not an directoy
return pathInfo.isDir() ? QValidator::Intermediate : QValidator::Acceptable;
case PathMode::ExistingFile://must be an existing file
return pathInfo.exists() && pathInfo.isFile() ? QValidator::Acceptable : QValidator::Intermediate;
case PathMode::ExistingFolder://must be an existing folder
return pathInfo.exists() && pathInfo.isDir() ? QValidator::Acceptable : QValidator::Intermediate;
default:
Q_UNREACHABLE();
}
return QValidator::Invalid;
}
} // namespace O3DE::ProjectManager
@@ -0,0 +1,45 @@
/*
* 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
#if !defined(Q_MOC_RUN)
#include <QValidator>
#endif
QT_FORWARD_DECLARE_CLASS(QWidget)
namespace O3DE::ProjectManager
{
class PathValidator
: public QValidator
{
public:
enum class PathMode {
ExistingFile, //!< A single, existings file. Useful for "Open file"
ExistingFolder, //!< A single, existing directory. Useful for "Open Folder"
AnyFile //!< A single, valid file, doesn't have to exist but the directory must. Useful for "Save File"
};
explicit PathValidator(PathMode pathMode, QWidget* parent = nullptr);
~PathValidator() = default;
void setAllowEmpty(bool allowEmpty);
void setPathMode(PathMode pathMode);
QValidator::State validate(QString &text, int &) const override;
private:
PathMode m_pathMode = PathMode::AnyFile;
bool m_allowEmpty = false;
};
} // namespace O3DE::ProjectManager
@@ -44,10 +44,10 @@ namespace O3DE::ProjectManager
QDir rootDir = QString::fromUtf8(engineRootPath.Native().data(), aznumeric_cast<int>(engineRootPath.Native().size()));
const auto pathOnDisk = rootDir.absoluteFilePath("Code/Tools/ProjectManager/Resources");
const auto qrcPath = QStringLiteral(":/ProjectManagerWindow");
AzQtComponents::StyleManager::addSearchPaths("projectmanagerwindow", pathOnDisk, qrcPath, engineRootPath);
const auto qrcPath = QStringLiteral(":/ProjectManager/style");
AzQtComponents::StyleManager::addSearchPaths("style", pathOnDisk, qrcPath, engineRootPath);
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("projectlauncherwindow:ProjectManagerWindow.qss"));
AzQtComponents::StyleManager::setStyleSheet(this, QStringLiteral("style:ProjectManager.qss"));
QVector<ProjectManagerScreen> screenEnums =
{
@@ -41,8 +41,8 @@
<string>Icon</string>
</property>
<property name="icon">
<iconset resource="../project_manager.qrc">
<normaloff>:/Resources/o3de_editor.ico</normaloff>:/Resources/o3de_editor.ico</iconset>
<iconset resource="../Resources/ProjectManager.qrc">
<normaloff>:/o3de_editor.ico</normaloff>:/o3de_editor.ico</iconset>
</property>
</widget>
<widget class="QMenu" name="projectsMenu">
@@ -61,7 +61,7 @@
</widget>
</widget>
<resources>
<include location="../project_manager.qrc"/>
<include location="../Resources/ProjectManager.qrc"/>
</resources>
<connections/>
</ui>
@@ -106,6 +106,7 @@ namespace O3DE::ProjectManager
auto result = PythonBindingsInterface::Get()->CreateProject(m_projectTemplatePath, m_projectInfo);
if (result.IsSuccess())
{
// adding gems is not implemented yet because we don't know what targets to add or how to add them
emit ChangeScreenRequest(ProjectManagerScreen::ProjectsHome);
}
else
@@ -48,8 +48,8 @@
<string/>
</property>
<property name="icon">
<iconset resource="../project_manager.qrc">
<normaloff>:/Resources/Add.svg</normaloff>:/Resources/Add.svg</iconset>
<iconset resource="../Resources/ProjectManager.qrc">
<normaloff>:/Add.svg</normaloff>:/Add.svg</iconset>
</property>
</widget>
</item>
@@ -65,8 +65,8 @@
<string/>
</property>
<property name="icon">
<iconset resource="../project_manager.qrc">
<normaloff>:/Resources/Select_Folder.svg</normaloff>:/Resources/Select_Folder.svg</iconset>
<iconset resource="../Resources/ProjectManager.qrc">
<normaloff>:/Select_Folder.svg</normaloff>:/Select_Folder.svg</iconset>
</property>
</widget>
</item>
@@ -131,7 +131,7 @@
</layout>
</widget>
<resources>
<include location="../project_manager.qrc"/>
<include location="../Resources/ProjectManager.qrc"/>
</resources>
<connections/>
</ui>
@@ -328,12 +328,91 @@ namespace O3DE::ProjectManager
AZ::Outcome<EngineInfo> PythonBindings::GetEngineInfo()
{
EngineInfo engineInfo;
bool result = ExecuteWithLock([&] {
pybind11::str enginePath = m_registration.attr("get_this_engine_path")();
auto o3deData = m_registration.attr("load_o3de_manifest")();
if (pybind11::isinstance<pybind11::dict>(o3deData))
{
engineInfo.m_path = Py_To_String(enginePath);
engineInfo.m_defaultGemsFolder = Py_To_String(o3deData["default_gems_folder"]);
engineInfo.m_defaultProjectsFolder = Py_To_String(o3deData["default_projects_folder"]);
engineInfo.m_defaultRestrictedFolder = Py_To_String(o3deData["default_restricted_folder"]);
engineInfo.m_defaultTemplatesFolder = Py_To_String(o3deData["default_templates_folder"]);
engineInfo.m_thirdPartyPath = Py_To_String_Optional(o3deData,"third_party_path","");
}
auto engineData = m_registration.attr("get_engine_data")(pybind11::none(), enginePath);
if (pybind11::isinstance<pybind11::dict>(engineData))
{
try
{
engineInfo.m_version = Py_To_String_Optional(engineData,"O3DEVersion","0.0.0.0");
engineInfo.m_name = Py_To_String_Optional(engineData,"engine_name","O3DE");
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("PythonBindings", false, "Failed to get EngineInfo from %s", Py_To_String(enginePath));
}
}
});
if (!result || !engineInfo.IsValid())
{
return AZ::Failure();
}
else
{
return AZ::Success(AZStd::move(engineInfo));
}
return AZ::Failure();
}
bool PythonBindings::SetEngineInfo([[maybe_unused]] const EngineInfo& engineInfo)
bool PythonBindings::SetEngineInfo(const EngineInfo& engineInfo)
{
return false;
bool result = ExecuteWithLock([&] {
pybind11::str enginePath = engineInfo.m_path.toStdString();
pybind11::str defaultProjectsFolder = engineInfo.m_defaultProjectsFolder.toStdString();
pybind11::str defaultGemsFolder = engineInfo.m_defaultGemsFolder.toStdString();
pybind11::str defaultTemplatesFolder = engineInfo.m_defaultTemplatesFolder.toStdString();
auto registrationResult = m_registration.attr("register")(
enginePath, // engine_path
pybind11::none(), // project_path
pybind11::none(), // gem_path
pybind11::none(), // template_path
pybind11::none(), // restricted_path
pybind11::none(), // repo_uri
pybind11::none(), // default_engines_folder
defaultProjectsFolder,
defaultGemsFolder,
defaultTemplatesFolder
);
if (registrationResult.cast<int>() != 0)
{
result = false;
}
auto manifest = m_registration.attr("load_o3de_manifest")();
if (pybind11::isinstance<pybind11::dict>(manifest))
{
try
{
manifest["third_party_path"] = engineInfo.m_thirdPartyPath.toStdString();
m_registration.attr("save_o3de_manifest")(manifest);
}
catch ([[maybe_unused]] const std::exception& e)
{
AZ_Warning("PythonBindings", false, "Failed to set third party path.");
}
}
});
return result;
}
AZ::Outcome<GemInfo> PythonBindings::GetGem(const QString& path)
@@ -1,16 +0,0 @@
<RCC>
<qresource prefix="/">
<file>Resources/ProjectManager.qss</file>
<file>Resources/Add.svg</file>
<file>Resources/Select_Folder.svg</file>
<file>Resources/o3de_editor.ico</file>
<file>Resources/Windows.svg</file>
<file>Resources/Android.svg</file>
<file>Resources/iOS.svg</file>
<file>Resources/Linux.svg</file>
<file>Resources/macOS.svg</file>
<file>Resources/ArrowDownLine.svg</file>
<file>Resources/ArrowUpLine.svg</file>
<file>Resources/Backgrounds/FirstTimeBackgroundImage.jpg</file>
</qresource>
</RCC>
@@ -10,7 +10,8 @@
#
set(FILES
project_manager.qrc
Resources/ProjectManager.qrc
Resources/ProjectManager.qss
Source/main.cpp
Source/ScreenDefs.h
Source/ScreenFactory.h
@@ -22,6 +23,12 @@ set(FILES
Source/EngineInfo.cpp
Source/FirstTimeUseScreen.h
Source/FirstTimeUseScreen.cpp
Source/FormLineEditWidget.h
Source/FormLineEditWidget.cpp
Source/FormBrowseEditWidget.h
Source/FormBrowseEditWidget.cpp
Source/PathValidator.h
Source/PathValidator.cpp
Source/ProjectManagerWindow.h
Source/ProjectManagerWindow.cpp
Source/ProjectTemplateInfo.h
@@ -44,7 +51,6 @@ set(FILES
Source/ProjectSettingsScreen.ui
Source/EngineSettingsScreen.h
Source/EngineSettingsScreen.cpp
Source/EngineSettingsScreen.ui
Source/LinkWidget.h
Source/LinkWidget.cpp
Source/TagWidget.h
@@ -80,7 +80,7 @@
{
"Name": "EyeAdaptationPass",
"TemplateName": "EyeAdaptationTemplate",
"Enabled": false,
"Enabled": true,
"Connections": [
{
"LocalSlot": "SceneLuminanceInput",
@@ -175,12 +175,18 @@ namespace AZ
void LightCullingTilePreparePass::OnShaderReinitialized(const AZ::RPI::Shader&)
{
LoadShader();
ChooseShaderVariant();
if (!m_flags.m_queuedForBuildAttachment && !m_flags.m_isBuildingAttachments)
{
ChooseShaderVariant();
}
}
void LightCullingTilePreparePass::OnShaderAssetReinitialized(const Data::Asset<AZ::RPI::ShaderAsset>&)
{
LoadShader();
ChooseShaderVariant();
if (!m_flags.m_queuedForBuildAttachment && !m_flags.m_isBuildingAttachments)
{
ChooseShaderVariant();
}
}
void LightCullingTilePreparePass::OnShaderVariantReinitialized(
@@ -188,7 +194,10 @@ namespace AZ
AZ::RPI::ShaderVariantStableId)
{
LoadShader();
ChooseShaderVariant();
if (!m_flags.m_queuedForBuildAttachment && !m_flags.m_isBuildingAttachments)
{
ChooseShaderVariant();
}
}
} // namespace Render
@@ -69,7 +69,6 @@ namespace AZ
if (m_shouldUpdatePassParameters)
{
UpdateEyeAdaptationPass();
UpdateLuminanceHeatmap();
m_shouldUpdatePassParameters = false;
@@ -198,30 +197,6 @@ namespace AZ
}
}
void ExposureControlSettings::UpdateEyeAdaptationPass()
{
auto* passSystem = AZ::RPI::PassSystemInterface::Get();
// [GFX-TODO][ATOM-13224] Remove UpdateLuminanceHeatmap and UpdateEyeAdaptationPass
auto passTemplateName = m_eyeAdaptationPassTemplateNameId;
if (passSystem->HasPassesForTemplateName(passTemplateName))
{
const AZStd::vector<RPI::Pass*>& eyeAdaptationPasses = passSystem->GetPassesForTemplateName(passTemplateName);
for (RPI::Pass* pass : eyeAdaptationPasses)
{
auto* eyeAdaptationPass = azrtti_cast<AZ::Render::EyeAdaptationPass*>(pass);
auto* renderPipeline = eyeAdaptationPass->GetRenderPipeline();
if (renderPipeline && renderPipeline->GetScene() == GetParentScene())
{
// update eye adaptation pass's enable state
eyeAdaptationPass->UpdateEnable();
}
}
}
}
void ExposureControlSettings::UpdateLuminanceHeatmap()
{
auto* passSystem = AZ::RPI::PassSystemInterface::Get();
@@ -85,7 +85,6 @@ namespace AZ
void UpdateExposureControlRelatedPassParameters();
void UpdateLuminanceHeatmap();
void UpdateEyeAdaptationPass();
PostProcessSettings* m_parentSettings = nullptr;
bool m_shouldUpdatePassParameters = true;
@@ -62,17 +62,24 @@ namespace AZ
m_buffer = RPI::BufferSystemInterface::Get()->CreateBufferFromCommonPool(desc);
}
void EyeAdaptationPass::UpdateEnable()
void EyeAdaptationPass::BuildAttachmentsInternal()
{
if (m_pipeline == nullptr)
if (!m_buffer)
{
SetEnabled(false);
return;
InitBuffer();
}
AZ_Assert(m_pipeline->GetScene(), "Scene shouldn't nullptr");
AttachBufferToSlot(EyeAdaptationDataInputOutputSlotName, m_buffer);
}
UpdateInputBufferIndices();
bool EyeAdaptationPass::IsEnabled() const
{
if (!ComputePass::IsEnabled() || m_pipeline == nullptr)
{
return false;
}
AZ_Assert(m_pipeline->GetScene(), "EyeAdaptationPass's Pipeline does not have a valid scene pointer");
AZ::RPI::Scene* scene = GetScene();
bool enabled = false;
@@ -95,38 +102,9 @@ namespace AZ
}
}
const bool lastEnabled = IsEnabled();
SetEnabled(enabled);
if (IsEnabled() && !lastEnabled)
{
// Need rebuilt this pass's attachment as any connections. So queue parent pass.
GetParent()->QueueForBuildAttachments();
}
return enabled;
}
void EyeAdaptationPass::UpdateInputBufferIndices()
{
if (m_exposureControlBufferInputIndex.IsNull())
{
m_exposureControlBufferInputIndex = GetView()->GetShaderResourceGroup()->FindShaderInputBufferIndex(Name("m_exposureControl"));
}
}
void EyeAdaptationPass::BuildAttachmentsInternal()
{
if (m_pipeline == nullptr)
{
return;
}
if (!m_buffer)
{
InitBuffer();
}
AttachBufferToSlot(EyeAdaptationDataInputOutputSlotName, m_buffer);
}
void EyeAdaptationPass::FrameBeginInternal(FramePrepareParams params)
{
@@ -17,6 +17,7 @@
#include <Atom/RHI/DrawItem.h>
#include <Atom/RHI/ScopeProducer.h>
#include <Atom/RHI.Reflect/ShaderResourceGroupLayoutDescriptor.h>
#include <Atom/RHI.Reflect/ShaderInputNameIndex.h>
#include <Atom/RPI.Public/Pass/ComputePass.h>
#include <Atom/RPI.Public/Shader/Shader.h>
@@ -45,12 +46,11 @@ namespace AZ
static RPI::Ptr<EyeAdaptationPass> Create(const RPI::PassDescriptor& descriptor);
// Check if we should enable of disable this pass
void UpdateEnable();
bool IsEnabled() const override;
protected:
EyeAdaptationPass(const RPI::PassDescriptor& descriptor);
void InitBuffer();
void UpdateInputBufferIndices();
// A StructuredBuffer for exposure calculation on the GPU.
struct ExposureCalculationData
@@ -65,7 +65,7 @@ namespace AZ
AZ::Data::Instance<RPI::Buffer> m_buffer;
// SRG binding indices...
AZ::RHI::ShaderInputBufferIndex m_exposureControlBufferInputIndex;
AZ::RHI::ShaderInputNameIndex m_exposureControlBufferInputIndex = "m_exposureControl";
};
} // namespace Render
} // namespace AZ
@@ -72,7 +72,7 @@ namespace AZ
//! Return the View if this pass is associated with a pipeline view via PipelineViewTag.
//! It may return nullptr if this pass is independent with any views.
ViewPtr GetView();
ViewPtr GetView() const;
protected:
explicit RenderPass(const PassDescriptor& descriptor);
@@ -98,7 +98,7 @@ namespace AZ
bool Pass::IsEnabled() const
{
return m_flags.m_enabled && (m_flags.m_parentEnabled || m_parent == nullptr);
return m_flags.m_enabled;
}
// --- Error Logging ---
@@ -342,7 +342,7 @@ namespace AZ
}
}
ViewPtr RenderPass::GetView()
ViewPtr RenderPass::GetView() const
{
if (m_flags.m_hasPipelineViewTag && m_pipeline)
{
@@ -727,7 +727,7 @@ namespace AZ
ImGui::BeginTooltip();
ImGui::Text("Name: %s", passEntry->m_name.GetCStr());
ImGui::Text("Path: %s", passEntry->m_path.GetCStr());
ImGui::Text("Duration in ticks: %" PRIu64, passEntry->m_timestampResult.GetDurationInTicks());
ImGui::Text("Duration in ticks: %llu", static_cast<AZ::u64>(passEntry->m_timestampResult.GetDurationInTicks()));
ImGui::Text("Duration in microsecond: %.3f us", passEntry->m_timestampResult.GetDurationInNanoseconds()/1000.f);
ImGui::EndTooltip();
}
@@ -15,6 +15,7 @@
#ifdef IMGUI_ENABLED
#include <AzCore/std/string/conversions.h>
#include <AzCore/std/sort.h>
#include <AzFramework/Input/Buses/Requests/InputSystemCursorRequestBus.h>
#include <AzFramework/Input/Devices/Mouse/InputDeviceMouse.h>
#include <ILevelSystem.h>
@@ -251,15 +252,37 @@ namespace ImGui
if (usePrefabSystemForLevels)
{
char levelName[256];
ImGui::TextColored(ImGui::Colors::s_PlainLabelColor, "Load Level: ");
bool result = ImGui::InputText("", levelName, sizeof(levelName), ImGuiInputTextFlags_EnterReturnsTrue);
if (result)
// Run through all the assets in the asset catalog and gather up the list of level assets
AZ::Data::AssetType levelAssetType = lvlSystem->GetLevelAssetType();
AZStd::vector<AZStd::string> levelNames;
auto enumerateCB =
[levelAssetType, &levelNames]([[maybe_unused]] const AZ::Data::AssetId id, const AZ::Data::AssetInfo& assetInfo)
{
AZ_TracePrintf("Imgui", "Attempting to load level '%s'\n", levelName);
AZ::TickBus::QueueFunction([lvlSystem, levelName]() {
lvlSystem->LoadLevel(levelName);
});
if (assetInfo.m_assetType == levelAssetType)
{
levelNames.emplace_back(assetInfo.m_relativePath);
}
};
AZ::Data::AssetCatalogRequestBus::Broadcast(
&AZ::Data::AssetCatalogRequestBus::Events::EnumerateAssets, nullptr, enumerateCB, nullptr);
AZStd::sort(levelNames.begin(), levelNames.end());
// Create a menu item for each level asset, with an action to load it if selected.
ImGui::TextColored(ImGui::Colors::s_PlainLabelColor, "Load Level: ");
for (int i = 0; i < levelNames.size(); i++)
{
if (ImGui::MenuItem(AZStd::string::format("%d- %s", i, levelNames[i].c_str()).c_str()))
{
AZ::TickBus::QueueFunction(
[lvlSystem, levelNames, i]()
{
lvlSystem->LoadLevel(levelNames[i].c_str());
});
}
}
}
else
@@ -269,9 +292,8 @@ namespace ImGui
{
if (ImGui::MenuItem(AZStd::string::format("%d- %s", i, lvlSystem->GetLevelInfo(i)->GetName()).c_str()))
{
AZStd::string mapCommandString = AZStd::string::format("map %s", lvlSystem->GetLevelInfo(i)->GetName());
AZ::TickBus::QueueFunction([mapCommandString]() {
gEnv->pConsole->ExecuteString(mapCommandString.c_str());
AZ::TickBus::QueueFunction([lvlSystem, i]() {
lvlSystem->LoadLevel(lvlSystem->GetLevelInfo(i)->GetName());
});
}
}
@@ -24,80 +24,6 @@ namespace Multiplayer
serializeContext->Class<MultiplayerComponent, AZ::Component>()
->Version(1);
}
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
if (behaviorContext)
{
behaviorContext->Class<MultiplayerComponent>("MultiplayerComponent")
->Attribute(AZ::Script::Attributes::Module, "multiplayer")
->Attribute(AZ::Script::Attributes::Category, "Multiplayer")
->Method("IsAuthority", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsAuthority failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
MultiplayerComponent* multiplayerComponent = entity->FindComponent<MultiplayerComponent>();
if (!multiplayerComponent)
{
AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsAuthority failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return multiplayerComponent->IsAuthority();
})
->Method("IsAutonomous", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsAutonomous failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
MultiplayerComponent* multiplayerComponent = entity->FindComponent<MultiplayerComponent>();
if (!multiplayerComponent)
{
AZ_Warning("MultiplayerComponent", false, "MultiplayerComponent IsAutonomous failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return multiplayerComponent->IsAutonomous();
})
->Method("IsClient", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsClient failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
MultiplayerComponent* multiplayerComponent = entity->FindComponent<MultiplayerComponent>();
if (!multiplayerComponent)
{
AZ_Warning("MultiplayerComponent", false, "MultiplayerComponent IsClient failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return multiplayerComponent->IsClient();
})
->Method("IsServer", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "MultiplayerComponent", false, "MultiplayerComponent IsServer failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
MultiplayerComponent* multiplayerComponent = entity->FindComponent<MultiplayerComponent>();
if (!multiplayerComponent)
{
AZ_Warning("MultiplayerComponent", false, "MultiplayerComponent IsServer failed. Entity '%s' (id: %s) is missing a MultiplayerComponent, make sure this entity contains a component which derives from MultiplayerComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return multiplayerComponent->IsServer();
})
;
}
}
void MultiplayerComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required)
@@ -46,6 +46,80 @@ namespace Multiplayer
->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game"));
}
}
AZ::BehaviorContext* behaviorContext = azrtti_cast<AZ::BehaviorContext*>(context);
if (behaviorContext)
{
behaviorContext->Class<NetBindComponent>("NetBindComponent")
->Attribute(AZ::Script::Attributes::Module, "multiplayer")
->Attribute(AZ::Script::Attributes::Category, "Multiplayer")
->Method("IsAuthority", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "NetBindComponent", false, "NetBindComponent IsAuthority failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
NetBindComponent* netBindComponent = entity-> FindComponent<NetBindComponent>();
if (!netBindComponent)
{
AZ_Warning( "NetBindComponent", false, "NetBindComponent IsAuthority failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return netBindComponent->IsAuthority();
})
->Method("IsAutonomous", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "NetBindComponent", false, "NetBindComponent IsAutonomous failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
NetBindComponent* netBindComponent = entity->FindComponent<NetBindComponent>();
if (!netBindComponent)
{
AZ_Warning("NetBindComponent", false, "NetBindComponent IsAutonomous failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return netBindComponent->IsAutonomous();
})
->Method("IsClient", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "NetBindComponent", false, "NetBindComponent IsClient failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
NetBindComponent* netBindComponent = entity->FindComponent<NetBindComponent>();
if (!netBindComponent)
{
AZ_Warning("NetBindComponent", false, "NetBindComponent IsClient failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return netBindComponent->IsClient();
})
->Method("IsServer", [](AZ::EntityId id) -> bool {
AZ::Entity* entity = AZ::Interface<AZ::ComponentApplicationRequests>::Get()->FindEntity(id);
if (!entity)
{
AZ_Warning( "NetBindComponent", false, "NetBindComponent IsServer failed. The entity with id %s doesn't exist, please provide a valid entity id.", id.ToString().c_str())
return false;
}
NetBindComponent* netBindComponent = entity->FindComponent<NetBindComponent>();
if (!netBindComponent)
{
AZ_Warning("NetBindComponent", false, "NetBindComponent IsServer failed. Entity '%s' (id: %s) is missing a NetBindComponent, make sure this entity contains a component which derives from NetBindComponent.", entity->GetName().c_str(), id.ToString().c_str())
return false;
}
return netBindComponent->IsServer();
})
;
}
}
void NetBindComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided)
+70 -12
View File
@@ -2118,27 +2118,35 @@ def find_engine_data(json_data: dict,
return None
def get_engine_data(engine_name: str = None,
engine_path: str or pathlib.Path = None, ) -> dict or None:
def _validate_engine_name_and_path(engine_name: str = None,
engine_path: str or pathlib.Path = None) -> pathlib.Path or None:
if not engine_name and not engine_path:
logger.error('Must specify either a Engine name or Engine Path.')
return 1
return None
if engine_name and not engine_path:
engine_path = get_registered(engine_name=engine_name)
if not engine_path:
logger.error(f'Engine Path {engine_path} has not been registered.')
return 1
return None
engine_path = pathlib.Path(engine_path).resolve()
engine_json = engine_path / 'engine.json'
if not engine_json.is_file():
logger.error(f'Engine json {engine_json} is not present.')
return 1
return None
if not valid_o3de_engine_json(engine_json):
logger.error(f'Engine json {engine_json} is not valid.')
return 1
return None
return engine_json
def get_engine_data(engine_name: str = None,
engine_path: str or pathlib.Path = None ) -> dict or None:
engine_json = _validate_engine_name_and_path(engine_name, engine_path)
if not engine_json:
return None
with engine_json.open('r') as f:
try:
@@ -2150,6 +2158,26 @@ def get_engine_data(engine_name: str = None,
return None
def set_engine_data(engine_name: str = None,
engine_path: str or pathlib.Path = None,
engine_data: dict = None ) -> int:
if not engine_data:
logger.error('Must provide engine data.')
return 1
engine_json = _validate_engine_name_and_path(engine_name, engine_path)
if not engine_json:
return 1
with engine_json.open('w') as f:
try:
json.dump(engine_data, f, indent=4)
except Exception as e:
logger.warn(f'Failed to load or write {engine_json}: {str(e)}')
return 1
return 0
def get_project_data(project_name: str = None,
project_path: str or pathlib.Path = None, ) -> dict or None:
@@ -2184,27 +2212,36 @@ def get_project_data(project_name: str = None,
return None
def get_gem_data(gem_name: str = None,
gem_path: str or pathlib.Path = None, ) -> dict or None:
def _validate_gem_name_and_path(gem_name: str = None,
gem_path: str or pathlib.Path = None) -> pathlib.Path or None:
if not gem_name and not gem_path:
logger.error('Must specify either a Gem name or Gem Path.')
return 1
return None
if gem_name and not gem_path:
gem_path = get_registered(gem_name=gem_name)
if not gem_path:
logger.error(f'Gem Path {gem_path} has not been registered.')
return 1
return None
gem_path = pathlib.Path(gem_path).resolve()
gem_json = gem_path / 'gem.json'
if not gem_json.is_file():
logger.error(f'Gem json {gem_json} is not present.')
return 1
return None
if not valid_o3de_gem_json(gem_json):
logger.error(f'Gem json {gem_json} is not valid.')
return 1
return None
return gem_json
def get_gem_data(gem_name: str = None,
gem_path: str or pathlib.Path = None) -> dict or None:
gem_json = _validate_gem_name_and_path(gem_name, gem_path)
if not gem_json:
return None
with gem_json.open('r') as f:
try:
@@ -2217,6 +2254,27 @@ def get_gem_data(gem_name: str = None,
return None
def set_gem_data(gem_name: str = None,
gem_path: str or pathlib.Path = None,
gem_data: dict = None) -> int:
if not gem_data:
logger.error('Must provide Gem data.')
return 1
gem_json = _validate_gem_name_and_path(gem_name, gem_path)
if not gem_json:
return 1
with gem_json.open('w') as f:
try:
json.dump(gem_data, f, indent=4)
except Exception as e:
logger.warn(f'Failed to load and write {gem_json}: {str(e)}')
return 1
return 0
def get_template_data(template_name: str = None,
template_path: str or pathlib.Path = None, ) -> dict or None:
if not template_name and not template_path:
+1 -1
View File
@@ -352,7 +352,7 @@ def TestMetrics(Map pipelineConfig, String workspace, String branchName, String
def command = "${pipelineConfig.PYTHON_DIR}/python.cmd -u mars/scripts/python/ctest_test_metric_scraper.py " +
'-e jenkins.creds.user %username% -e jenkins.creds.pass %apitoken% ' +
"-e jenkins.base_url ${env.JENKINS_URL} " +
"${cmakeBuildDir} ${branchName} %BUILD_NUMBER% AR ${configuration} ${repoName} "
"${cmakeBuildDir} ${branchName} %BUILD_NUMBER% AR ${configuration} ${repoName} --url ${env.BUILD_URL}"
bat label: "Publishing ${buildJobName} Test Metrics",
script: command
}