diff --git a/Code/Framework/AzCore/AzCore/Math/Random.h b/Code/Framework/AzCore/AzCore/Math/Random.h index 5ae37433ec..8b28f6aaad 100644 --- a/Code/Framework/AzCore/AzCore/Math/Random.h +++ b/Code/Framework/AzCore/AzCore/Math/Random.h @@ -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(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 + 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 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 + AZStd::array, N> GetHaltonSequence() + { + AZStd::array, N> result; + + AZStd::array indices = m_offsets; + + // Generator that returns the Halton number for all bases for a single entry. + auto f = [&] () + { + AZStd::array 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 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 increments) + { + m_increments = increments; + } + + private: + + AZStd::array m_bases; + AZStd::array m_offsets; + AZStd::array m_increments; + + }; } diff --git a/Code/Framework/AzCore/Tests/Math/RandomTests.cpp b/Code/Framework/AzCore/Tests/Math/RandomTests.cpp new file mode 100644 index 0000000000..ace7d99704 --- /dev/null +++ b/Code/Framework/AzCore/Tests/Math/RandomTests.cpp @@ -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 +#include + +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]); + } +} diff --git a/Code/Framework/AzCore/Tests/azcoretests_files.cmake b/Code/Framework/AzCore/Tests/azcoretests_files.cmake index f90717d003..78b2701d92 100644 --- a/Code/Framework/AzCore/Tests/azcoretests_files.cmake +++ b/Code/Framework/AzCore/Tests/azcoretests_files.cmake @@ -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 diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h index f51af58c51..6c230ff5c7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/EditorEntityAPI.h @@ -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 diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp index 80d7fc7c5a..880217e807 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp @@ -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); + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h index 580ad22bda..939f73729e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h @@ -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; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h index 5ee91c85ae..4feecb9da3 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabDomUtils.h @@ -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"; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index 571aea5875..579f465eb2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -13,6 +13,8 @@ #include #include +#include +#include #include #include @@ -31,6 +33,8 @@ #include #include +#include + namespace AzToolsFramework { namespace Prefab @@ -83,7 +87,8 @@ namespace AzToolsFramework commonRootInstanceDomBeforeCreate, commonRootEntityOwningInstance->get()); AZStd::vector entities; - AZStd::vector> instances; + AZStd::vector> instancePtrs; + AZStd::vector instances; AZStd::unordered_map 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 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 entities; + AZStd::vector 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 oldAliasToNewAliasMap; + AZStd::unordered_map 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 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 entities; - AZStd::vector> instances; + AZStd::vector 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 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>& outInstances) const + EntityList& outEntities, AZStd::vector& 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; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index 5ad5b4a9cf..223a725c6c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -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>& outInstances) const; + EntityList& outEntities, AZStd::vector& outInstances) const; InstanceOptionalReference GetOwnerInstanceByEntityId(AZ::EntityId entityId) const; bool EntitiesBelongToSameInstance(const EntityIdList& entityIds) const; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 1a8da0dfe0..0750c4d264 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -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 diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp index 5b44594398..4a72afb16b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp @@ -63,6 +63,7 @@ #include #include #include +#include #include //////////////////////////////////////////////////////////////////////////// @@ -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) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h index 53dab661ce..276982d46d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UnitTest/AzToolsFrameworkTestHelpers.h @@ -138,7 +138,7 @@ namespace UnitTest if (!GetApplication()) { // Create & Start a new ToolsApplication if there's no existing one - m_app = AZStd::make_unique("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 CreateTestApplication() + { + return AZStd::make_unique("ToolsApplication"); + } + private: AZStd::unique_ptr m_app; }; diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp new file mode 100644 index 0000000000..514942166f --- /dev/null +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabDuplicateTests.cpp @@ -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 + +#include +#include +#include + +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(); + entity1->Activate(); + + AzToolsFramework::EditorEntityContextRequestBus::Broadcast( + &AzToolsFramework::EditorEntityContextRequests::HandleEntitiesAdded, AzToolsFramework::EntityList{ entity1 }); + AZStd::unique_ptr 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(); + 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(); + 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 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(); + 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(); + 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); + } +} diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp index 3a8d9cc7eb..ace2356732 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.cpp @@ -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::Get(); EXPECT_TRUE(m_prefabLoaderInterface); + m_prefabPublicInterface = AZ::Interface::Get(); + EXPECT_TRUE(m_prefabPublicInterface); + m_instanceUpdateExecutorInterface = AZ::Interface::Get(); EXPECT_TRUE(m_instanceUpdateExecutorInterface); @@ -41,6 +55,11 @@ namespace UnitTest GetApplication()->RegisterComponentDescriptor(PrefabTestComponent::CreateDescriptor()); } + AZStd::unique_ptr PrefabTestFixture::CreateTestApplication() + { + return AZStd::make_unique("PrefabTestApplication"); + } + AZ::Entity* PrefabTestFixture::CreateEntity(const char* entityName, const bool shouldActivate) { // Circumvent the EntityContext system and generate a new entity with a transformcomponent diff --git a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h index af90309867..ee471cf192 100644 --- a/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h +++ b/Code/Framework/AzToolsFramework/Tests/Prefab/PrefabTestFixture.h @@ -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 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; }; diff --git a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake index e54aa187e4..cd3796a64e 100644 --- a/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake +++ b/Code/Framework/AzToolsFramework/Tests/aztoolsframeworktests_files.cmake @@ -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 diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp index 027487a435..d36c20c56a 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp @@ -35,6 +35,7 @@ #include #include #include +#include #include #include #include @@ -192,6 +193,9 @@ void SandboxIntegrationManager::Setup() (m_prefabIntegrationInterface != nullptr), "SandboxIntegrationManager requires a PrefabIntegrationInterface instance to be present on Setup()."); + m_editorEntityAPI = AZ::Interface::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 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 clonedEntities; + handled = AzToolsFramework::CloneInstantiatedEntities(duplicationSet, clonedEntities); + m_unsavedEntities.insert(clonedEntities.begin(), clonedEntities.end()); + } } else { diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h index 14f52591a4..528b93e44e 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h @@ -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; diff --git a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp index ac9b92adce..11c279c7f7 100644 --- a/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp +++ b/Code/Sandbox/Plugins/ComponentEntityEditorPlugin/UI/Outliner/OutlinerListModel.cpp @@ -65,6 +65,7 @@ #include "OutlinerTreeView.hxx" #include "Include/ICommandManager.h" #include "Include/IObjectManager.h" +#include "OutlinerCacheBus.h" #include #include @@ -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) diff --git a/Code/Tools/ProjectManager/Resources/Add.svg b/Code/Tools/ProjectManager/Resources/Add.svg index d2b9b2e0a6..4fa30932fb 100644 --- a/Code/Tools/ProjectManager/Resources/Add.svg +++ b/Code/Tools/ProjectManager/Resources/Add.svg @@ -1,4 +1,3 @@ - diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc new file mode 100644 index 0000000000..1ffd7cf3e7 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -0,0 +1,16 @@ + + + ProjectManager.qss + + + Add.svg + Select_Folder.svg + o3de_editor.ico + Windows.svg + Android.svg + iOS.svg + Linux.svg + macOS.svg + Backgrounds/FirstTimeBackgroundImage.jpg + + diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index e69de29bb2..16ef48ee7c 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -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; +} + diff --git a/Code/Tools/ProjectManager/Resources/Select_Folder.svg b/Code/Tools/ProjectManager/Resources/Select_Folder.svg index 72dcd3385e..df20a06e76 100644 --- a/Code/Tools/ProjectManager/Resources/Select_Folder.svg +++ b/Code/Tools/ProjectManager/Resources/Select_Folder.svg @@ -1,4 +1,3 @@ - diff --git a/Code/Tools/ProjectManager/Source/EngineInfo.cpp b/Code/Tools/ProjectManager/Source/EngineInfo.cpp index 8043a498ff..934d3af9d8 100644 --- a/Code/Tools/ProjectManager/Source/EngineInfo.cpp +++ b/Code/Tools/ProjectManager/Source/EngineInfo.cpp @@ -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 diff --git a/Code/Tools/ProjectManager/Source/EngineInfo.h b/Code/Tools/ProjectManager/Source/EngineInfo.h index ada6e73a15..262c42e56b 100644 --- a/Code/Tools/ProjectManager/Source/EngineInfo.h +++ b/Code/Tools/ProjectManager/Source/EngineInfo.h @@ -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 diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp index 1adab41c0e..f51996bd65 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp @@ -11,20 +11,99 @@ */ #include - -#include +#include +#include +#include +#include +#include +#include +#include +#include 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 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 diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h index 4baa3fb28c..0e91ec2d3b 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h @@ -15,13 +15,11 @@ #include #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 m_ui; + FormLineEditWidget* m_engineVersion; + FormBrowseEditWidget* m_thirdParty; + FormBrowseEditWidget* m_defaultProjects; + FormBrowseEditWidget* m_defaultGems; + FormBrowseEditWidget* m_defaultProjectTemplates; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui deleted file mode 100644 index c8fda8bfd7..0000000000 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.ui +++ /dev/null @@ -1,82 +0,0 @@ - - - EngineSettingsClass - - - - 0 - 0 - 839 - 597 - - - - Form - - - - - - O3DE Settings - - - - - - - Engine Version - - - - - - - v1.01 - - - - - - - 3rd Party Software Folder - - - - - - - - - - Restricted Folder - - - - - - - - - - Default Gems Folder - - - - - - - - - - Default Project Templates Folder - - - - - - - - - - - diff --git a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp index 2c96078d43..a1be7e8ac9 100644 --- a/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp +++ b/Code/Tools/ProjectManager/Source/FirstTimeUseScreen.cpp @@ -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); diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp new file mode 100644 index 0000000000..c30d6a7b30 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include + +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 diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h new file mode 100644 index 0000000000..887fc29dd9 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h @@ -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 +#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 diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp new file mode 100644 index 0000000000..7ef7e3c7d8 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.cpp @@ -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 +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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()) + { + child->style()->unpolish(child); + child->style()->polish(child); + } + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/FormLineEditWidget.h b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h new file mode 100644 index 0000000000..3094442cbd --- /dev/null +++ b/Code/Tools/ProjectManager/Source/FormLineEditWidget.h @@ -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 +#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 diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp index 434a4aeef2..9a45600f70 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemItemDelegate.cpp @@ -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) diff --git a/Code/Tools/ProjectManager/Source/PathValidator.cpp b/Code/Tools/ProjectManager/Source/PathValidator.cpp new file mode 100644 index 0000000000..8b74284b6c --- /dev/null +++ b/Code/Tools/ProjectManager/Source/PathValidator.cpp @@ -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 +#include +#include + +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 diff --git a/Code/Tools/ProjectManager/Source/PathValidator.h b/Code/Tools/ProjectManager/Source/PathValidator.h new file mode 100644 index 0000000000..aeb35571b9 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/PathValidator.h @@ -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 +#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 diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp index 6b9d268564..121add657f 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.cpp @@ -44,10 +44,10 @@ namespace O3DE::ProjectManager QDir rootDir = QString::fromUtf8(engineRootPath.Native().data(), aznumeric_cast(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 screenEnums = { diff --git a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui index a71ed3aabf..4e33511bff 100644 --- a/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui +++ b/Code/Tools/ProjectManager/Source/ProjectManagerWindow.ui @@ -41,8 +41,8 @@ Icon - - :/Resources/o3de_editor.ico:/Resources/o3de_editor.ico + + :/o3de_editor.ico:/o3de_editor.ico @@ -61,7 +61,7 @@ - + diff --git a/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp b/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp index fd1013c871..95dcec3e18 100644 --- a/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ProjectSettingsCtrl.cpp @@ -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 diff --git a/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui b/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui index ea3e34d84b..2ba93ccf90 100644 --- a/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui +++ b/Code/Tools/ProjectManager/Source/ProjectsHomeScreen.ui @@ -48,8 +48,8 @@ - - :/Resources/Add.svg:/Resources/Add.svg + + :/Add.svg:/Add.svg @@ -65,8 +65,8 @@ - - :/Resources/Select_Folder.svg:/Resources/Select_Folder.svg + + :/Select_Folder.svg:/Select_Folder.svg @@ -131,7 +131,7 @@ - + diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index e4642c95e0..9a5e82dafb 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -328,12 +328,91 @@ namespace O3DE::ProjectManager AZ::Outcome 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(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(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() != 0) + { + result = false; + } + + auto manifest = m_registration.attr("load_o3de_manifest")(); + if (pybind11::isinstance(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 PythonBindings::GetGem(const QString& path) diff --git a/Code/Tools/ProjectManager/project_manager.qrc b/Code/Tools/ProjectManager/project_manager.qrc deleted file mode 100644 index f36633142f..0000000000 --- a/Code/Tools/ProjectManager/project_manager.qrc +++ /dev/null @@ -1,16 +0,0 @@ - - - Resources/ProjectManager.qss - Resources/Add.svg - Resources/Select_Folder.svg - Resources/o3de_editor.ico - Resources/Windows.svg - Resources/Android.svg - Resources/iOS.svg - Resources/Linux.svg - Resources/macOS.svg - Resources/ArrowDownLine.svg - Resources/ArrowUpLine.svg - Resources/Backgrounds/FirstTimeBackgroundImage.jpg - - diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index 3594d1e079..858fb972aa 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -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 diff --git a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass index 3e804d23e2..ff55ebc200 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/LightAdaptationParent.pass @@ -80,7 +80,7 @@ { "Name": "EyeAdaptationPass", "TemplateName": "EyeAdaptationTemplate", - "Enabled": false, + "Enabled": true, "Connections": [ { "LocalSlot": "SceneLuminanceInput", diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp index a76467bccd..816ef25bc6 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/LightCullingTilePreparePass.cpp @@ -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&) { 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 diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp index b22f4b5861..056f7b7da4 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.cpp @@ -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& eyeAdaptationPasses = passSystem->GetPassesForTemplateName(passTemplateName); - for (RPI::Pass* pass : eyeAdaptationPasses) - { - auto* eyeAdaptationPass = azrtti_cast(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(); diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h index 566f60dd28..8344d6aa09 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcess/ExposureControl/ExposureControlSettings.h @@ -85,7 +85,6 @@ namespace AZ void UpdateExposureControlRelatedPassParameters(); void UpdateLuminanceHeatmap(); - void UpdateEyeAdaptationPass(); PostProcessSettings* m_parentSettings = nullptr; bool m_shouldUpdatePassParameters = true; diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp index 97c11a9d89..e7d4c47f02 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.cpp @@ -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) { diff --git a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h index b87d9db86f..cef168a122 100644 --- a/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h +++ b/Gems/Atom/Feature/Common/Code/Source/PostProcessing/EyeAdaptationPass.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -45,12 +46,11 @@ namespace AZ static RPI::Ptr 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 m_buffer; // SRG binding indices... - AZ::RHI::ShaderInputBufferIndex m_exposureControlBufferInputIndex; + AZ::RHI::ShaderInputNameIndex m_exposureControlBufferInputIndex = "m_exposureControl"; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h index 84ccb57e06..5cd917e841 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/RenderPass.h @@ -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); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp index 6ed8ac018c..8d613eb2bb 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp @@ -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 --- diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp index 3f5c16678c..9c6a95e582 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp @@ -342,7 +342,7 @@ namespace AZ } } - ViewPtr RenderPass::GetView() + ViewPtr RenderPass::GetView() const { if (m_flags.m_hasPipelineViewTag && m_pipeline) { diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl index fa9395dffc..b3cdb591f4 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl @@ -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(passEntry->m_timestampResult.GetDurationInTicks())); ImGui::Text("Duration in microsecond: %.3f us", passEntry->m_timestampResult.GetDurationInNanoseconds()/1000.f); ImGui::EndTooltip(); } diff --git a/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp b/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp index 8e3e2663d5..5107a02835 100644 --- a/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp +++ b/Gems/ImGui/Code/Source/LYCommonMenu/ImGuiLYCommonMenu.cpp @@ -15,6 +15,7 @@ #ifdef IMGUI_ENABLED #include +#include #include #include #include @@ -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 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()); }); } } diff --git a/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp b/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp index 21ae9d9f01..8542288b23 100644 --- a/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/MultiplayerComponent.cpp @@ -24,80 +24,6 @@ namespace Multiplayer serializeContext->Class() ->Version(1); } - - AZ::BehaviorContext* behaviorContext = azrtti_cast(context); - if (behaviorContext) - { - behaviorContext->Class("MultiplayerComponent") - ->Attribute(AZ::Script::Attributes::Module, "multiplayer") - ->Attribute(AZ::Script::Attributes::Category, "Multiplayer") - - ->Method("IsAuthority", [](AZ::EntityId id) -> bool { - AZ::Entity* entity = AZ::Interface::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(); - 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::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(); - 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::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(); - 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::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(); - 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) diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index adc369e9ed..d91bba2e0c 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -46,6 +46,80 @@ namespace Multiplayer ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")); } } + + AZ::BehaviorContext* behaviorContext = azrtti_cast(context); + if (behaviorContext) + { + behaviorContext->Class("NetBindComponent") + ->Attribute(AZ::Script::Attributes::Module, "multiplayer") + ->Attribute(AZ::Script::Attributes::Category, "Multiplayer") + + ->Method("IsAuthority", [](AZ::EntityId id) -> bool { + AZ::Entity* entity = AZ::Interface::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(); + 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::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(); + 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::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(); + 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::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(); + 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) diff --git a/cmake/Tools/registration.py b/cmake/Tools/registration.py index 184d2cdb31..b13419414b 100755 --- a/cmake/Tools/registration.py +++ b/cmake/Tools/registration.py @@ -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: diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index eb0778791d..2f6be2b060 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -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 }