diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py index 69a0e9c85d..45298ed563 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_Optimized.py @@ -37,6 +37,10 @@ class TestAutomation(EditorTestSuite): @pytest.mark.test_case_id("C32078115") class AtomEditorComponents_GlobalSkylightIBLAdded(EditorSharedTest): from Atom.tests import hydra_AtomEditorComponents_GlobalSkylightIBLAdded as test_module + + @pytest.mark.test_case_id("C32078122") + class AtomEditorComponents_GridAdded(EditorSharedTest): + from Atom.tests import hydra_AtomEditorComponents_GridAdded as test_module @pytest.mark.test_case_id("C32078117") class AtomEditorComponents_LightAdded(EditorSharedTest): diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_GridAdded.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_GridAdded.py new file mode 100644 index 0000000000..a77a1f50a4 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_GridAdded.py @@ -0,0 +1,158 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +class Tests: + creation_undo = ( + "UNDO Entity creation success", + "UNDO Entity creation failed") + creation_redo = ( + "REDO Entity creation success", + "REDO Entity creation failed") + grid_entity_creation = ( + "Grid Entity successfully created", + "Grid Entity failed to be created") + grid_component_added = ( + "Entity has a Grid component", + "Entity failed to find Grid component") + enter_game_mode = ( + "Entered game mode", + "Failed to enter game mode") + exit_game_mode = ( + "Exited game mode", + "Couldn't exit game mode") + is_visible = ( + "Entity is visible", + "Entity was not visible") + is_hidden = ( + "Entity is hidden", + "Entity was not hidden") + entity_deleted = ( + "Entity deleted", + "Entity was not deleted") + deletion_undo = ( + "UNDO deletion success", + "UNDO deletion failed") + deletion_redo = ( + "REDO deletion success", + "REDO deletion failed") + + +def AtomEditorComponents_Grid_AddedToEntity(): + """ + Summary: + Tests the Grid component can be added to an entity and has the expected functionality. + + Test setup: + - Wait for Editor idle loop. + - Open the "Base" level. + + Expected Behavior: + The component can be added, used in game mode, hidden/shown, deleted, and has accurate required components. + Creation and deletion undo/redo should also work. + + Test Steps: + 1) Create a Grid entity with no components. + 2) Add a Grid component to Grid entity. + 3) UNDO the entity creation and component addition. + 4) REDO the entity creation and component addition. + 5) Enter/Exit game mode. + 6) Test IsHidden. + 7) Test IsVisible. + 8) Delete Grid entity. + 9) UNDO deletion. + 10) REDO deletion. + 11) Look for errors. + + :return: None + """ + + import os + + import azlmbr.legacy.general as general + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.utils import Report, Tracer, TestHelper + from Atom.atom_utils.atom_constants import AtomComponentProperties + + with Tracer() as error_tracer: + # Test setup begins. + # Setup: Wait for Editor idle loop before executing Python hydra scripts then open "Base" level. + TestHelper.init_idle() + TestHelper.open_level("", "Base") + + # Test steps begin. + # 1. Create a Grid entity with no components. + grid_entity = EditorEntity.create_editor_entity(AtomComponentProperties.grid()) + Report.critical_result(Tests.grid_entity_creation, grid_entity.exists()) + + # 2. Add a Grid component to Grid entity. + grid_component = grid_entity.add_component(AtomComponentProperties.grid()) + Report.critical_result( + Tests.grid_component_added, + grid_entity.has_component(AtomComponentProperties.grid())) + + # 3. UNDO the entity creation and component addition. + # -> UNDO component addition. + general.undo() + # -> UNDO naming entity. + general.undo() + # -> UNDO selecting entity. + general.undo() + # -> UNDO entity creation. + general.undo() + general.idle_wait_frames(1) + Report.result(Tests.creation_undo, not grid_entity.exists()) + + # 4. REDO the entity creation and component addition. + # -> REDO entity creation. + general.redo() + # -> REDO selecting entity. + general.redo() + # -> REDO naming entity. + general.redo() + # -> REDO component addition. + general.redo() + general.idle_wait_frames(1) + Report.result(Tests.creation_redo, grid_entity.exists()) + + # 5. Enter/Exit game mode. + TestHelper.enter_game_mode(Tests.enter_game_mode) + general.idle_wait_frames(1) + TestHelper.exit_game_mode(Tests.exit_game_mode) + + # 6. Test IsHidden. + grid_entity.set_visibility_state(False) + Report.result(Tests.is_hidden, grid_entity.is_hidden() is True) + + # 7. Test IsVisible. + grid_entity.set_visibility_state(True) + general.idle_wait_frames(1) + Report.result(Tests.is_visible, grid_entity.is_visible() is True) + + # 8. Delete Grid entity. + grid_entity.delete() + Report.result(Tests.entity_deleted, not grid_entity.exists()) + + # 9. UNDO deletion. + general.undo() + Report.result(Tests.deletion_undo, grid_entity.exists()) + + # 10. REDO deletion. + general.redo() + Report.result(Tests.deletion_redo, not grid_entity.exists()) + + # 11. Look for errors or asserts. + TestHelper.wait_for_condition(lambda: error_tracer.has_errors or error_tracer.has_asserts, 1.0) + for error_info in error_tracer.errors: + Report.info(f"Error: {error_info.filename} {error_info.function} | {error_info.message}") + for assert_info in error_tracer.asserts: + Report.info(f"Assert: {assert_info.filename} {assert_info.function} | {assert_info.message}") + + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(AtomEditorComponents_Grid_AddedToEntity) diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFXLayerAdded.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFXLayerAdded.py index a81864f379..efef4c0a43 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFXLayerAdded.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_AtomEditorComponents_PostFXLayerAdded.py @@ -70,8 +70,6 @@ def AtomEditorComponents_postfx_layer_AddedToEntity(): :return: None """ - import os - import azlmbr.legacy.general as general from editor_python_test_tools.editor_entity_utils import EditorEntity diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py index 844f8de903..9e857ed8bc 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/editor_entity_utils.py @@ -107,6 +107,19 @@ class EditorComponent: return type_ids + +def convert_to_azvector3(xyz) -> azlmbr.math.Vector3: + """ + Converts a vector3-like element into a azlmbr.math.Vector3 + """ + if isinstance(xyz, Tuple) or isinstance(xyz, List): + assert len(xyz) == 3, ValueError("vector must be a 3 element list/tuple or azlmbr.math.Vector3") + return math.Vector3(float(xyz[0]), float(xyz[1]), float(xyz[2])) + elif isinstance(xyz, type(math.Vector3())): + return xyz + else: + raise ValueError("vector must be a 3 element list/tuple or azlmbr.math.Vector3") + class EditorEntity: """ Entity class is used to create and interact with Editor Entities. @@ -183,15 +196,6 @@ class EditorEntity: :return: EditorEntity class object """ - def convert_to_azvector3(xyz) -> math.Vector3: - if isinstance(xyz, Tuple) or isinstance(xyz, List): - assert len(xyz) == 3, ValueError("vector must be a 3 element list/tuple or azlmbr.math.Vector3") - return math.Vector3(*xyz) - elif isinstance(xyz, type(math.Vector3())): - return xyz - else: - raise ValueError("vector must be a 3 element list/tuple or azlmbr.math.Vector3") - if parent_id is None: parent_id = azlmbr.entity.EntityId() @@ -206,7 +210,7 @@ class EditorEntity: return entity # Methods - def set_name(self, entity_name: str): + def set_name(self, entity_name: str) -> None: """ Given entity_name, sets name to Entity :param: entity_name: Name of the entity to set @@ -324,7 +328,7 @@ class EditorEntity: self.start_status = status return status - def set_start_status(self, desired_start_status: str): + def set_start_status(self, desired_start_status: str) -> None: """ Set an entity as active/inactive at beginning of runtime or it is editor-only, given its entity id and the start status then return set success @@ -382,18 +386,75 @@ class EditorEntity: """ return editor.EditorEntityInfoRequestBus(bus.Event, "IsVisible", self.id) + # World Transform Functions + def get_world_translation(self) -> azlmbr.math.Vector3: + """ + Gets the world translation of the entity + """ + return azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", self.id) + + def set_world_translation(self, new_translation) -> None: + """ + Sets the new world translation of the current entity + """ + new_translation = convert_to_azvector3(new_translation) + azlmbr.components.TransformBus(azlmbr.bus.Event, "SetWorldTranslation", self.id, new_translation) + + def get_world_rotation(self) -> azlmbr.math.Quaternion: + """ + Gets the world rotation of the entity + """ + return azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldRotation", self.id) + + def set_world_rotation(self, new_rotation): + """ + Sets the new world rotation of the current entity + """ + new_rotation = convert_to_azvector3(new_rotation) + azlmbr.components.TransformBus(azlmbr.bus.Event, "SetWorldRotation", self.id, new_rotation) + + # Local Transform Functions + def get_local_uniform_scale(self) -> float: + """ + Gets the local uniform scale of the entity + """ + return azlmbr.components.TransformBus(azlmbr.bus.Event, "GetLocalUniformScale", self.id) + def set_local_uniform_scale(self, scale_float) -> None: """ - Sets the "SetLocalUniformScale" value on the entity. + Sets the local uniform scale value(relative to the parent) on the entity. :param scale_float: value for "SetLocalUniformScale" to set to. :return: None """ azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalUniformScale", self.id, scale_float) - def set_local_rotation(self, vector3_rotation) -> None: + def get_local_rotation(self) -> azlmbr.math.Quaternion: """ - Sets the "SetLocalRotation" value on the entity. + Gets the local rotation of the entity + """ + return azlmbr.components.TransformBus(azlmbr.bus.Event, "GetLocalRotation", self.id) + + def set_local_rotation(self, new_rotation) -> None: + """ + Sets the set the local rotation(relative to the parent) of the current entity. :param vector3_rotation: The math.Vector3 value to use for rotation on the entity (uses radians). :return: None """ - azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", self.id, vector3_rotation) + new_rotation = convert_to_azvector3(new_rotation) + azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalRotation", self.id, new_rotation) + + def get_local_translation(self) -> azlmbr.math.Vector3: + """ + Gets the local translation of the current entity. + :return: The math.Vector3 value of the local translation. + """ + return azlmbr.components.TransformBus(azlmbr.bus.Event, "GetLocalTranslation", self.id) + + def set_local_translation(self, new_translation) -> None: + """ + Sets the local translation(relative to the parent) of the current entity. + :param vector3_translation: The math.Vector3 value to use for translation on the entity. + :return: None + """ + new_translation = convert_to_azvector3(new_translation) + azlmbr.components.TransformBus(azlmbr.bus.Event, "SetLocalTranslation", self.id, new_translation) diff --git a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py index 10a6ab1ef4..76c2a42c0a 100644 --- a/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py +++ b/AutomatedTesting/Gem/PythonTests/EditorPythonTestTools/editor_python_test_tools/prefab_utils.py @@ -138,6 +138,14 @@ class PrefabInstance: self.container_entity = reparented_container_entity current_instance_prefab.instances.add(self) + def get_direct_child_entities(self): + """ + Returns the entities only contained in the current prefab instance. + This function does not return entities contained in other child instances + """ + return self.container_entity.get_children() + + # This is a helper class which contains some of the useful information about a prefab template. class Prefab: diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py index 5337f0669c..479915752f 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py @@ -55,3 +55,11 @@ class TestAutomation(TestAutomationBase): def test_PrefabBasicWorkflow_CreateAndDuplicatePrefab(self, request, workspace, editor, launcher_platform): from .tests import PrefabBasicWorkflow_CreateAndDuplicatePrefab as test_module self._run_prefab_test(request, workspace, editor, test_module) + + def test_PrefabComplexWorflow_CreatePrefabOfChildEntity(self, request, workspace, editor, launcher_platform): + from .tests import PrefabComplexWorflow_CreatePrefabOfChildEntity as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabComplexWorflow_CreatePrefabInsidePrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabComplexWorflow_CreatePrefabInsidePrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabComplexWorflow_CreatePrefabInsidePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabComplexWorflow_CreatePrefabInsidePrefab.py new file mode 100644 index 0000000000..e14fc96449 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabComplexWorflow_CreatePrefabInsidePrefab.py @@ -0,0 +1,57 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +def PrefabComplexWorflow_CreatePrefabInsidePrefab(): + """ + Test description: + - Creates an entity with a physx collider + - Creates a prefab "Outer_prefab" and an instance based of that entity + - Creates a prefab "Inner_prefab" inside "Outer_prefab" based the entity contained inside of it + Checks that the entity is correctly handlded by the prefab system checking the name and that it contains the physx collider + """ + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new Entity at the root level + # Asserts if creation didn't succeed + entity = EditorEntity.create_editor_entity_at((100.0, 100.0, 100.0), name = "TestEntity") + assert entity.id.IsValid(), "Couldn't create entity" + entity.add_component("PhysX Collider") + assert entity.has_component("PhysX Collider"), "Attempted to add a PhysX Collider but no physx collider collider was found afterwards" + + # Create a prefab based on that entity + outer_prefab, outer_instance = Prefab.create_prefab([entity], "Outer_prefab") + # The test should be now inside the outer prefab instance. + entity = outer_instance.get_direct_child_entities()[0] + # We track if that is the same entity by checking the name and if it still contains the component that we created before + assert entity.get_name() == "TestEntity", f"Entity name inside outer_prefab doesn't match the original name, original:'TestEntity' current:'{entity.get_name()}'" + assert entity.has_component("PhysX Collider"), "Entity name inside outer_prefab doesn't have the collider component it should" + + # Now, create another prefab, based on the entity that is inside outer_prefab + inner_prefab, inner_instance = Prefab.create_prefab([entity], "Inner_prefab") + # The test entity should now be inside the inner prefab instance + entity = inner_instance.get_direct_child_entities()[0] + # We track if that is the same entity by checking the name and if it still contains the component that we created before + assert entity.get_name() == "TestEntity", f"Entity name inside inner_prefab doesn't match the original name, original:'TestEntity' current:'{entity.get_name()}'" + assert entity.has_component("PhysX Collider"), "Entity name inside inner_prefab doesn't have the collider component it should" + + # Verify hierarchy of entities: + # Outer_prefab + # |- Inner_prefab + # | |- TestEntity + assert entity.get_parent_id() == inner_instance.container_entity.id + assert inner_instance.container_entity.get_parent_id() == outer_instance.container_entity.id + + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabComplexWorflow_CreatePrefabInsidePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabComplexWorflow_CreatePrefabOfChildEntity.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabComplexWorflow_CreatePrefabOfChildEntity.py new file mode 100644 index 0000000000..dec44d52be --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabComplexWorflow_CreatePrefabOfChildEntity.py @@ -0,0 +1,52 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" + +def PrefabComplexWorflow_CreatePrefabOfChildEntity(): + """ + Test description: + - Creates two entities, parent and child. Child entity has Parent entity as its parent. + - Creates a prefab of the child entity. + Test is successful if the new instanced prefab of the child has the parent entity id + """ + + CAR_PREFAB_FILE_NAME = 'car_prefab' + + from editor_python_test_tools.editor_entity_utils import EditorEntity + from editor_python_test_tools.prefab_utils import Prefab + + import PrefabTestUtils as prefab_test_utils + + prefab_test_utils.open_base_tests_level() + + # Creates a new Entity at the root level + # Asserts if creation didn't succeed + parent_entity = EditorEntity.create_editor_entity_at((100.0, 100.0, 100.0)) + assert parent_entity.id.IsValid(), "Couldn't create parent entity" + + child_entity = EditorEntity.create_editor_entity(parent_id=parent_entity.id) + assert child_entity.id.IsValid(), "Couldn't create child entity" + assert child_entity.get_world_translation().IsClose(parent_entity.get_world_translation()), f"Child entity position{child_entity.get_world_translation().ToString()}" \ + f" is not located at the same position as the parent{parent_entity.get_world_translation().ToString()}" + + # Asserts if prefab creation doesn't succeed + child_prefab, child_instance = Prefab.create_prefab([child_entity], CAR_PREFAB_FILE_NAME) + child_entity_on_child_instance = child_instance.get_direct_child_entities()[0] + assert child_instance.container_entity.get_parent_id().IsValid(), "Newly instanced entity has no parent" + assert child_instance.container_entity.get_parent_id() == parent_entity.id, "Newly instanced entity parent does not match the expected parent" + assert child_instance.container_entity.get_world_translation().IsClose(parent_entity.get_world_translation()), "Newly instanced entity position is not located at the same position as the parent" + # Move the parent position, it should update the child position + parent_entity.set_world_translation((200.0, 200.0, 200.0)) + child_instance_translation = child_instance.container_entity.get_world_translation() + assert child_instance_translation.IsClose(azlmbr.math.Vector3(200.0, 200.0, 200.0)), f"Instance position position{child_instance_translation.ToString()} didn't get updated" \ + f" to the same position as the parent{parent_entity.get_world_translation().ToString()}" + child_translation = child_entity_on_child_instance.get_world_translation() + assert child_translation.IsClose(azlmbr.math.Vector3(200.0, 200.0, 200.0)), f"Entity position{child_translation.ToString()} of the instance didn't get updated" \ + f" to the same position as the parent{parent_entity.get_world_translation().ToString()}" + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabComplexWorflow_CreatePrefabOfChildEntity) diff --git a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py index 443a420b61..a6e8b97b63 100644 --- a/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py +++ b/AutomatedTesting/Gem/PythonTests/PythonAssetBuilder/mock_asset_builder.py @@ -23,7 +23,7 @@ def create_jobs(request): jobDescriptorList = [] for platformInfo in request.enabledPlatforms: jobDesc = azlmbr.asset.builder.JobDescriptor() - jobDesc.jobKey = jobKeyName + jobDesc.jobKey = f'{jobKeyName}-{platformInfo.identifier}' jobDesc.set_platform_identifier(platformInfo.identifier) jobDescriptorList.append(jobDesc) @@ -38,7 +38,7 @@ def on_create_jobs(args): return create_jobs(request) except: log_exception_traceback() - # returing back a default CreateJobsResponse() records an asset error + # returning back a default CreateJobsResponse() records an asset error return azlmbr.asset.builder.CreateJobsResponse() def process_file(request): @@ -58,6 +58,7 @@ def process_file(request): fileOutput = open(tempFilename, "w") fileOutput.write('{}') fileOutput.close() + print(f'Wrote mock asset file: {tempFilename}') # generate a product asset file entry subId = binascii.crc32(mockFilename.encode()) diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/ap_fixtures/ap_fast_scan_setting_backup_fixture.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/ap_fixtures/ap_fast_scan_setting_backup_fixture.py index 4a096ca3fe..c224289cf4 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/ap_fixtures/ap_fast_scan_setting_backup_fixture.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/ap_fixtures/ap_fast_scan_setting_backup_fixture.py @@ -29,6 +29,9 @@ def ap_fast_scan_setting_backup_fixture(request, workspace) -> PlatformSetting: if workspace.asset_processor_platform == 'mac': pytest.skip("Mac plist file editing not implemented yet") + if workspace.asset_processor_platform == 'linux': + pytest.skip("Linux system settings not implemented yet") + key = fast_scan_key subkey = fast_scan_subkey diff --git a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py index ee8e177bbb..264f690534 100755 --- a/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py +++ b/AutomatedTesting/Gem/PythonTests/assetpipeline/asset_processor_tests/asset_processor_batch_dependency_tests.py @@ -79,7 +79,7 @@ class TestsAssetProcessorBatch_DependenycyTests(object): env = ap_setup_fixture BATCH_LOG_PATH = env["ap_batch_log_file"] asset_processor.create_temp_asset_root() - asset_processor.add_relative_source_asset(os.path.join("Assets", "Engine", "engine_dependencies.xml")) + asset_processor.add_relative_source_asset(os.path.join("Assets", "Engine", "Engine_Dependencies.xml")) asset_processor.add_scan_folder(os.path.join("Assets", "Engine")) asset_processor.add_relative_source_asset(os.path.join("Assets", "Engine", "Libs", "MaterialEffects", "surfacetypes.xml")) diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py index 15ba6690a1..50b89ab138 100644 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py @@ -15,6 +15,7 @@ import sys import importlib import re +import ly_test_tools from ly_test_tools import LAUNCHERS sys.path.append(os.path.dirname(os.path.abspath(__file__))) @@ -25,8 +26,15 @@ import ly_test_tools.environment.process_utils as process_utils import argparse, sys -@pytest.mark.SUITE_main -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) +def get_editor_launcher_platform(): + if ly_test_tools.WINDOWS: + return "windows_editor" + elif ly_test_tools.LINUX: + return "linux_editor" + else: + return None + +@pytest.mark.parametrize("launcher_platform", [get_editor_launcher_platform()]) @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestEditorTest: @@ -69,7 +77,7 @@ class TestEditorTest: from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorTestSuite @pytest.mark.SUITE_main - @pytest.mark.parametrize("launcher_platform", ['windows_editor']) + @pytest.mark.parametrize("launcher_platform", [{get_editor_launcher_platform()}]) @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestAutomation(EditorTestSuite): class test_single(EditorSingleTest): @@ -123,7 +131,7 @@ class TestEditorTest: from ly_test_tools.o3de.editor_test import EditorSingleTest, EditorSharedTest, EditorTestSuite @pytest.mark.SUITE_main - @pytest.mark.parametrize("launcher_platform", ['windows_editor']) + @pytest.mark.parametrize("launcher_platform", [{get_editor_launcher_platform()}]) @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestAutomation(EditorTestSuite): {module_class_code} diff --git a/AutomatedTesting/Gem/PythonTests/smoke/test_CLITool_AzTestRunner_Works.py b/AutomatedTesting/Gem/PythonTests/smoke/test_CLITool_AzTestRunner_Works.py index b269c9e131..df755d5d11 100644 --- a/AutomatedTesting/Gem/PythonTests/smoke/test_CLITool_AzTestRunner_Works.py +++ b/AutomatedTesting/Gem/PythonTests/smoke/test_CLITool_AzTestRunner_Works.py @@ -13,18 +13,26 @@ import os import pytest import subprocess +import ly_test_tools + @pytest.mark.SUITE_smoke class TestCLIToolAzTestRunnerWorks(object): - def test_CLITool_AzTestRunner_Works(self, build_directory): + def test_CLITool_AzTestRunner_ListSelfTests(self, build_directory): file_path = os.path.join(build_directory, "AzTestRunner") help_message = "OKAY Symbol found: AzRunUnitTests" - # Launch AzTestRunner + + if ly_test_tools.WINDOWS: + target_lib = "AzTestRunner.Tests" + else: + target_lib = "libAzTestRunner.Tests" + + # Launch AzTestRunner, load self-tests, print test names output = subprocess.run( - [file_path, "AzTestRunner.Tests", "AzRunUnitTests", "--gtest_list_tests"], capture_output=True, timeout=10 + [file_path, target_lib, "AzRunUnitTests", "--gtest_list_tests"], capture_output=True, timeout=10 ) assert ( len(output.stderr) == 0 and output.returncode == 0 ), f"Error occurred while launching {file_path}: {output.stderr}" # Verify help message - assert help_message in str(output.stdout), f"Help Message: {help_message} is not present" + assert help_message in str(output.stdout), f"Help Message: '{help_message}' unexpectedly not present" diff --git a/Code/Editor/CVarMenu.h b/Code/Editor/CVarMenu.h index efcc8e8caf..5195bd99d7 100644 --- a/Code/Editor/CVarMenu.h +++ b/Code/Editor/CVarMenu.h @@ -16,9 +16,12 @@ #include #include +struct ICVar; + class CVarMenu : public QMenu { + Q_OBJECT public: // CVar that can be toggled on and off struct CVarToggle diff --git a/Code/Editor/ConfigGroup.cpp b/Code/Editor/ConfigGroup.cpp index 74fe6f7b5c..42236e43dd 100644 --- a/Code/Editor/ConfigGroup.cpp +++ b/Code/Editor/ConfigGroup.cpp @@ -19,10 +19,9 @@ namespace Config CConfigGroup::~CConfigGroup() { - for (TConfigVariables::const_iterator it = m_vars.begin(); - it != m_vars.end(); ++it) + for (IConfigVar* var : m_vars) { - delete (*it); + delete var; } } @@ -31,17 +30,15 @@ namespace Config m_vars.push_back(var); } - uint32 CConfigGroup::GetVarCount() + AZ::u32 CConfigGroup::GetVarCount() { - return static_cast(m_vars.size()); + return aznumeric_cast(m_vars.size()); } IConfigVar* CConfigGroup::GetVar(const char* szName) { - for (TConfigVariables::const_iterator it = m_vars.begin(); - it != m_vars.end(); ++it) + for (IConfigVar* var : m_vars) { - IConfigVar* var = (*it); if (0 == _stricmp(szName, var->GetName().c_str())) { return var; @@ -53,20 +50,19 @@ namespace Config const IConfigVar* CConfigGroup::GetVar(const char* szName) const { - for (TConfigVariables::const_iterator it = m_vars.begin(); - it != m_vars.end(); ++it) + for (const IConfigVar* var : m_vars) { - IConfigVar* var = (*it); if (0 == _stricmp(szName, var->GetName().c_str())) { return var; } + } return nullptr; } - IConfigVar* CConfigGroup::GetVar(uint index) + IConfigVar* CConfigGroup::GetVar(AZ::u32 index) { if (index < m_vars.size()) { @@ -76,7 +72,7 @@ namespace Config return nullptr; } - const IConfigVar* CConfigGroup::GetVar(uint index) const + const IConfigVar* CConfigGroup::GetVar(AZ::u32 index) const { if (index < m_vars.size()) { @@ -89,114 +85,110 @@ namespace Config void CConfigGroup::SaveToXML(XmlNodeRef node) { // save only values that don't have default values - for (TConfigVariables::const_iterator it = m_vars.begin(); - it != m_vars.end(); ++it) + for (const IConfigVar* var : m_vars) { - IConfigVar* var = (*it); - if (!var->IsFlagSet(IConfigVar::eFlag_DoNotSave)) + if (var->IsFlagSet(IConfigVar::eFlag_DoNotSave) || var->IsDefault()) { - if (!var->IsDefault()) - { - const char* szName = var->GetName().c_str(); + continue; + } - switch (var->GetType()) - { - case IConfigVar::eType_BOOL: - { - bool currentValue = false; - var->Get(¤tValue); - node->setAttr(szName, currentValue); - break; - } + const char* szName = var->GetName().c_str(); - case IConfigVar::eType_INT: - { - int currentValue = 0; - var->Get(¤tValue); - node->setAttr(szName, currentValue); - break; - } + switch (var->GetType()) + { + case IConfigVar::eType_BOOL: + { + bool currentValue = false; + var->Get(¤tValue); + node->setAttr(szName, currentValue); + break; + } - case IConfigVar::eType_FLOAT: - { - float currentValue = 0; - var->Get(¤tValue); - node->setAttr(szName, currentValue); - break; - } + case IConfigVar::eType_INT: + { + int currentValue = 0; + var->Get(¤tValue); + node->setAttr(szName, currentValue); + break; + } - case IConfigVar::eType_STRING: - { - AZStd::string currentValue; - var->Get(¤tValue); - node->setAttr(szName, currentValue.c_str()); - break; - } - } - } + case IConfigVar::eType_FLOAT: + { + float currentValue = 0; + var->Get(¤tValue); + node->setAttr(szName, currentValue); + break; + } + + case IConfigVar::eType_STRING: + { + AZStd::string currentValue; + var->Get(¤tValue); + node->setAttr(szName, currentValue.c_str()); + break; + } } } } void CConfigGroup::LoadFromXML(XmlNodeRef node) { - // save only values that don't have default values - for (TConfigVariables::const_iterator it = m_vars.begin(); - it != m_vars.end(); ++it) + // load values that are save-able + for (IConfigVar* var : m_vars) { - IConfigVar* var = (*it); - if (!var->IsFlagSet(IConfigVar::eFlag_DoNotSave)) + if (var->IsFlagSet(IConfigVar::eFlag_DoNotSave)) { - const char* szName = var->GetName().c_str(); + continue; + } + const char* szName = var->GetName().c_str(); - switch (var->GetType()) + switch (var->GetType()) + { + case IConfigVar::eType_BOOL: + { + bool currentValue = false; + var->GetDefault(¤tValue); + if (node->getAttr(szName, currentValue)) { - case IConfigVar::eType_BOOL: - { - bool currentValue = false; - var->GetDefault(¤tValue); - if (node->getAttr(szName, currentValue)) - { - var->Set(¤tValue); - } - break; + var->Set(¤tValue); } + break; + } - case IConfigVar::eType_INT: + case IConfigVar::eType_INT: + { + int currentValue = 0; + var->GetDefault(¤tValue); + if (node->getAttr(szName, currentValue)) { - int currentValue = 0; - var->GetDefault(¤tValue); - if (node->getAttr(szName, currentValue)) - { - var->Set(¤tValue); - } - break; + var->Set(¤tValue); } + break; + } - case IConfigVar::eType_FLOAT: + case IConfigVar::eType_FLOAT: + { + float currentValue = 0; + var->GetDefault(¤tValue); + if (node->getAttr(szName, currentValue)) { - float currentValue = 0; - var->GetDefault(¤tValue); - if (node->getAttr(szName, currentValue)) - { - var->Set(¤tValue); - } - break; + var->Set(¤tValue); } + break; + } - case IConfigVar::eType_STRING: + case IConfigVar::eType_STRING: + { + AZStd::string currentValue; + var->GetDefault(¤tValue); + QString readValue(currentValue.c_str()); + if (node->getAttr(szName, readValue)) { - AZStd::string currentValue; - var->GetDefault(¤tValue); - QString readValue(currentValue.c_str()); - if (node->getAttr(szName, readValue)) - { - currentValue = readValue.toUtf8().data(); - var->Set(¤tValue); - } - break; - } + currentValue = readValue.toUtf8().data(); + var->Set(¤tValue); } + break; + } } } } diff --git a/Code/Editor/ConfigGroup.h b/Code/Editor/ConfigGroup.h index 769a29ba8a..004725e32c 100644 --- a/Code/Editor/ConfigGroup.h +++ b/Code/Editor/ConfigGroup.h @@ -8,8 +8,12 @@ #pragma once -#ifndef CRYINCLUDE_EDITOR_CONFIGGROUP_H -#define CRYINCLUDE_EDITOR_CONFIGGROUP_H +#include +#include +#include + +struct ICVar; +class XmlNodeRef; namespace Config { @@ -32,7 +36,7 @@ namespace Config eFlag_DoNotSave = 1 << 2, }; - IConfigVar(const char* szName, const char* szDescription, EType varType, uint8 flags) + IConfigVar(const char* szName, const char* szDescription, EType varType, AZ::u8 flags) : m_name(szName) , m_description(szDescription) , m_type(varType) @@ -42,22 +46,22 @@ namespace Config virtual ~IConfigVar() = default; - ILINE EType GetType() const + AZ_FORCE_INLINE EType GetType() const { return m_type; } - ILINE const AZStd::string& GetName() const + AZ_FORCE_INLINE const AZStd::string& GetName() const { return m_name; } - ILINE const AZStd::string& GetDescription() const + AZ_FORCE_INLINE const AZStd::string& GetDescription() const { return m_description; } - ILINE bool IsFlagSet(EFlags flag) const + AZ_FORCE_INLINE bool IsFlagSet(EFlags flag) const { return 0 != (m_flags & flag); } @@ -68,73 +72,28 @@ namespace Config virtual void GetDefault(void* outPtr) const = 0; virtual void Reset() = 0; - static EType TranslateType(const bool&) { return eType_BOOL; } - static EType TranslateType(const int&) { return eType_INT; } - static EType TranslateType(const float&) { return eType_FLOAT; } - static EType TranslateType(const AZStd::string&) { return eType_STRING; } + static constexpr EType TranslateType(const bool&) { return eType_BOOL; } + static constexpr EType TranslateType(const int&) { return eType_INT; } + static constexpr EType TranslateType(const float&) { return eType_FLOAT; } + static constexpr EType TranslateType(const AZStd::string&) { return eType_STRING; } protected: EType m_type; - uint8 m_flags; + AZ::u8 m_flags; AZStd::string m_name; AZStd::string m_description; void* m_ptr; ICVar* m_pCVar; }; - // Typed wrapper for config variable - template - class TConfigVar - : public IConfigVar - { - private: - T m_default; - - public: - TConfigVar(const char* szName, const char* szDescription, uint8 flags, T& ptr, const T& defaultValue) - : IConfigVar(szName, szDescription, IConfigVar::TranslateType(ptr), flags) - , m_default(defaultValue) - { - m_ptr = &ptr; - - // reset to default value on initializations - ptr = defaultValue; - } - - virtual void Get(void* outPtr) const - { - *reinterpret_cast(outPtr) = *reinterpret_cast(m_ptr); - } - - virtual void Set(const void* ptr) - { - *reinterpret_cast(m_ptr) = *reinterpret_cast(ptr); - } - - virtual void Reset() - { - *reinterpret_cast(m_ptr) = m_default; - } - - virtual void GetDefault(void* outPtr) const - { - *reinterpret_cast(outPtr) = m_default; - } - - virtual bool IsDefault() const - { - return *reinterpret_cast(m_ptr) == m_default; - } - }; - // Group of configuration variables with optional mapping to CVars class CConfigGroup { private: - typedef std::vector TConfigVariables; + using TConfigVariables = AZStd::vector ; TConfigVariables m_vars; - typedef std::vector TConsoleVariables; + using TConsoleVariables = AZStd::vector; TConsoleVariables m_consoleVars; public: @@ -142,20 +101,13 @@ namespace Config virtual ~CConfigGroup(); void AddVar(IConfigVar* var); - uint32 GetVarCount(); + AZ::u32 GetVarCount(); IConfigVar* GetVar(const char* szName); - IConfigVar* GetVar(uint index); + IConfigVar* GetVar(AZ::u32 index); const IConfigVar* GetVar(const char* szName) const; - const IConfigVar* GetVar(uint index) const; + const IConfigVar* GetVar(AZ::u32 index) const; void SaveToXML(XmlNodeRef node); void LoadFromXML(XmlNodeRef node); - - template - void AddVar(const char* szName, const char* szDescription, T& var, const T& defaultValue, uint8 flags = 0) - { - AddVar(new TConfigVar(szName, szDescription, flags, var, defaultValue)); - } }; }; -#endif // CRYINCLUDE_EDITOR_CONFIGGROUP_H diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h index 5ec24b679d..849e44cedd 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyMiscCtrl.h @@ -6,8 +6,6 @@ * */ -#ifndef CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H -#define CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H #pragma once #if !defined(Q_MOC_RUN) @@ -53,6 +51,7 @@ private: class UserPopupWidgetHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarUser, UserPropertyEditor> { + Q_OBJECT public: AZ_CLASS_ALLOCATOR(UserPopupWidgetHandler, AZ::SystemAllocator, 0); bool IsDefaultHandler() const override { return false; } @@ -67,6 +66,7 @@ public: class FloatCurveHandler : public QObject, public AzToolsFramework::PropertyHandler < CReflectedVarSpline, CSplineCtrl> { + Q_OBJECT public: AZ_CLASS_ALLOCATOR(FloatCurveHandler, AZ::SystemAllocator, 0); bool IsDefaultHandler() const override { return false; } @@ -80,5 +80,3 @@ public: void OnSplineChange(CSplineCtrl*); }; - -#endif // CRYINCLUDE_EDITOR_UTILS_PROPERTYMISCCTRL_H diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.cpp b/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.cpp index c5ccc599d6..d26e978ae8 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.cpp @@ -58,17 +58,9 @@ private: void OnClicked() override { QString tempValue(""); - QString ext(""); - if (m_path.isEmpty() == false) + if (!m_path.isEmpty() && !Path::GetExt(m_path).isEmpty()) { - if (Path::GetExt(m_path) == "") - { - tempValue = ""; - } - else - { - tempValue = m_path; - } + tempValue = m_path; } AssetSelectionModel selection; diff --git a/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.h b/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.h index fa30eba034..087ee9f1db 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.h +++ b/Code/Editor/Controls/ReflectedPropertyControl/PropertyResourceCtrl.h @@ -99,6 +99,7 @@ class FileResourceSelectorWidgetHandler : QObject , public AzToolsFramework::PropertyHandler < CReflectedVarResource, FileResourceSelectorWidget > { + Q_OBJECT public: AZ_CLASS_ALLOCATOR(FileResourceSelectorWidgetHandler, AZ::SystemAllocator, 0); diff --git a/Code/Editor/Controls/ReflectedPropertyControl/ReflectedVarWrapper.cpp b/Code/Editor/Controls/ReflectedPropertyControl/ReflectedVarWrapper.cpp index b3f1b35461..2320938f08 100644 --- a/Code/Editor/Controls/ReflectedPropertyControl/ReflectedVarWrapper.cpp +++ b/Code/Editor/Controls/ReflectedPropertyControl/ReflectedVarWrapper.cpp @@ -446,41 +446,54 @@ void ReflectedVarUserAdapter::SetVariable(IVariable *pVariable) m_reflectedVar.reset(new CReflectedVarUser( pVariable->GetHumanName().toUtf8().data())); } -void ReflectedVarUserAdapter::SyncReflectedVarToIVar(IVariable *pVariable) +void ReflectedVarUserAdapter::SyncReflectedVarToIVar(IVariable* pVariable) { QString value; pVariable->Get(value); m_reflectedVar->m_value = value.toUtf8().data(); - //extract the list of custom items from the IVariable user data - IVariable::IGetCustomItems* pGetCustomItems = static_cast (pVariable->GetUserData().value()); - if (pGetCustomItems != nullptr) - { - std::vector items; - QString dlgTitle; - // call the user supplied callback to fill-in items and get dialog title - bool bShowIt = pGetCustomItems->GetItems(pVariable, items, dlgTitle); - if (bShowIt) // if func didn't veto, show the dialog - { - m_reflectedVar->m_enableEdit = true; - m_reflectedVar->m_useTree = pGetCustomItems->UseTree(); - m_reflectedVar->m_treeSeparator = pGetCustomItems->GetTreeSeparator(); - m_reflectedVar->m_dialogTitle = dlgTitle.toUtf8().data(); - m_reflectedVar->m_itemNames.resize(items.size()); - m_reflectedVar->m_itemDescriptions.resize(items.size()); - - QByteArray ba; - int i = -1; - std::generate(m_reflectedVar->m_itemNames.begin(), m_reflectedVar->m_itemNames.end(), [&items, &i, &ba]() { ++i; ba = items[i].name.toUtf8(); return ba.data(); }); - i = -1; - std::generate(m_reflectedVar->m_itemDescriptions.begin(), m_reflectedVar->m_itemDescriptions.end(), [&items, &i, &ba]() { ++i; ba = items[i].desc.toUtf8(); return ba.data(); }); - - } - } - else + // extract the list of custom items from the IVariable user data + IVariable::IGetCustomItems* pGetCustomItems = static_cast(pVariable->GetUserData().value()); + if (pGetCustomItems == nullptr) { m_reflectedVar->m_enableEdit = false; + return; } + + std::vector items; + QString dlgTitle; + // call the user supplied callback to fill-in items and get dialog title + bool bShowIt = pGetCustomItems->GetItems(pVariable, items, dlgTitle); + if (!bShowIt) // if func vetoed it, don't show the dialog + { + return; + } + m_reflectedVar->m_enableEdit = true; + m_reflectedVar->m_useTree = pGetCustomItems->UseTree(); + m_reflectedVar->m_treeSeparator = pGetCustomItems->GetTreeSeparator(); + m_reflectedVar->m_dialogTitle = dlgTitle.toUtf8().data(); + m_reflectedVar->m_itemNames.resize(items.size()); + m_reflectedVar->m_itemDescriptions.resize(items.size()); + + QByteArray ba; + int i = -1; + AZStd::generate( + m_reflectedVar->m_itemNames.begin(), m_reflectedVar->m_itemNames.end(), + [&items, &i, &ba]() + { + ++i; + ba = items[i].name.toUtf8(); + return ba.data(); + }); + i = -1; + AZStd::generate( + m_reflectedVar->m_itemDescriptions.begin(), m_reflectedVar->m_itemDescriptions.end(), + [&items, &i, &ba]() + { + ++i; + ba = items[i].desc.toUtf8(); + return ba.data(); + }); } void ReflectedVarUserAdapter::SyncIVarToReflectedVar(IVariable *pVariable) diff --git a/Code/Editor/Controls/TimelineCtrl.cpp b/Code/Editor/Controls/TimelineCtrl.cpp index aa30c326ac..3890b46b2b 100644 --- a/Code/Editor/Controls/TimelineCtrl.cpp +++ b/Code/Editor/Controls/TimelineCtrl.cpp @@ -126,7 +126,6 @@ void TimelineWidget::DrawTicks(QPainter* painter) const QPen pOldPen = painter->pen(); const QPen ltgray(QColor(110, 110, 110)); - const QPen black(palette().color(QPalette::Normal, QPalette::Text)); const QPen redpen(QColor(255, 0, 255)); // Draw time ticks every tick step seconds. @@ -598,7 +597,6 @@ void TimelineWidget::DrawSecondTicks(QPainter* painter) { const QPen ltgray(QColor(110, 110, 110)); const QPen black(palette().color(QPalette::Normal, QPalette::Text)); - const QPen redpen(QColor(255, 0, 255)); for (int gx = m_grid.firstGridLine.x(); gx < m_grid.firstGridLine.x() + m_grid.numGridLines.x() + 1; gx++) { diff --git a/Code/Editor/CryEdit.cpp b/Code/Editor/CryEdit.cpp index e4138da932..0277abe60d 100644 --- a/Code/Editor/CryEdit.cpp +++ b/Code/Editor/CryEdit.cpp @@ -4137,7 +4137,15 @@ extern "C" int AZ_DLL_EXPORT CryEditMain(int argc, char* argv[]) AzQtComponents::Utilities::HandleDpiAwareness(AzQtComponents::Utilities::SystemDpiAware); Editor::EditorQtApplication* app = Editor::EditorQtApplication::newInstance(argc, argv); - if (app->arguments().contains("-autotest_mode")) + QStringList qArgs = app->arguments(); + const bool is_automated_test = AZStd::any_of(qArgs.begin(), qArgs.end(), + [](const QString& elem) + { + return elem.endsWith("autotest_mode") || elem.endsWith("runpythontest"); + } + ); + + if (is_automated_test) { // Nullroute all stdout to null for automated tests, this way we make sure // that the test result output is not polluted with unrelated output data. diff --git a/Code/Editor/CryEdit.h b/Code/Editor/CryEdit.h index f68cfdc33d..53ee8f1905 100644 --- a/Code/Editor/CryEdit.h +++ b/Code/Editor/CryEdit.h @@ -431,6 +431,7 @@ public: class CCrySingleDocTemplate : public QObject { + Q_OBJECT private: explicit CCrySingleDocTemplate(const QMetaObject* pDocClass) : QObject() diff --git a/Code/Editor/CustomResolutionDlg.h b/Code/Editor/CustomResolutionDlg.h index 5dd9acaae8..e1b8035c65 100644 --- a/Code/Editor/CustomResolutionDlg.h +++ b/Code/Editor/CustomResolutionDlg.h @@ -12,8 +12,6 @@ // Notice : Refer to ViewportTitleDlg.cpp for a use case. -#ifndef CRYINCLUDE_EDITOR_CUSTOMRESOLUTIONDLG_H -#define CRYINCLUDE_EDITOR_CUSTOMRESOLUTIONDLG_H #pragma once #if !defined(Q_MOC_RUN) @@ -28,6 +26,7 @@ namespace Ui class CCustomResolutionDlg : public QDialog { + Q_OBJECT public: CCustomResolutionDlg(int w, int h, QWidget* pParent = nullptr); ~CCustomResolutionDlg(); @@ -42,5 +41,3 @@ protected: QScopedPointer m_ui; }; - -#endif // CRYINCLUDE_EDITOR_CUSTOMRESOLUTIONDLG_H diff --git a/Code/Editor/CustomizeKeyboardDialog.cpp b/Code/Editor/CustomizeKeyboardDialog.cpp index ce09f8d878..280d6c5323 100644 --- a/Code/Editor/CustomizeKeyboardDialog.cpp +++ b/Code/Editor/CustomizeKeyboardDialog.cpp @@ -211,9 +211,9 @@ public: void Reset(QAction& action) { - emit beginResetModel(); + beginResetModel(); m_action = &action; - emit endResetModel(); + endResetModel(); } private: @@ -266,7 +266,7 @@ QStringList CustomizeKeyboardDialog::BuildModels(QWidget* parent) categories.append(category); QMenu* menu = menuAction->menu(); - m_menuActions[category] = GetAllActionsForMenu(menu, QStringLiteral("")); + m_menuActions[category] = GetAllActionsForMenu(menu, QString()); } return categories; diff --git a/Code/Editor/ErrorDialog.cpp b/Code/Editor/ErrorDialog.cpp index 0a45b2bf89..e43df75947 100644 --- a/Code/Editor/ErrorDialog.cpp +++ b/Code/Editor/ErrorDialog.cpp @@ -25,9 +25,9 @@ namespace SandboxEditor connect(m_ui->okButton, &QPushButton::clicked, this, &ErrorDialog::OnOK); connect( m_ui->messages, - SIGNAL(itemSelectionChanged()), + &QTreeWidget::itemSelectionChanged, this, - SLOT(MessageSelectionChanged())); + &ErrorDialog::MessageSelectionChanged); } ErrorDialog::~ErrorDialog() diff --git a/Code/Editor/KeyboardCustomizationSettings.cpp b/Code/Editor/KeyboardCustomizationSettings.cpp index c4f9133f66..81d9375850 100644 --- a/Code/Editor/KeyboardCustomizationSettings.cpp +++ b/Code/Editor/KeyboardCustomizationSettings.cpp @@ -240,7 +240,7 @@ QJsonObject KeyboardCustomizationSettings::ExportGroup() void KeyboardCustomizationSettings::ImportFromFile(QWidget* parent) { - QString fileName = QFileDialog::getOpenFileName(parent, QObject::tr("Export Keyboard Shortcuts"), QStringLiteral(""), QObject::tr("Keyboard Settings (*.keys)")); + QString fileName = QFileDialog::getOpenFileName(parent, QObject::tr("Export Keyboard Shortcuts"), QString(), QObject::tr("Keyboard Settings (*.keys)")); if (fileName.isEmpty()) { return; diff --git a/Code/Editor/MainStatusBar.cpp b/Code/Editor/MainStatusBar.cpp index f955035bd8..acc7f664df 100644 --- a/Code/Editor/MainStatusBar.cpp +++ b/Code/Editor/MainStatusBar.cpp @@ -419,7 +419,7 @@ void MemoryStatusItem::updateStatus() GeneralStatusItem::GeneralStatusItem(QString name, MainStatusBar* parent) : StatusBarItem(name, parent) { - connect(parent, SIGNAL(messageChanged(QString)), this, SLOT(update())); + connect(parent, &MainStatusBar::messageChanged, this, [this](const QString&) { update(); }); } QString GeneralStatusItem::CurrentText() const diff --git a/Code/Editor/NewLevelDialog.cpp b/Code/Editor/NewLevelDialog.cpp index 29445cef2b..c773acdb6f 100644 --- a/Code/Editor/NewLevelDialog.cpp +++ b/Code/Editor/NewLevelDialog.cpp @@ -100,9 +100,10 @@ CNewLevelDialog::CNewLevelDialog(QWidget* pParent /*=nullptr*/) m_level = ""; // First of all, keyboard focus is related to widget tab order, and the default tab order is based on the order in which // widgets are constructed. Therefore, creating more widgets changes the keyboard focus. That is why setFocus() is called last. - // Secondly, using singleShot() allows setFocus() slot of the QLineEdit instance to be invoked right after the event system - // is ready to do so. Therefore, it is better to use singleShot() than directly call setFocus(). - QTimer::singleShot(0, ui->LEVEL, SLOT(OnStartup())); + // in OnStartup() + // Secondly, using singleShot() allows OnStartup() slot of the QLineEdit instance to be invoked right after the event system + // is ready to do so. Therefore, it is better to use singleShot() than directly call OnStartup(). + QTimer::singleShot(0, this, &CNewLevelDialog::OnStartup); ReloadLevelFolder(); } diff --git a/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm b/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm index a7f59b7ac8..c664d13c79 100644 --- a/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm +++ b/Code/Editor/Platform/Mac/Editor/Core/QtEditorApplication_mac.mm @@ -9,7 +9,7 @@ #import #include "EditorDefs.h" -#include "QtEditorApplication.h" +#include "QtEditorApplication_mac.h" // AzFramework #include diff --git a/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp b/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp index 41e950a058..5b849dcbe7 100644 --- a/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp +++ b/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.cpp @@ -1788,6 +1788,11 @@ AZStd::string SandboxIntegrationManager::GetComponentEditorIcon(const AZ::Uuid& return iconPath; } +AZStd::string SandboxIntegrationManager::GetComponentTypeEditorIcon(const AZ::Uuid& componentType) +{ + return GetComponentEditorIcon(componentType, nullptr); +} + AZStd::string SandboxIntegrationManager::GetComponentIconPath(const AZ::Uuid& componentType, AZ::Crc32 componentIconAttrib, AZ::Component* component) { diff --git a/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h b/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h index 40962409a3..9afa944438 100644 --- a/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h +++ b/Code/Editor/Plugins/ComponentEntityEditorPlugin/SandboxIntegration.h @@ -233,6 +233,7 @@ private: } AZStd::string GetComponentEditorIcon(const AZ::Uuid& componentType, AZ::Component* component) override; + AZStd::string GetComponentTypeEditorIcon(const AZ::Uuid& componentType) override; AZStd::string GetComponentIconPath(const AZ::Uuid& componentType, AZ::Crc32 componentIconAttrib, AZ::Component* component) override; ////////////////////////////////////////////////////////////////////////// diff --git a/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.cpp b/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.cpp index d0091f968e..df8d6db4a8 100644 --- a/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.cpp +++ b/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.cpp @@ -197,48 +197,46 @@ AssetCatalogModel::~AssetCatalogModel() AzFramework::AssetCatalogEventBus::Handler::BusDisconnect(); } -AZ::Data::AssetType AssetCatalogModel::GetAssetType(QString filename) const +AZ::Data::AssetType AssetCatalogModel::GetAssetType(const QString &filename) const { - AZ::Data::AssetType returnType = AZ::Uuid::CreateNull(); // Compare file extensions with the map created from the asset database. int dotIndex = filename.lastIndexOf('.'); - if (dotIndex >= 0) + if (dotIndex < 0) { - QString extension = filename.mid(dotIndex); - for (auto pair : m_extensionToAssetType) - { - QString qExtensions = pair.first.c_str(); - if (qExtensions.indexOf(extension) >= 0) - { - if (pair.second.size() > 1) - { - // There are multiple types with this extension. Check each handler to see if they can handle this data type. - AZStd::string azFilename = filename.toStdString().c_str(); - EBUS_EVENT(AzFramework::ApplicationRequests::Bus, MakePathAssetRootRelative, azFilename); - AZ::Data::AssetId assetId; - EBUS_EVENT_RESULT(assetId, AZ::Data::AssetCatalogRequestBus, GetAssetIdByPath, azFilename.c_str(), AZ::Data::s_invalidAssetType, false); + return AZ::Uuid::CreateNull(); + } - for (AZ::Uuid type : pair.second) - { - const AZ::Data::AssetHandler* handler = AZ::Data::AssetManager::Instance().GetHandler(type); - if (handler && handler->CanHandleAsset(assetId)) - { - returnType = type; - break; - } - } - } - else - { - returnType = pair.second[0]; - break; - } + QStringRef extension = filename.midRef(dotIndex); + for (const auto& pair : m_extensionToAssetType) + { + QString qExtensions = pair.first.c_str(); + if (qExtensions.indexOf(extension) < 0 || pair.second.empty()) + { + continue; + } + if (pair.second.size() == 1) + { + return pair.second[0]; + } + + // There are multiple types with this extension. Search for a handler that can handle this data type. + AZStd::string azFilename = filename.toStdString().c_str(); + EBUS_EVENT(AzFramework::ApplicationRequests::Bus, MakePathAssetRootRelative, azFilename); + AZ::Data::AssetId assetId; + EBUS_EVENT_RESULT(assetId, AZ::Data::AssetCatalogRequestBus, GetAssetIdByPath, azFilename.c_str(), AZ::Data::s_invalidAssetType, false); + + for (const AZ::Uuid& type : pair.second) + { + const AZ::Data::AssetHandler* handler = AZ::Data::AssetManager::Instance().GetHandler(type); + if (handler && handler->CanHandleAsset(assetId)) + { + return type; } } } - return returnType; + return AZ::Uuid::CreateNull(); } QStandardItem* AssetCatalogModel::GetPath(QString& path, bool createIfNeeded, QStandardItem* parent) @@ -419,7 +417,7 @@ AssetCatalogEntry* AssetCatalogModel::AddAsset(QString assetPath, AZ::Data::Asse // icons' memory being reclaimed and crashing the Editor. QSize size = fileIcon.actualSize(QSize(16, 16)); QIcon deepCopy = fileIcon.pixmap(size).copy(0, 0, size.width(), size.height()); - + if (!fileIcon.isNull()) { m_assetTypeToIcon[assetType] = deepCopy; diff --git a/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.h b/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.h index c9143bb259..1dffa81d67 100644 --- a/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.h +++ b/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/AssetCatalogModel.h @@ -110,7 +110,7 @@ protected: void SetFilterRegExp(const AZStd::string& filterType, const QRegExp& regExp); void ClearFilterRegExp(const AZStd::string& filterType = AZStd::string()); - AZ::Data::AssetType GetAssetType(QString filename) const; + AZ::Data::AssetType GetAssetType(const QString &filename) const; QStandardItem* GetPath(QString& path, bool createIfNeeded, QStandardItem* parent = nullptr); void ApplyFilter(QStandardItem* parent); diff --git a/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/ComponentPalette/ComponentDataModel.cpp b/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/ComponentPalette/ComponentDataModel.cpp index cd0fba350f..dcae6abfeb 100644 --- a/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/ComponentPalette/ComponentDataModel.cpp +++ b/Code/Editor/Plugins/ComponentEntityEditorPlugin/UI/ComponentPalette/ComponentDataModel.cpp @@ -139,7 +139,7 @@ ComponentDataModel::ComponentDataModel(QObject* parent) if (element.m_elementId == AZ::Edit::ClassElements::EditorData) { AZStd::string iconPath; - EBUS_EVENT_RESULT(iconPath, AzToolsFramework::EditorRequests::Bus, GetComponentEditorIcon, classData->m_typeId, nullptr); + AzToolsFramework::EditorRequestBus::BroadcastResult(iconPath, &AzToolsFramework::EditorRequests::GetComponentTypeEditorIcon, classData->m_typeId); if (!iconPath.empty()) { m_componentIcons[classData->m_typeId] = QIcon(iconPath.c_str()); diff --git a/Code/Editor/TrackView/TrackViewNodes.cpp b/Code/Editor/TrackView/TrackViewNodes.cpp index 2a2d584e46..db65abf9d2 100644 --- a/Code/Editor/TrackView/TrackViewNodes.cpp +++ b/Code/Editor/TrackView/TrackViewNodes.cpp @@ -408,7 +408,7 @@ CTrackViewNodesCtrl::CTrackViewNodesCtrl(QWidget* hParentWnd, CTrackViewDialog* serializeContext->EnumerateDerived([this](const AZ::SerializeContext::ClassData* classData, const AZ::Uuid&) -> bool { AZStd::string iconPath; - EBUS_EVENT_RESULT(iconPath, AzToolsFramework::EditorRequests::Bus, GetComponentEditorIcon, classData->m_typeId, nullptr); + AzToolsFramework::EditorRequestBus::BroadcastResult(iconPath, &AzToolsFramework::EditorRequests::GetComponentTypeEditorIcon, classData->m_typeId); if (!iconPath.empty()) { m_componentTypeToIconMap[classData->m_typeId] = QIcon(iconPath.c_str()); diff --git a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp index dabf02e260..df8db79db0 100644 --- a/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp +++ b/Code/Framework/AzCore/AzCore/Component/ComponentApplication.cpp @@ -215,11 +215,6 @@ namespace AZ m_oldProjectPath = newProjectPath; // Merge the project.json file into settings registry under ProjectSettingsRootKey path. - AZ::IO::FixedMaxPath projectMetadataFile{ AZ::SettingsRegistryMergeUtils::FindEngineRoot(m_registry) / newProjectPath }; - projectMetadataFile /= "project.json"; - m_registry.MergeSettingsFile(projectMetadataFile.Native(), - AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey); - // Update all the runtime file paths based on the new "project_path" value. AZ::SettingsRegistryMergeUtils::MergeSettingsToRegistry_AddRuntimeFilePaths(m_registry); } diff --git a/Code/Framework/AzCore/AzCore/Component/TransformBus.h b/Code/Framework/AzCore/AzCore/Component/TransformBus.h index 1af77da51b..f8e30147da 100644 --- a/Code/Framework/AzCore/AzCore/Component/TransformBus.h +++ b/Code/Framework/AzCore/AzCore/Component/TransformBus.h @@ -168,6 +168,11 @@ namespace AZ //! Rotation modifiers //! @{ + //! Set the world rotation matrix using the composition of rotations around + //! the principle axes in the order of z-axis first and y-axis and then x-axis. + //! @param eulerRadianAngles A Vector3 denoting radian angles of the rotations around each principle axis. + virtual void SetWorldRotation([[maybe_unused]] const AZ::Vector3& eulerAnglesRadian) {} + //! Sets the entity's rotation in the world in quaternion notation. //! The origin of the axes is the entity's position in world space. //! @param quaternion A quaternion that represents the rotation to use for the entity. diff --git a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp new file mode 100644 index 0000000000..5d66bb6ac5 --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.cpp @@ -0,0 +1,239 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include + +namespace AZ::DOM +{ + const char* VisitorError::CodeToString(VisitorErrorCode code) + { + switch (code) + { + case VisitorErrorCode::UnsupportedOperation: + return "operation not supported"; + case VisitorErrorCode::InvalidData: + return "invalid data specified"; + case VisitorErrorCode::InternalError: + return "internal error"; + default: + return "unknown error"; + } + } + + VisitorError::VisitorError(VisitorErrorCode code) + : m_code(code) + { + } + + VisitorError::VisitorError(VisitorErrorCode code, AZStd::string additionalInfo) + : m_code(code) + , m_additionalInfo(AZStd::move(additionalInfo)) + { + } + + VisitorErrorCode VisitorError::GetCode() const + { + return m_code; + } + + const AZStd::string& VisitorError::GetAdditionalInfo() const + { + return m_additionalInfo; + } + + AZStd::string VisitorError::FormatVisitorErrorMessage() const + { + if (m_additionalInfo.empty()) + { + return AZStd::string::format("VisitorError: %s.", CodeToString(m_code)); + } + return AZStd::string::format("VisitorError: %s. %s.", CodeToString(m_code), m_additionalInfo.c_str()); + } + + Visitor::Result Visitor::VisitorFailure(VisitorErrorCode code) + { + return AZ::Failure(VisitorError(code)); + } + + Visitor::Result Visitor::VisitorFailure(VisitorErrorCode code, AZStd::string additionalInfo) + { + return AZ::Failure(VisitorError(code, AZStd::move(additionalInfo))); + } + + Visitor::Result Visitor::VisitorFailure(VisitorError error) + { + return AZ::Failure(error); + } + + Visitor::Result Visitor::VisitorSuccess() + { + return AZ::Success(); + } + + Visitor::Result Visitor::Null() + { + return VisitorSuccess(); + } + + Visitor::Result Visitor::Bool([[maybe_unused]] bool value) + { + return VisitorSuccess(); + } + + Visitor::Result Visitor::Int64([[maybe_unused]] AZ::s64 value) + { + return VisitorSuccess(); + } + + Visitor::Result Visitor::Uint64([[maybe_unused]] AZ::u64 value) + { + return VisitorSuccess(); + } + + Visitor::Result Visitor::Double([[maybe_unused]] double value) + { + return VisitorSuccess(); + } + + Visitor::Result Visitor::String([[maybe_unused]] AZStd::string_view value, [[maybe_unused]] Lifetime lifetime) + { + return VisitorSuccess(); + } + + Visitor::Result Visitor::OpaqueValue([[maybe_unused]] const OpaqueType& value, [[maybe_unused]] Lifetime lifetime) + { + if (!SupportsOpaqueValues()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Opaque values are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::RawValue([[maybe_unused]] AZStd::string_view value, [[maybe_unused]] Lifetime lifetime) + { + if (!SupportsRawValues()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Raw values are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::StartObject() + { + if (!SupportsObjects()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Objects are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::EndObject([[maybe_unused]] AZ::u64 attributeCount) + { + if (!SupportsObjects()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Objects are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::Key([[maybe_unused]] AZ::Name key) + { + if (!SupportsObjects() && !SupportsNodes()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Keys are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::RawKey(AZStd::string_view key, [[maybe_unused]] Lifetime lifetime) + { + if (!SupportsRawKeys()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Raw keys are not supported by this visitor"); + } + return Key(AZ::Name(key)); + } + + Visitor::Result Visitor::StartArray() + { + if (!SupportsArrays()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Arrays are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::EndArray([[maybe_unused]] AZ::u64 elementCount) + { + if (!SupportsArrays()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Arrays are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::StartNode([[maybe_unused]] AZ::Name name) + { + if (!SupportsNodes()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Nodes are not supported by this visitor"); + } + return VisitorSuccess(); + } + + Visitor::Result Visitor::RawStartNode(AZStd::string_view name, [[maybe_unused]] Lifetime lifetime) + { + return StartNode(AZ::Name(name)); + } + + Visitor::Result Visitor::EndNode([[maybe_unused]] AZ::u64 attributeCount, [[maybe_unused]] AZ::u64 elementCount) + { + if (!SupportsNodes()) + { + return VisitorFailure(VisitorErrorCode::UnsupportedOperation, "Nodes are not supported by this visitor"); + } + return VisitorSuccess(); + } + + VisitorFlags Visitor::GetVisitorFlags() const + { + // By default support raw keys (promoting them to AZ::Name) and support Array / Object / Node + // We leave Opaque type support and Raw Values to more specialized, implementation-specific cases + return VisitorFlags::SupportsRawKeys | VisitorFlags::SupportsArrays | VisitorFlags::SupportsObjects | VisitorFlags::SupportsNodes; + } + + bool Visitor::SupportsRawValues() const + { + return (GetVisitorFlags() & VisitorFlags::SupportsRawValues) != VisitorFlags::Null; + } + + bool Visitor::SupportsRawKeys() const + { + return (GetVisitorFlags() & VisitorFlags::SupportsRawKeys) != VisitorFlags::Null; + } + + bool Visitor::SupportsObjects() const + { + return (GetVisitorFlags() & VisitorFlags::SupportsObjects) != VisitorFlags::Null; + } + + bool Visitor::SupportsArrays() const + { + return (GetVisitorFlags() & VisitorFlags::SupportsArrays) != VisitorFlags::Null; + } + + bool Visitor::SupportsNodes() const + { + return (GetVisitorFlags() & VisitorFlags::SupportsNodes) != VisitorFlags::Null; + } + + bool Visitor::SupportsOpaqueValues() const + { + return (GetVisitorFlags() & VisitorFlags::SupportsOpaqueValues) != VisitorFlags::Null; + } +} // namespace AZ::DOM diff --git a/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h new file mode 100644 index 0000000000..584cfce4ed --- /dev/null +++ b/Code/Framework/AzCore/AzCore/DOM/DomVisitor.h @@ -0,0 +1,237 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include +#include +#include +#include + +namespace AZ::DOM +{ + // + // Lifetime enum + // + //! Specifies the period in which a reference value will still be alive and safe to read. + enum class Lifetime + { + //! Specifies that the value is safe to read and will remain so indefinitely. + //! This implies that the value will not be mutated for the duration of this storage. + Persistent, + //! Specifies that the value may change or be deallocated, and must be copied to be safely stored. + Temporary, + }; + + // + // VisitorErrorCode enum + // + //! Error code specifying the reason a Visitor operation failed. + enum class VisitorErrorCode + { + //! Set when a Visitor doesn't have an implementation for a given attribute type. + //! A pure-JSON serializer might reject a Node attribute, for example, and serialization visitors + //! can forbid non-serializable Opaque types. + UnsupportedOperation, + //! Set when a Visitor has received malformed or invalid data. + //! Potential sources include mismatching Begin/End call pairs or invalid attribute or element counts + //! being sent to End methods. + InvalidData, + //! The Visitor failed for some other reason not caused by invalid input. + //! If returning a custom error with this code, it's preferrable to also provide supplemental info + //! in the form of an explanatory string. + InternalError + }; + + // + // VisitorError class + // + //! Details of the reason for failure within a VisitorInterface operation. + class VisitorError final + { + public: + explicit VisitorError(VisitorErrorCode code); + VisitorError(VisitorErrorCode code, AZStd::string additionalInfo); + + //! Gets the error code associated with this error. + VisitorErrorCode GetCode() const; + //! Gets a supplemental error info string from the error. + //! Returns an empty string if no additional information was provided to the error. + const AZStd::string& GetAdditionalInfo() const; + //! Provides a formatted, human-readable error description that can be used for logging purposes. + AZStd::string FormatVisitorErrorMessage() const; + + //! Helper method, translates a VisitorErrorCode to a human readable string. + static const char* CodeToString(VisitorErrorCode code); + + private: + VisitorErrorCode m_code; + AZStd::string m_additionalInfo; + }; + + //! A type alias for opaque DOM types that aren't meant to be serializable. + //! /see VisitorInterface::OpaqueValue + using OpaqueType = AZStd::any; + + // + // VisitorFlags enum + // + //! Flags representning capabilities of a \ref Visitor. + enum class VisitorFlags : AZ::u16 + { + //! No flags are set. This can be used in conjunction with bitwise operators to check a flag. + Null = 0, + //! If set, this Visitor interface supports raw strings in place of specific value types. + //! Visitors with this flag accept RawValue calls in lieu of more specific value calls such as Int64 or String. + SupportsRawValues = (1 << 1), + //! If set, this Visitor interface supports raw strings in place of Name types for keys and Node names. + //! Visitors with this flag accept RawKey and RawStartNode in lieu of Key and StartNode calls. + SupportsRawKeys = (1 << 2), + //! If set, this Visitor interface supports Object types described via BeginObject and EndObject. + SupportsObjects = (1 << 3), + //! If set, this Visitor interface supports Array types described via BeginArray and EndArray. + SupportsArrays = (1 << 4), + //! If set, this Visitor interface supports Node types described BeginNode and EndNode. + SupportsNodes = (1 << 4), + //! If set, this Visitor interface supports opaque values described via OpaqueValue. + SupportsOpaqueValues = (1 << 5), + }; + + AZ_DEFINE_ENUM_BITWISE_OPERATORS(VisitorFlags); + + // + // Visitor class + // + //! An interface for performing operations on elements of a generic DOM (Document Object Model). + //! A Document Object Model is defined here as a tree structure comprised of one of the following values: + //! - Primitives: plain data types, including + //! - \ref Int64: 64 bit signed integer + //! - \ref Uint64: 64 bit unsigned integer + //! - \ref Bool: boolean value + //! - \ref Double: 64 bit double precision float + //! - \ref Null: sentinel "empty" type with no value representation + //! - \ref String: UTF8 encoded string + //! - \ref Object: an ordered container of key/value pairs where keys are AZ::Names and values may be any DOM type + //! (including Object) + //! - \ref Array: an ordered container of values, in which values are any DOM value type (including Array) + //! - \ref Node: a container + //! - \ref OpaqueValue: An arbitrary value stored in an AZStd::any. This is a non-serializable representation of an + //! entry useful for in-memory options. This is intended to be used as an intermediate value over the course of DOM + //! transformation and as a proxy to pass through types of which the DOM has no knowledge to other systems. + //! + //! Opaque values are rejected by the default VisitorInterface implementation. + //! + //! Care should be ensured that DOMs representing opaque types are only visited by consumers that understand them. + class Visitor + { + public: + virtual ~Visitor() = default; + + //! The result of a Visitor operation. + //! A failure indicates a non-recoverable issue and signals that no further visit calls may be made in the + //! current state. + using Result = AZ::Outcome; + + //! Returns a set of flags representing the operations this Visitor supports. + //! The base implementation supports raw keys (\see VisitorFlags::SupportsRawKeys) and + //! arrays (\see VisitorFlags::SupportsArrays), objects (\see VisitorFlags::SupportsObjects), and + //! nodes (\see VisitorFlags::SupportsNodes). + //! Raw (\see VisitorFlags::SupportsRawValues) and opaque values (\see VisitorFlags::SupportsOpaqueValues) + //! are disallowed by default, as their handling is intended to be implementation-specific. + virtual VisitorFlags GetVisitorFlags() const; + //! /see VisitorFlags::SupportsRawValues + bool SupportsRawValues() const; + //! /see VisitorFlags::SupportsRawKeys + bool SupportsRawKeys() const; + //! /see VisitorFlags::SupportsObjects + bool SupportsObjects() const; + //! /see VisitorFlags::SupportsArrays + bool SupportsArrays() const; + //! /see VisitorFlags::SupportsNodes + bool SupportsNodes() const; + //! /see VisitorFlags::SupportsOpaqueValues + bool SupportsOpaqueValues() const; + + //! Operates on an empty null value. + virtual Result Null(); + //! Operates on a bool value. + virtual Result Bool(bool value); + //! Operates on a signed, 64 bit integer value. + virtual Result Int64(AZ::s64 value); + //! Operates on an unsigned, 64 bit integer value. + virtual Result Uint64(AZ::u64 value); + //! Operates on a double precision, 64 bit floating point value. + virtual Result Double(double value); + //! Operates on a string value. As strings are a reference type. + //! Storage semantics are provided to indicate where the value may be stored persistently or requires a copy. + virtual Result String(AZStd::string_view value, Lifetime lifetime); + //! Operates on an opaque value. As opaque values are a reference type, storage semantics are provided to + //! indicate where the value may be stored persistently or requires a copy. + //! The base implementation of OpaqueValue rejects the operation, as opaque values are meant for special + //! cases with specific implementations, not generic usage. + //! Storage semantics are provided to indicate where the value may be stored persistently or requires a copy. + virtual Result OpaqueValue(const OpaqueType& value, Lifetime lifetime); + //! Operates on a raw value encoded as a UTF-8 string that hasn't had its type deduced. + //! Visitors that support raw values (\see VisitorFlags::SupportsRawValues) may parse the raw value and + //! forward it to the corresponding value call or calls of their choice. + //! The base implementation of RawValue rejects the operation, as raw values are meant to be handled on + //! a per-implementation basis. + virtual Result RawValue(AZStd::string_view value, Lifetime lifetime); + + //! Operates on an Object. + //! Callers may make any number of Key calls, followed by calls representing a value (including a nested + //! StartObject call) and then must call EndObject. + virtual Result StartObject(); + //! Finishes operating on an Object. + //! Callers must provide the number of attributes that were provided to the object, i.e. the number of key + //! and value calls made within the direct context of this object (but not any nested objects / nodes). + virtual Result EndObject(AZ::u64 attributeCount); + + //! Specifies a key for a key/value pair. + //! Key must be called subsequent to a call to \ref StartObject or \ref StartNode and immediately followed by + //! calls representing the key's associated value. + virtual Result Key(AZ::Name key); + //! Specifies a key for a key/value pair using a raw string instead of \ref AZ::Name. + //! \see Key + virtual Result RawKey(AZStd::string_view key, Lifetime lifetime); + + //! Operates on an Array. + //! Callers may make any number of subsequent value calls to represent the elements of the array, and then must + //! call EndArray. + virtual Result StartArray(); + //! Finishes operating on an Array. + //! Callers must provide the number of elements that were provided to the array, i.e. the number of value calls + //! made within the direct context of this array (but not any nested arrays / nodes). + virtual Result EndArray(AZ::u64 elementCount); + + //! Operates on a Node. + //! Callers may make any number of Key calls followed by value calls or value calls not prefixed with a Key + //! call, and then must call EndNode. See \ref StartObject and \ref StartArray as Node types combine the + //! functionality of both structures into a named Node structure. + virtual Result StartNode(AZ::Name name); + //! Operates on a Node using a raw string instead of \ref AZ::Name. + //! \see StartNode + virtual Result RawStartNode(AZStd::string_view name, Lifetime lifetime); + //! Finishes operating on a Node. + //! Callers must provide both the number of attributes the were provided and the number of elements that were + //! provided to the node, attributes being values prefaced by a call to Key. + virtual Result EndNode(AZ::u64 attributeCount, AZ::u64 elementCount); + + protected: + Visitor() = default; + + //! Helper method, constructs a failure \ref Result with the specified code. + static Result VisitorFailure(VisitorErrorCode code); + //! Helper method, constructs a failure \ref Result with the specified code and supplemental info. + static Result VisitorFailure(VisitorErrorCode code, AZStd::string additionalInfo); + //! Helper method, constructs a failure \ref Result with the specified error. + static Result VisitorFailure(VisitorError error); + //! Helper method, constructs a success \ref Result. + static Result VisitorSuccess(); + }; +} // namespace AZ::DOM diff --git a/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp b/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp index 36f66312d8..5458a3fadf 100644 --- a/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp +++ b/Code/Framework/AzCore/AzCore/Settings/SettingsRegistryMergeUtils.cpp @@ -634,12 +634,18 @@ namespace AZ::SettingsRegistryMergeUtils } // Project name - if it was set via merging project.json use that value, otherwise use the project path's folder name. - auto projectNameKey = - AZ::SettingsRegistryInterface::FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + constexpr auto projectNameKey = + FixedValueString(AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey) + "/project_name"; - AZ::SettingsRegistryInterface::FixedValueString projectName; - if (!registry.Get(projectName, projectNameKey)) + // Read the project name from the project.json file if it exists + if (AZ::IO::FixedMaxPath projectJsonPath = normalizedProjectPath / "project.json"; + AZ::IO::SystemFile::Exists(projectJsonPath.c_str())) + { + registry.MergeSettingsFile(projectJsonPath.Native(), + AZ::SettingsRegistryInterface::Format::JsonMergePatch, AZ::SettingsRegistryMergeUtils::ProjectSettingsRootKey); + } + if (FixedValueString projectName; !registry.Get(projectName, projectNameKey)) { projectName = path.Filename().Native(); registry.Set(projectNameKey, projectName); diff --git a/Code/Framework/AzCore/AzCore/azcore_files.cmake b/Code/Framework/AzCore/AzCore/azcore_files.cmake index 0c5a360844..6a9e5a29d6 100644 --- a/Code/Framework/AzCore/AzCore/azcore_files.cmake +++ b/Code/Framework/AzCore/AzCore/azcore_files.cmake @@ -123,6 +123,8 @@ set(FILES Debug/TraceMessagesDrillerBus.h Debug/TraceReflection.cpp Debug/TraceReflection.h + DOM/DomVisitor.cpp + DOM/DomVisitor.h Driller/DefaultStringPool.h Driller/Driller.cpp Driller/Driller.h diff --git a/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.cpp b/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.cpp index 68c4b00243..bc109b73c2 100644 --- a/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.cpp @@ -323,6 +323,13 @@ namespace AzFramework return localZ; } + void TransformComponent::SetWorldRotation(const AZ::Vector3& eulerAnglesRadian) + { + AZ::Transform newWorldTransform = m_worldTM; + newWorldTransform.SetRotation(AZ::Quaternion::CreateFromEulerAnglesRadians(eulerAnglesRadian)); + SetWorldTM(newWorldTransform); + } + void TransformComponent::SetWorldRotationQuaternion(const AZ::Quaternion& quaternion) { AZ::Transform newWorldTransform = m_worldTM; diff --git a/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.h b/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.h index ac282b4d49..2375f28ed1 100644 --- a/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.h +++ b/Code/Framework/AzFramework/AzFramework/Components/TransformComponent.h @@ -108,6 +108,7 @@ namespace AzFramework float GetLocalZ() override; // Rotation modifiers + void SetWorldRotation(const AZ::Vector3& eulerAnglesRadian) override; void SetWorldRotationQuaternion(const AZ::Quaternion& quaternion) override; AZ::Vector3 GetWorldRotation() override; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp index d2f74510e5..0a0e1bb8e3 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.cpp @@ -10,6 +10,7 @@ #include #include +#include namespace AzPhysics { @@ -28,6 +29,71 @@ namespace AzPhysics ->Field("ChildLocalPosition", &JointConfiguration::m_childLocalPosition) ->Field("StartSimulationEnabled", &JointConfiguration::m_startSimulationEnabled) ; + + if (auto* editContext = serializeContext->GetEditContext()) + { + editContext->Class("Joint Configuration", "Joint configuration.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement(AZ::Edit::UIHandlers::Default, &JointConfiguration::m_parentLocalRotation, + "Parent local rotation", "Parent joint frame relative to parent body.") + ->Attribute(AZ::Edit::Attributes::Visibility, &JointConfiguration::GetParentLocalRotationVisibility) + ->DataElement(AZ::Edit::UIHandlers::Default, &JointConfiguration::m_parentLocalPosition, + "Parent local position", "Joint position relative to parent body.") + ->Attribute(AZ::Edit::Attributes::Visibility, &JointConfiguration::GetParentLocalPositionVisibility) + ->DataElement(AZ::Edit::UIHandlers::Default, &JointConfiguration::m_childLocalRotation, + "Child local rotation", "Child joint frame relative to child body.") + ->Attribute(AZ::Edit::Attributes::Visibility, &JointConfiguration::GetChildLocalRotationVisibility) + ->DataElement(AZ::Edit::UIHandlers::Default, &JointConfiguration::m_childLocalPosition, + "Child local position", "Joint position relative to child body.") + ->Attribute(AZ::Edit::Attributes::Visibility, &JointConfiguration::GetChildLocalPositionVisibility) + ->DataElement(AZ::Edit::UIHandlers::Default, &JointConfiguration::m_startSimulationEnabled, + "Start simulation enabled", "When active, the joint will be enabled when the simulation begins.") + ->Attribute(AZ::Edit::Attributes::Visibility, &JointConfiguration::GetStartSimulationEnabledVisibility) + ; + } } } -} + + AZ::Crc32 JointConfiguration::GetPropertyVisibility(JointConfiguration::PropertyVisibility property) const + { + return (m_propertyVisibilityFlags & property) != 0 ? AZ::Edit::PropertyVisibility::Show : AZ::Edit::PropertyVisibility::Hide; + } + + void JointConfiguration::SetPropertyVisibility(JointConfiguration::PropertyVisibility property, bool isVisible) + { + if (isVisible) + { + m_propertyVisibilityFlags |= property; + } + else + { + m_propertyVisibilityFlags &= ~property; + } + } + + AZ::Crc32 JointConfiguration::GetParentLocalRotationVisibility() const + { + return GetPropertyVisibility(JointConfiguration::PropertyVisibility::ParentLocalRotation); + } + + AZ::Crc32 JointConfiguration::GetParentLocalPositionVisibility() const + { + return GetPropertyVisibility(JointConfiguration::PropertyVisibility::ParentLocalPosition); + } + + AZ::Crc32 JointConfiguration::GetChildLocalRotationVisibility() const + { + return GetPropertyVisibility(JointConfiguration::PropertyVisibility::ChildLocalRotation); + } + + AZ::Crc32 JointConfiguration::GetChildLocalPositionVisibility() const + { + return GetPropertyVisibility(JointConfiguration::PropertyVisibility::ChildLocalPosition); + } + + AZ::Crc32 JointConfiguration::GetStartSimulationEnabledVisibility() const + { + return GetPropertyVisibility(JointConfiguration::PropertyVisibility::StartSimulationEnabled); + } +} // namespace AzPhysics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h index ff9bfb5bea..2a246692d7 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/Configuration/JointConfiguration.h @@ -31,6 +31,25 @@ namespace AzPhysics JointConfiguration() = default; virtual ~JointConfiguration() = default; + // Visibility helpers for use in the Editor when reflected. + enum PropertyVisibility : AZ::u8 + { + ParentLocalRotation = 1 << 0, //!< Whether the parent local rotation is visible. + ParentLocalPosition = 1 << 1, //!< Whether the parent local position is visible. + ChildLocalRotation = 1 << 2, //!< Whether the child local rotation is visible. + ChildLocalPosition = 1 << 3, //!< Whether the child local position is visible. + StartSimulationEnabled = 1 << 4 //!< Whether the start simulation enabled setting is visible. + }; + + AZ::Crc32 GetPropertyVisibility(PropertyVisibility property) const; + void SetPropertyVisibility(PropertyVisibility property, bool isVisible); + + AZ::Crc32 GetParentLocalRotationVisibility() const; + AZ::Crc32 GetParentLocalPositionVisibility() const; + AZ::Crc32 GetChildLocalRotationVisibility() const; + AZ::Crc32 GetChildLocalPositionVisibility() const; + AZ::Crc32 GetStartSimulationEnabledVisibility() const; + // Entity/object association. void* m_customUserData = nullptr; @@ -40,8 +59,11 @@ namespace AzPhysics AZ::Quaternion m_childLocalRotation = AZ::Quaternion::CreateIdentity(); ///< Child joint frame relative to child body. AZ::Vector3 m_childLocalPosition = AZ::Vector3::CreateZero(); ///< Joint position relative to child body. bool m_startSimulationEnabled = true; - + // For debugging/tracking purposes only. AZStd::string m_debugName; + + // Default all visibility settings to invisible, since most joint configurations don't need to display these. + AZ::u8 m_propertyVisibilityFlags = 0; }; } diff --git a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp index 79a6333612..c7663dc1ab 100644 --- a/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp +++ b/Code/Framework/AzFramework/Platform/Common/Xcb/AzFramework/XcbNativeWindow.cpp @@ -258,10 +258,17 @@ namespace AzFramework //////////////////////////////////////////////////////////////////////////////////////////////// void XcbNativeWindow::SetWindowTitle(const AZStd::string& title) { + // Set the title of both the window and the task bar by using + // a buffer to hold the title twice, separated by a null-terminator + auto doubleTitleSize = (title.size() + 1) * 2; + AZStd::string doubleTitle(doubleTitleSize, '\0'); + azstrncpy(doubleTitle.data(), doubleTitleSize, title.c_str(), title.size()); + azstrncpy(&doubleTitle.data()[title.size() + 1], title.size(), title.c_str(), title.size()); + xcb_void_cookie_t xcbCheckResult; xcbCheckResult = xcb_change_property( - m_xcbConnection, XCB_PROP_MODE_REPLACE, m_xcbWindow, XCB_ATOM_WM_NAME, XCB_ATOM_STRING, 8, static_cast(title.size()), - title.c_str()); + m_xcbConnection, XCB_PROP_MODE_REPLACE, m_xcbWindow, XCB_ATOM_WM_CLASS, XCB_ATOM_STRING, 8, static_cast(doubleTitle.size()), + doubleTitle.c_str()); AZ_Assert(ValidateXcbResult(xcbCheckResult), "Failed to set window title."); } diff --git a/Code/Framework/AzGameFramework/AzGameFramework/Application/GameApplication.cpp b/Code/Framework/AzGameFramework/AzGameFramework/Application/GameApplication.cpp index 0cce93d751..6957844452 100644 --- a/Code/Framework/AzGameFramework/AzGameFramework/Application/GameApplication.cpp +++ b/Code/Framework/AzGameFramework/AzGameFramework/Application/GameApplication.cpp @@ -82,7 +82,7 @@ namespace AzGameFramework // Used the lowercase the platform name since the bootstrap.game...setreg is being loaded // from the asset cache root where all the files are in lowercased from regardless of the filesystem case-sensitivity - static constexpr char filename[] = "bootstrap.game." AZ_BUILD_CONFIGURATION_TYPE "." AZ_TRAIT_OS_PLATFORM_CODENAME_LOWER ".setreg"; + static constexpr char filename[] = "bootstrap.game." AZ_BUILD_CONFIGURATION_TYPE ".setreg"; AZ::IO::FixedMaxPath cacheRootPath; if (registry.Get(cacheRootPath.Native(), AZ::SettingsRegistryMergeUtils::FilePathKey_CacheRootFolder)) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h index 9e29b9813c..fd4196a296 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ToolsApplicationAPI.h @@ -826,6 +826,10 @@ namespace AzToolsFramework /// Path will be empty if component should have no icon. virtual AZStd::string GetComponentEditorIcon(const AZ::Uuid& /*componentType*/, AZ::Component* /*component*/) { return AZStd::string(); } + //! Return path to icon for component type. + //! Path will be empty if component type should have no icon. + virtual AZStd::string GetComponentTypeEditorIcon(const AZ::Uuid& /*componentType*/) { return AZStd::string(); } + /** * Return the icon image path based on the component type and where it is used. * \param componentType component type diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h index 966b9f8478..2d25dbbafc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/API/ViewportEditorModeTrackerNotificationBus.h @@ -43,6 +43,8 @@ namespace AzToolsFramework }; //! Provides a bus to notify when the different editor modes are entered/exit. + //! @note The editor modes are not discrete states but rather each progression of mode retain the active the parent + //! mode that the new mode progressed from. class ViewportEditorModeNotifications : public AZ::EBusTraits { public: diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp index ce50fc7e00..6a926b89fd 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.cpp @@ -9,9 +9,27 @@ #include #include +#include namespace AzToolsFramework { + static bool AreEntitiesValidForDuplication(const EntityIdList& entityIds) + { + for (AZ::EntityId entityId : entityIds) + { + if (GetEntityById(entityId) == nullptr) + { + AZ_Error( + "Entity", false, + "Entity with id '%llu' is not found. This can happen when you try to duplicate the entity before it is created. Please " + "ensure entities are created before trying to duplicate them.", + static_cast(entityId)); + return false; + } + } + return true; + } + void EditorEntityManager::Start() { m_prefabPublicInterface = AZ::Interface::Get(); @@ -62,7 +80,11 @@ namespace AzToolsFramework EntityIdList selectedEntities; ToolsApplicationRequestBus::BroadcastResult(selectedEntities, &ToolsApplicationRequests::GetSelectedEntities); - m_prefabPublicInterface->DuplicateEntitiesInInstance(selectedEntities); + if (AreEntitiesValidForDuplication(selectedEntities)) + { + m_prefabPublicInterface->DuplicateEntitiesInInstance(selectedEntities); + } + } void EditorEntityManager::DuplicateEntityById(AZ::EntityId entityId) @@ -72,7 +94,10 @@ namespace AzToolsFramework void EditorEntityManager::DuplicateEntities(const EntityIdList& entities) { - m_prefabPublicInterface->DuplicateEntitiesInInstance(entities); + if (AreEntitiesValidForDuplication(entities)) + { + m_prefabPublicInterface->DuplicateEntitiesInInstance(entities); + } } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h index 5786cc3fbc..c4311de672 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/EditorEntityManager.h @@ -34,5 +34,4 @@ namespace AzToolsFramework private: Prefab::PrefabPublicInterface* m_prefabPublicInterface = nullptr; }; - } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp index 3f254581dd..67563aaf69 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Application/ToolsApplication.cpp @@ -176,7 +176,7 @@ namespace AzToolsFramework , public AZ::BehaviorEBusHandler { AZ_EBUS_BEHAVIOR_BINDER(ToolsApplicationNotificationBusHandler, "{7EB67956-FF86-461A-91E2-7B08279CFACF}", AZ::SystemAllocator, - EntityRegistered, EntityDeregistered); + EntityRegistered, EntityDeregistered, AfterEntitySelectionChanged); void EntityRegistered(AZ::EntityId entityId) override { @@ -187,6 +187,11 @@ namespace AzToolsFramework { Call(FN_EntityDeregistered, entityId); } + + void AfterEntitySelectionChanged(const EntityIdList& newlySelectedEntities, const EntityIdList& newlyDeselectedEntities) override + { + Call(FN_AfterEntitySelectionChanged, newlySelectedEntities, newlyDeselectedEntities); + } }; struct ViewPaneCallbackBusHandler final @@ -410,6 +415,7 @@ namespace AzToolsFramework ->Handler() ->Event("EntityRegistered", &ToolsApplicationEvents::EntityRegistered) ->Event("EntityDeregistered", &ToolsApplicationEvents::EntityDeregistered) + ->Event("AfterEntitySelectionChanged", &ToolsApplicationEvents::AfterEntitySelectionChanged) ; behaviorContext->Class() @@ -428,6 +434,7 @@ namespace AzToolsFramework ->Attribute(AZ::Script::Attributes::Module, "editor") ->Event("RegisterCustomViewPane", &EditorRequests::RegisterCustomViewPane) ->Event("UnregisterViewPane", &EditorRequests::UnregisterViewPane) + ->Event("GetComponentTypeEditorIcon", &EditorRequests::GetComponentTypeEditorIcon) ; behaviorContext->EBus("EditorEventBus") diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index d976c91c3e..a6a80e58d3 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -1110,7 +1110,8 @@ namespace AzToolsFramework // Select the duplicated entities/instances auto selectionUndo = aznew SelectionCommand(duplicatedEntityAndInstanceIds, "Select Duplicated Entities/Instances"); selectionUndo->SetParent(undoBatch.GetUndoBatch()); - ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds); + ToolsApplicationRequestBus::Broadcast( + &ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds); } return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds)); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp index b6f5b41b65..888554d065 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/ComponentPalette/ComponentPaletteUtil.cpp @@ -90,7 +90,7 @@ namespace AzToolsFramework } AZStd::string componentIconPath; - EBUS_EVENT_RESULT(componentIconPath, AzToolsFramework::EditorRequests::Bus, GetComponentEditorIcon, componentClass->m_typeId, nullptr); + AzToolsFramework::EditorRequestBus::BroadcastResult(componentIconPath, &AzToolsFramework::EditorRequests::GetComponentTypeEditorIcon, componentClass->m_typeId); componentIconTable[componentClass] = QString::fromUtf8(componentIconPath.c_str()); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp index d94ced392c..0609f8113d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp @@ -72,7 +72,7 @@ namespace AzToolsFramework void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event) { - m_mousePosition = QPoint(); + m_mousePosition = QPoint(-1, -1); m_currentHoveredIndex = QModelIndex(); update(); } @@ -200,7 +200,7 @@ namespace AzToolsFramework const bool isEnabled = (this->model()->flags(index) & Qt::ItemIsEnabled); const bool isSelected = selectionModel()->isSelected(index); - const bool isHovered = (index == indexAt(m_mousePosition)) && isEnabled; + const bool isHovered = (index == indexAt(m_mousePosition).siblingAtColumn(0)) && isEnabled; // Paint the branch Selection/Hover Rect PaintBranchSelectionHoverRect(painter, rect, isSelected, isHovered); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp index 3e97f967fc..d6e9cac754 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp @@ -153,6 +153,9 @@ namespace AzToolsFramework { initEntityOutlinerWidgetResources(); + m_editorEntityUiInterface = AZ::Interface::Get(); + AZ_Assert(m_editorEntityUiInterface != nullptr, "EntityOutlinerWidget requires a EditorEntityUiInterface instance on Initialize."); + m_gui = new Ui::EntityOutlinerWidgetUI(); m_gui->setupUi(this); @@ -282,12 +285,6 @@ namespace AzToolsFramework m_listModel->Initialize(); - m_editorEntityUiInterface = AZ::Interface::Get(); - - AZ_Assert( - m_editorEntityUiInterface != nullptr, - "EntityOutlinerWidget requires a EditorEntityUiInterface instance on Initialize."); - EditorPickModeNotificationBus::Handler::BusConnect(GetEntityContextId()); EntityHighlightMessages::Bus::Handler::BusConnect(); EntityOutlinerModelNotificationBus::Handler::BusConnect(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ComponentEditor.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ComponentEditor.cpp index 91d2359ca6..48dec6f225 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ComponentEditor.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ComponentEditor.cpp @@ -583,7 +583,7 @@ namespace AzToolsFramework } AZStd::string iconPath; - EBUS_EVENT_RESULT(iconPath, AzToolsFramework::EditorRequests::Bus, GetComponentEditorIcon, componentType, const_cast(&componentInstance)); + AzToolsFramework::EditorRequestBus::BroadcastResult(iconPath, &AzToolsFramework::EditorRequests::GetComponentEditorIcon, componentType, const_cast(&componentInstance)); GetHeader()->SetIcon(QIcon(iconPath.c_str())); bool isExpanded = true; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyAssetCtrl.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyAssetCtrl.cpp index 7f3b9e61fd..24b2c7466e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyAssetCtrl.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/PropertyAssetCtrl.cpp @@ -1387,7 +1387,10 @@ namespace AzToolsFramework QDataStream stream(&pixmapBytes, QIODevice::ReadOnly); QPixmap pixmap; stream >> pixmap; - GUI->SetBrowseButtonIcon(pixmap); + if (!pixmap.isNull()) + { + GUI->SetBrowseButtonIcon(pixmap); + } } } } @@ -1417,6 +1420,8 @@ namespace AzToolsFramework } else if (attrib == AZ_CRC_CE("ThumbnailIcon")) { + GUI->SetCustomThumbnailEnabled(false); + AZStd::string iconPath; if (attrValue->Read(iconPath) && !iconPath.empty()) { @@ -1434,8 +1439,11 @@ namespace AzToolsFramework QDataStream stream(&pixmapBytes, QIODevice::ReadOnly); QPixmap pixmap; stream >> pixmap; - GUI->SetCustomThumbnailEnabled(true); - GUI->SetCustomThumbnailPixmap(pixmap); + if (!pixmap.isNull()) + { + GUI->SetCustomThumbnailEnabled(true); + GUI->SetCustomThumbnailPixmap(pixmap); + } } } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ThumbnailPropertyCtrl.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ThumbnailPropertyCtrl.cpp index d8ddee6b76..0bbb898196 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ThumbnailPropertyCtrl.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/PropertyEditor/ThumbnailPropertyCtrl.cpp @@ -67,16 +67,9 @@ namespace AzToolsFramework void ThumbnailPropertyCtrl::SetThumbnailKey(Thumbnailer::SharedThumbnailKey key, const char* contextName) { - if (m_customThumbnailEnabled) - { - ClearThumbnail(); - } - else - { - m_key = key; - m_thumbnail->SetThumbnailKey(m_key, contextName); - m_thumbnailEnlarged->SetThumbnailKey(m_key, contextName); - } + m_key = key; + m_thumbnail->SetThumbnailKey(m_key, contextName); + m_thumbnailEnlarged->SetThumbnailKey(m_key, contextName); UpdateVisibility(); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp index 43e2a6793f..0f79e3535d 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp @@ -301,6 +301,7 @@ namespace AzToolsFramework::ViewportUi::Internal HighlightBorderSize + ViewportUiOverlayMargin, HighlightBorderSize + ViewportUiOverlayMargin); m_componentModeBorderText.setVisible(true); m_componentModeBorderText.setText(borderTitle.c_str()); + UpdateUiOverlayGeometry(); } void ViewportUiDisplay::RemoveViewportBorder() @@ -427,8 +428,8 @@ namespace AzToolsFramework::ViewportUi::Internal void ViewportUiDisplay::PositionUiOverlayOverRenderViewport() { QPoint offset = m_renderOverlay->mapToGlobal(QPoint()); - m_uiMainWindow.move(offset); - m_uiOverlay.setFixedSize(m_renderOverlay->width(), m_renderOverlay->height()); + m_uiMainWindow.setGeometry(offset.x(), offset.y(), m_renderOverlay->width(), m_renderOverlay->height()); + m_uiOverlay.setGeometry(m_uiMainWindow.rect()); UpdateUiOverlayGeometry(); } diff --git a/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp b/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp index db6a0b8571..85b65f3edf 100644 --- a/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp +++ b/Code/Framework/AzToolsFramework/Tests/Viewport/ViewportEditorModeTests.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -187,10 +188,13 @@ namespace UnitTest ASSERT_NE(m_viewportEditorModeTracker, nullptr); m_viewportEditorModes = m_viewportEditorModeTracker->GetViewportEditorModes({AzToolsFramework::GetEntityContextId()}); ASSERT_NE(m_viewportEditorModes, nullptr); + m_focusModeInterface = AZ::Interface::Get(); + ASSERT_NE(m_focusModeInterface, nullptr); } ViewportEditorModeTrackerInterface* m_viewportEditorModeTracker = nullptr; const ViewportEditorModesInterface* m_viewportEditorModes = nullptr; + AzToolsFramework::FocusModeInterface* m_focusModeInterface = nullptr; }; TEST_F(ViewportEditorModesTestsFixture, NumberOfEditorModesIsEqualTo4) @@ -522,32 +526,48 @@ namespace UnitTest } TEST_F( - ViewportEditorModeTrackerIntegrationTestFixture, EnteringComponentModeAfterInitialStateHasViewportEditorModesDefaultAndComponentModeActive) + ViewportEditorModeTrackerIntegrationTestFixture, + EnteringComponentModeAfterInitialStateHasViewportEditorModesDefaultAndComponentModeActive) { // When component mode is entered AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast( &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::BeginComponentMode, AZStd::vector{}); - bool inComponentMode = false; - AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::BroadcastResult( - inComponentMode, &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::InComponentMode); - // Expect to be in component mode - EXPECT_TRUE(inComponentMode); + EXPECT_TRUE(AzToolsFramework::ComponentModeFramework::InComponentMode()); // Expect the default and component viewport editor modes to be active EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Default)); EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Component)); - - // Do not expect the pick and focus viewport editor modes to be active EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Pick)); EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Focus)); } TEST_F( ViewportEditorModeTrackerIntegrationTestFixture, - EnteringEditorPickEntitySelectionAfterInitialStateHasOnlyViewportEditorModePickModeActive) + ExitingComponentModeAfterEnteringFrominitialStateHasViewportEditorModesDefaultActive) + { + // When component mode is entered and exited + AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast( + &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::BeginComponentMode, + AZStd::vector{}); + + EXPECT_TRUE(AzToolsFramework::ComponentModeFramework::InComponentMode()); + + AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast( + &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::EndComponentMode); + + // Expect to not be in component mode + EXPECT_FALSE(AzToolsFramework::ComponentModeFramework::InComponentMode()); + + // Expect only the default viewport editor mode to be active + ExpectOnlyModeActive(*m_viewportEditorModes, ViewportEditorMode::Default); + } + + TEST_F( + ViewportEditorModeTrackerIntegrationTestFixture, + EnteringEditorPickEntitySelectionAfterInitialStateHasOnlyViewportEditorModePickActive) { // When entering pick mode using AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus; @@ -563,6 +583,96 @@ namespace UnitTest ExpectOnlyModeActive(*m_viewportEditorModes, ViewportEditorMode::Pick); } - // FocusMode integration tests will follow (LYN-6995) + TEST_F( + ViewportEditorModeTrackerIntegrationTestFixture, + EnteringEditorDefaultEntitySelectionFromEditorPickEntitySelectionHasOnlyViewportEditorModeDefaultActive) + { + // When pick mode is entered and exited + using AzToolsFramework::EditorInteractionSystemViewportSelectionRequestBus; + EditorInteractionSystemViewportSelectionRequestBus::Event( + AzToolsFramework::GetEntityContextId(), &EditorInteractionSystemViewportSelectionRequestBus::Events::SetHandler, + [](const AzToolsFramework::EditorVisibleEntityDataCache* entityDataCache, + [[maybe_unused]] AzToolsFramework::ViewportEditorModeTrackerInterface* viewportEditorModeTracker) + { + return AZStd::make_unique(entityDataCache, viewportEditorModeTracker); + }); + EditorInteractionSystemViewportSelectionRequestBus::Event( + AzToolsFramework::GetEntityContextId(), &EditorInteractionSystemViewportSelectionRequestBus::Events::SetHandler, + [](const AzToolsFramework::EditorVisibleEntityDataCache* entityDataCache, + [[maybe_unused]] AzToolsFramework::ViewportEditorModeTrackerInterface* viewportEditorModeTracker) + { + return AZStd::make_unique(entityDataCache, viewportEditorModeTracker); + }); + + // Expect only the default viewport editor mode to be active + ExpectOnlyModeActive(*m_viewportEditorModes, ViewportEditorMode::Default); + } + + TEST_F(ViewportEditorModeTrackerIntegrationTestFixture, EnteringFocusModeAfterInitialStateHasViewportEditorModeDefaultAndPickActive) + { + // When entering focus mode + m_focusModeInterface->SetFocusRoot(AZ::EntityId{ 1 }); + + // Expect the default and focus viewport editor modes to be active + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Default)); + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Focus)); + EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Pick)); + EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Component)); + } + + TEST_F( + ViewportEditorModeTrackerIntegrationTestFixture, + ExitingFocusModeAfterEnteringFromInitialStateHasOnlyViewportEditorModeDefaultActive) + { + // When entering and leaving focus mode + m_focusModeInterface->SetFocusRoot(AZ::EntityId{ 1 }); + m_focusModeInterface->SetFocusRoot(AZ::EntityId()); + + // Expect only the default mode to be active + ExpectOnlyModeActive(*m_viewportEditorModes, ViewportEditorMode::Default); + } + + TEST_F(ViewportEditorModeTrackerIntegrationTestFixture, EnteringComponentModeFromFocusModeStateHasViewportEditorModeDefaultAndFocusAndComponentActive) + { + // When entering component mode from focus mode + m_focusModeInterface->SetFocusRoot(AZ::EntityId{ 1 }); + AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast( + &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::BeginComponentMode, + AZStd::vector{}); + + // Expect to be in component mode + EXPECT_TRUE(AzToolsFramework::ComponentModeFramework::InComponentMode()); + + // Expect the default, focus and component viewport editor modes to be active + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Default)); + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Focus)); + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Component)); + EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Pick)); + } + + TEST_F( + ViewportEditorModeTrackerIntegrationTestFixture, + ExitingComponentModeAfterEnteringFromFocusModeHasViewportEditorModeDefaultAndFocusActive) + { + // When entering and leaving component mode from focus mode + m_focusModeInterface->SetFocusRoot(AZ::EntityId{ 1 }); + AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast( + &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::BeginComponentMode, + AZStd::vector{}); + + EXPECT_TRUE(AzToolsFramework::ComponentModeFramework::InComponentMode()); + + AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequestBus::Broadcast( + &AzToolsFramework::ComponentModeFramework::ComponentModeSystemRequests::EndComponentMode); + + // Expect to not be in component mode + EXPECT_FALSE(AzToolsFramework::ComponentModeFramework::InComponentMode()); + + // Expect the default and focus viewport editor modes to be active + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Default)); + EXPECT_TRUE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Focus)); + EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Component)); + EXPECT_FALSE(m_viewportEditorModes->IsModeActive(ViewportEditorMode::Pick)); + } } // namespace UnitTest diff --git a/Code/Legacy/CrySystem/CrySystem_precompiled.h b/Code/Legacy/CrySystem/CrySystem_precompiled.h index faa924931f..3ececa6514 100644 --- a/Code/Legacy/CrySystem/CrySystem_precompiled.h +++ b/Code/Legacy/CrySystem/CrySystem_precompiled.h @@ -60,13 +60,6 @@ #include -#if defined(WIN32) || defined(WIN64) || defined(APPLE) || defined(LINUX) -#if defined(DEDICATED_SERVER) -// enable/disable map load slicing functionality from the build -#define MAP_LOADING_SLICING -#endif -#endif - #ifdef WIN32 #include #include diff --git a/Code/Legacy/CrySystem/ViewSystem/ViewSystem.cpp b/Code/Legacy/CrySystem/ViewSystem/ViewSystem.cpp index e83676827f..d7e5c081c0 100644 --- a/Code/Legacy/CrySystem/ViewSystem/ViewSystem.cpp +++ b/Code/Legacy/CrySystem/ViewSystem/ViewSystem.cpp @@ -115,17 +115,20 @@ CViewSystem::CViewSystem(ISystem* pSystem) , m_useDeferredViewSystemUpdate(false) , m_bControlsAudioListeners(true) { -#if !defined(_RELEASE) && !defined(DEDICATED_SERVER) - if (!s_debugCamera) +#if !defined(_RELEASE) + if (!gEnv->IsDedicated()) { - s_debugCamera = new DebugCamera; - } + if (!s_debugCamera) + { + s_debugCamera = new DebugCamera; + } - REGISTER_COMMAND("debugCameraToggle", ToggleDebugCamera, VF_DEV_ONLY, "Toggle the debug camera.\n"); - REGISTER_COMMAND("debugCameraInvertY", ToggleDebugCameraInvertY, VF_DEV_ONLY, "Toggle debug camera Y-axis inversion.\n"); - REGISTER_COMMAND("debugCameraMove", DebugCameraMove, VF_DEV_ONLY, "Move the debug camera the specified distance (x y z).\n"); - gEnv->pConsole->CreateKeyBind("ctrl_keyboard_key_punctuation_backslash", "debugCameraToggle"); - gEnv->pConsole->CreateKeyBind("alt_keyboard_key_punctuation_backslash", "debugCameraInvertY"); + REGISTER_COMMAND("debugCameraToggle", ToggleDebugCamera, VF_DEV_ONLY, "Toggle the debug camera.\n"); + REGISTER_COMMAND("debugCameraInvertY", ToggleDebugCameraInvertY, VF_DEV_ONLY, "Toggle debug camera Y-axis inversion.\n"); + REGISTER_COMMAND("debugCameraMove", DebugCameraMove, VF_DEV_ONLY, "Move the debug camera the specified distance (x y z).\n"); + gEnv->pConsole->CreateKeyBind("ctrl_keyboard_key_punctuation_backslash", "debugCameraToggle"); + gEnv->pConsole->CreateKeyBind("alt_keyboard_key_punctuation_backslash", "debugCameraInvertY"); + } #endif REGISTER_CVAR2("cl_camera_noise", &m_fCameraNoise, -1, 0, @@ -167,6 +170,21 @@ CViewSystem::~CViewSystem() { m_pSystem->GetILevelSystem()->RemoveListener(this); } + +#if !defined(_RELEASE) + if (!gEnv->IsDedicated()) + { + UNREGISTER_COMMAND("debugCameraToggle"); + UNREGISTER_COMMAND("debugCameraInvertY"); + UNREGISTER_COMMAND("debugCameraMove"); + + if (s_debugCamera) + { + delete s_debugCamera; + s_debugCamera = nullptr; + } + } +#endif } //------------------------------------------------------------------------ diff --git a/Code/Tools/AssetBundler/CMakeLists.txt b/Code/Tools/AssetBundler/CMakeLists.txt index d613b35ce5..dcc62595c9 100644 --- a/Code/Tools/AssetBundler/CMakeLists.txt +++ b/Code/Tools/AssetBundler/CMakeLists.txt @@ -47,6 +47,10 @@ ly_add_target( AZ::AssetBundlerBatch.Static ) +# Adds a specialized .setreg to identify gems enabled in the active project. +# This associates the AssetBundlerBatch target with the .Builders gem variants. +ly_set_gem_variant_to_load(TARGETS AssetBundlerBatch VARIANTS Builders) + # AssetBundler - Qt GUI Application ly_add_target( NAME AssetBundler ${PAL_TRAIT_BUILD_ASSETBUNDLER_APPLICATION_TYPE} @@ -73,6 +77,10 @@ ly_add_target( ${additional_dependencies} ) +# Adds a specialized .setreg to identify gems enabled in the active project. +# This associates the AssetBundler target with the .Builders gem variants. +ly_set_gem_variant_to_load(TARGETS AssetBundler VARIANTS Builders) + if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) ly_add_target( diff --git a/Code/Tools/AssetBundler/source/utils/applicationManager.cpp b/Code/Tools/AssetBundler/source/utils/applicationManager.cpp index 0742035c1a..0388570fdd 100644 --- a/Code/Tools/AssetBundler/source/utils/applicationManager.cpp +++ b/Code/Tools/AssetBundler/source/utils/applicationManager.cpp @@ -54,7 +54,12 @@ namespace AssetBundler bool ApplicationManager::Init() { AZ::Debug::TraceMessageBus::Handler::BusConnect(); - Start(AzFramework::Application::Descriptor()); + + ComponentApplication::StartupParameters startupParameters; + // The AssetBundler does not need to load gems + startupParameters.m_loadDynamicModules = false; + Start(AzFramework::Application::Descriptor(), startupParameters); + AZ::SerializeContext* context; EBUS_EVENT_RESULT(context, AZ::ComponentApplicationBus, GetSerializeContext); AZ_Assert(context, "No serialize context"); diff --git a/Code/Tools/AssetBundler/tests/applicationManagerTests.cpp b/Code/Tools/AssetBundler/tests/applicationManagerTests.cpp index 35d3a5da1f..0d915dcc49 100644 --- a/Code/Tools/AssetBundler/tests/applicationManagerTests.cpp +++ b/Code/Tools/AssetBundler/tests/applicationManagerTests.cpp @@ -71,7 +71,11 @@ namespace AssetBundler m_data->m_applicationManager.reset(aznew MockApplicationManagerTest(0, 0)); - m_data->m_applicationManager->Start(AzFramework::Application::Descriptor()); + + AZ::ComponentApplication::StartupParameters startupParameters; + // The AssetBundler does not need to load gems + startupParameters.m_loadDynamicModules = false; + m_data->m_applicationManager->Start(AzFramework::Application::Descriptor(), startupParameters); // Without this, the user settings component would attempt to save on finalize/shutdown. Since the file is // shared across the whole engine, if multiple tests are run in parallel, the saving could cause a crash diff --git a/Code/Tools/AssetProcessor/native/InternalBuilders/SettingsRegistryBuilder.cpp b/Code/Tools/AssetProcessor/native/InternalBuilders/SettingsRegistryBuilder.cpp index c65ab24aed..6f15fa5ea2 100644 --- a/Code/Tools/AssetProcessor/native/InternalBuilders/SettingsRegistryBuilder.cpp +++ b/Code/Tools/AssetProcessor/native/InternalBuilders/SettingsRegistryBuilder.cpp @@ -259,6 +259,11 @@ namespace AssetProcessor scratchBuffer.reserve(512 * 1024); // Reserve 512kb to avoid repeatedly resizing the buffer; AZStd::fixed_vector platformCodes; AzFramework::PlatformHelper::AppendPlatformCodeNames(platformCodes, request.m_platformInfo.m_identifier); + AZ_Assert(platformCodes.size() <= 1, "A one-to-one mapping of asset type platform identifier" + " to platform codename is required in the SettingsRegistryBuilder." + " The bootstrap.game is now only produced per build configuration and doesn't take into account" + " different platforms names"); + const AZStd::string& assetPlatformIdentifier = request.m_jobDescription.GetPlatformIdentifier(); // Determines the suffix that will be used for the launcher based on processing server vs non-server assets const char* launcherType = assetPlatformIdentifier != AzFramework::PlatformHelper::GetPlatformName(AzFramework::PlatformId::SERVER) @@ -293,9 +298,10 @@ namespace AssetProcessor outputBuffer.Reserve(512 * 1024); // Reserve 512kb to avoid repeatedly resizing the buffer; SettingsExporter exporter(outputBuffer, excludes); - for (AZStd::string_view platform : platformCodes) + if (!platformCodes.empty()) { - AZ::u32 productSubID = static_cast(AZStd::hash{}(platform)); // Deliberately ignoring half the bits. + AZStd::string_view platform = platformCodes.front(); + constexpr AZ::u32 productSubID = 0; for (size_t i = 0; i < AZStd::size(specializations); ++i) { const AZ::SettingsRegistryInterface::Specializations& specialization = specializations[i]; @@ -337,7 +343,7 @@ namespace AssetProcessor // The purpose of this section is to copy the Gem's SourcePaths from the Global Settings Registry // the local SettingsRegistry. The reason this is needed is so that the call to // `MergeSettingsToRegistry_GemRegistries` below is able to locate each gem's "/Registry" folder - // that will be merged into the bootstrap.game...setreg file + // that will be merged into the bootstrap.game..setreg file // This is used by the GameLauncher applications to read from a single merged .setreg file // containing the settings needed to run a game/simulation without have access to the source code base registry AZStd::vector gemInfos; @@ -408,8 +414,6 @@ namespace AssetProcessor } outputPath += specialization.GetSpecialization(0); // Append configuration - outputPath += '.'; - outputPath += platform; outputPath += ".setreg"; AZ::IO::SystemFile file; diff --git a/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp b/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp index 6c19213414..0b02b168dc 100644 --- a/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp +++ b/Code/Tools/AssetProcessor/native/tests/assetmanager/AssetProcessorManagerTest.cpp @@ -4447,7 +4447,9 @@ AssetBuilderSDK::AssetBuilderDesc MockBuilderInfoHandler::CreateBuilderDesc(cons void FingerprintTest::SetUp() { + AZ_Printf("FingerprintTest", "SetUp start"); AssetProcessorManagerTest::SetUp(); + AZ_Printf("FingerprintTest", "SetUp self"); // We don't want the mock application manager to provide builder descriptors, mockBuilderInfoHandler will provide our own m_mockApplicationManager->BusDisconnect(); @@ -4466,18 +4468,23 @@ void FingerprintTest::SetUp() }); ASSERT_TRUE(UnitTestUtils::CreateDummyFile(m_absolutePath, "")); + AZ_Printf("FingerprintTest", "SetUp end"); } void FingerprintTest::TearDown() { + AZ_Printf("FingerprintTest", "TearDown start"); m_jobResults = AZStd::vector{}; m_mockBuilderInfoHandler = {}; + AZ_Printf("FingerprintTest", "TearDown parent"); AssetProcessorManagerTest::TearDown(); + AZ_Printf("FingerprintTest", "TearDown end"); } void FingerprintTest::RunFingerprintTest(QString builderFingerprint, QString jobFingerprint, bool expectedResult) { + AZ_Printf("FingerprintTest", "Fingerprint Test Start"); m_mockBuilderInfoHandler.m_builderDesc.m_analysisFingerprint = builderFingerprint.toUtf8().data(); m_mockBuilderInfoHandler.m_jobFingerprint = jobFingerprint; QMetaObject::invokeMethod(m_assetProcessorManager.get(), "AssessModifiedFile", Qt::QueuedConnection, Q_ARG(QString, m_absolutePath)); @@ -4486,6 +4493,7 @@ void FingerprintTest::RunFingerprintTest(QString builderFingerprint, QString job ASSERT_EQ(m_mockBuilderInfoHandler.m_createJobsCount, 1); ASSERT_EQ(m_jobResults.size(), 1); ASSERT_EQ(m_jobResults[0].m_autoFail, expectedResult); + AZ_Printf("FingerprintTest", "Fingerprint Test End"); } TEST_F(FingerprintTest, FingerprintChecking_JobFingerprint_NoBuilderFingerprint) diff --git a/Code/Tools/AssetProcessor/native/unittests/UnitTestRunner.h b/Code/Tools/AssetProcessor/native/unittests/UnitTestRunner.h index c281b3050a..0c4357f84a 100644 --- a/Code/Tools/AssetProcessor/native/unittests/UnitTestRunner.h +++ b/Code/Tools/AssetProcessor/native/unittests/UnitTestRunner.h @@ -21,6 +21,7 @@ #endif #include +#include //! These macros can be used for checking your unit tests, //! you can check AssetScannerUnitTest.cpp for usage @@ -155,6 +156,7 @@ namespace UnitTestUtils bool OnPreWarning([[maybe_unused]] const char* window, [[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* message) override { + UnitTest::ColoredPrintf(UnitTest::COLOR_YELLOW, message); ++m_numWarningsAbsorbed; if (m_debugMessages) { @@ -165,6 +167,7 @@ namespace UnitTestUtils bool OnPreAssert([[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* message) override { + UnitTest::ColoredPrintf(UnitTest::COLOR_YELLOW, message); ++m_numAssertsAbsorbed; if (m_debugMessages) { @@ -175,6 +178,7 @@ namespace UnitTestUtils bool OnPreError([[maybe_unused]] const char* window, [[maybe_unused]] const char* fileName, [[maybe_unused]] int line, [[maybe_unused]] const char* func, [[maybe_unused]] const char* message) override { + UnitTest::ColoredPrintf(UnitTest::COLOR_YELLOW, message); ++m_numErrorsAbsorbed; if (m_debugMessages) { @@ -183,8 +187,9 @@ namespace UnitTestUtils return true; // I handled this, do not forward it } - bool OnPrintf(const char* /*window*/, const char* /*message*/) override + bool OnPrintf(const char* /*window*/, const char* message) override { + UnitTest::ColoredPrintf(UnitTest::COLOR_YELLOW, message); ++m_numMessagesAbsorbed; return true; } diff --git a/Code/Tools/ProjectManager/Platform/Windows/ProjectBuilderWorker_windows.cpp b/Code/Tools/ProjectManager/Platform/Windows/ProjectBuilderWorker_windows.cpp index 075c6de774..b6b37b222d 100644 --- a/Code/Tools/ProjectManager/Platform/Windows/ProjectBuilderWorker_windows.cpp +++ b/Code/Tools/ProjectManager/Platform/Windows/ProjectBuilderWorker_windows.cpp @@ -21,8 +21,7 @@ namespace O3DE::ProjectManager return AZ::Success(QStringList{ ProjectCMakeCommand, "-B", targetBuildPath, "-S", m_projectInfo.m_path, - QString("-DLY_3RDPARTY_PATH=").append(thirdPartyPath), - "-DLY_UNITY_BUILD=ON" } ); + QString("-DLY_3RDPARTY_PATH=").append(thirdPartyPath) } ); } AZ::Outcome ProjectBuilderWorker::ConstructCmakeBuildCommandArguments() const diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index db0ec7e91a..d3ec066be7 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -242,11 +242,15 @@ QTabBar::tab:focus { /************** Project Settings **************/ #projectSettings { - margin-top:42px; + margin-top:30px; +} + +#projectPreviewLabel { + margin: 10px 0 5px 0; } #projectTemplate { - margin: 55px 0 0 50px; + margin: 25px 0 0 50px; } #projectTemplateLabel { font-size:16px; @@ -691,6 +695,12 @@ QProgressBar::chunk { #gemRepoAddDialogInstructionTitleLabel { font-size:14px; + font-weight:bold; +} + +#gemRepoAddDialogWarningLabel { + font-size:12px; + font-style:italic; } #addGemRepoDialog #formFrame { diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp index dec1c9a257..c7df00f423 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.cpp @@ -11,19 +11,28 @@ #include #include #include +#include #include #include #include #include +#include namespace O3DE::ProjectManager { EngineSettingsScreen::EngineSettingsScreen(QWidget* parent) : ScreenWidget(parent) { - auto* layout = new QVBoxLayout(); + QScrollArea* scrollArea = new QScrollArea(this); + scrollArea->setWidgetResizable(true); + + QWidget* scrollWidget = new QWidget(this); + scrollArea->setWidget(scrollWidget); + + QVBoxLayout* layout = new QVBoxLayout(scrollWidget); layout->setAlignment(Qt::AlignTop); + scrollWidget->setLayout(layout); setObjectName("engineSettingsScreen"); @@ -39,9 +48,18 @@ namespace O3DE::ProjectManager 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); + FormLineEditWidget* engineName = new FormLineEditWidget(tr("Engine Name"), engineInfo.m_name, this); + engineName->lineEdit()->setReadOnly(true); + layout->addWidget(engineName); + + FormLineEditWidget* engineVersion = new FormLineEditWidget(tr("Engine Version"), engineInfo.m_version, this); + engineVersion->lineEdit()->setReadOnly(true); + layout->addWidget(engineVersion); + + FormBrowseEditWidget* engineFolder = new FormBrowseEditWidget(tr("Engine Folder"), engineInfo.m_path, this); + engineFolder->lineEdit()->setReadOnly(true); + connect( engineFolder, &FormBrowseEditWidget::OnBrowse, [engineInfo]{ AzQtComponents::ShowFileOnDesktop(engineInfo.m_path); }); + layout->addWidget(engineFolder); m_thirdParty = new FormFolderBrowseEditWidget(tr("3rd Party Software Folder"), engineInfo.m_thirdPartyPath, this); m_thirdParty->lineEdit()->setValidator(new PathValidator(PathValidator::PathMode::ExistingFolder, this)); @@ -71,7 +89,11 @@ namespace O3DE::ProjectManager connect(m_defaultProjectTemplates->lineEdit(), &QLineEdit::textChanged, this, &EngineSettingsScreen::OnTextChanged); layout->addWidget(m_defaultProjectTemplates); - setLayout(layout); + QVBoxLayout* mainLayout = new QVBoxLayout(); + mainLayout->setAlignment(Qt::AlignTop); + mainLayout->setMargin(0); + mainLayout->addWidget(scrollArea); + setLayout(mainLayout); } ProjectManagerScreen EngineSettingsScreen::GetScreenEnum() diff --git a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h index 2f16400405..1efabd4b5e 100644 --- a/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h +++ b/Code/Tools/ProjectManager/Source/EngineSettingsScreen.h @@ -29,7 +29,6 @@ namespace O3DE::ProjectManager void OnTextChanged(); private: - FormLineEditWidget* m_engineVersion; FormBrowseEditWidget* m_thirdParty; FormBrowseEditWidget* m_defaultProjects; FormBrowseEditWidget* m_defaultGems; diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp index 8cd28fcbb4..fe101cf37a 100644 --- a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp +++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.cpp @@ -20,7 +20,8 @@ namespace O3DE::ProjectManager setObjectName("formBrowseEditWidget"); QPushButton* browseButton = new QPushButton(this); - connect(browseButton, &QPushButton::pressed, this, &FormBrowseEditWidget::HandleBrowseButton); + connect( browseButton, &QPushButton::pressed, [this]{ emit OnBrowse(); }); + connect( this, &FormBrowseEditWidget::OnBrowse, this, &FormBrowseEditWidget::HandleBrowseButton); m_frameLayout->addWidget(browseButton); } @@ -34,7 +35,7 @@ namespace O3DE::ProjectManager int key = event->key(); if (key == Qt::Key_Return || key == Qt::Key_Enter) { - HandleBrowseButton(); + emit OnBrowse(); } } diff --git a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h index a1f6948ce9..179fe03253 100644 --- a/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h +++ b/Code/Tools/ProjectManager/Source/FormBrowseEditWidget.h @@ -24,10 +24,13 @@ namespace O3DE::ProjectManager explicit FormBrowseEditWidget(const QString& labelText = "", QWidget* parent = nullptr); ~FormBrowseEditWidget() = default; + signals: + void OnBrowse(); + protected: void keyPressEvent(QKeyEvent* event) override; protected slots: - virtual void HandleBrowseButton() = 0; + virtual void HandleBrowseButton() {}; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 3f219da1a1..98121d7cd2 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -80,7 +80,7 @@ namespace O3DE::ProjectManager void GemCatalogScreen::ReinitForProject(const QString& projectPath) { - m_gemModel->clear(); + m_gemModel->Clear(); m_gemsToRegisterWithProject.clear(); FillModel(projectPath); @@ -145,10 +145,11 @@ namespace O3DE::ProjectManager } } - void GemCatalogScreen::OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies) + void GemCatalogScreen::OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies) { if (m_notificationsEnabled) { + QModelIndex modelIndex = m_gemModel->FindIndexByNameString(gemName); bool added = GemModel::IsAdded(modelIndex); bool dependency = GemModel::IsAddedDependency(modelIndex); @@ -167,6 +168,10 @@ namespace O3DE::ProjectManager { notification += " " + tr("and") + " "; } + if (added && GemModel::GetDownloadStatus(modelIndex) == GemInfo::DownloadStatus::NotDownloaded) + { + m_downloadController->AddGemDownload(GemModel::GetName(modelIndex)); + } } if (numChangedDependencies == 1 ) @@ -229,7 +234,11 @@ namespace O3DE::ProjectManager const QVector allRepoGemInfos = allRepoGemInfosResult.GetValue(); for (const GemInfo& gemInfo : allRepoGemInfos) { - m_gemModel->AddGem(gemInfo); + // do not add gems that have already been downloaded + if (!m_gemModel->FindIndexByNameString(gemInfo.m_name).isValid()) + { + m_gemModel->AddGem(gemInfo); + } } } else @@ -253,7 +262,8 @@ namespace O3DE::ProjectManager GemModel::SetWasPreviouslyAdded(*m_gemModel, modelIndex, true); GemModel::SetIsAdded(*m_gemModel, modelIndex, true); } - else + // ${Name} is a special name used in templates and is not really an error + else if (enabledGemName != "${Name}") { AZ_Warning("ProjectManager::GemCatalog", false, "Cannot find entry for gem with name '%s'. The CMake target name probably does not match the specified name in the gem.json.", diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 1ade87af0c..1b34019d1a 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -46,7 +46,7 @@ namespace O3DE::ProjectManager DownloadController* GetDownloadController() const { return m_downloadController; } public slots: - void OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + void OnGemStatusChanged(const QString& gemName, uint32_t numChangedDependencies); void OnAddGemClicked(); protected: diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp index b425c15dee..4f737d8629 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemFilterWidget.cpp @@ -225,21 +225,22 @@ namespace O3DE::ProjectManager QVector elementNames; QVector elementCounts; const int totalGems = m_gemModel->rowCount(); - const int selectedGemTotal = m_gemModel->TotalAddedGems(); + const int selectedGemTotal = m_gemModel->GatherGemsToBeAdded(/*includeDependencies=*/true).size(); + const int unselectedGemTotal = m_gemModel->GatherGemsToBeRemoved(/*includeDependencies=*/true).size(); const int enabledGemTotal = m_gemModel->TotalAddedGems(/*includeDependencies=*/true); - elementNames.push_back(GemSortFilterProxyModel::GetGemSelectedString(GemSortFilterProxyModel::GemSelected::Unselected)); - elementCounts.push_back(totalGems - selectedGemTotal); - elementNames.push_back(GemSortFilterProxyModel::GetGemSelectedString(GemSortFilterProxyModel::GemSelected::Selected)); elementCounts.push_back(selectedGemTotal); - elementNames.push_back(GemSortFilterProxyModel::GetGemActiveString(GemSortFilterProxyModel::GemActive::Inactive)); - elementCounts.push_back(totalGems - enabledGemTotal); + elementNames.push_back(GemSortFilterProxyModel::GetGemSelectedString(GemSortFilterProxyModel::GemSelected::Unselected)); + elementCounts.push_back(unselectedGemTotal); elementNames.push_back(GemSortFilterProxyModel::GetGemActiveString(GemSortFilterProxyModel::GemActive::Active)); elementCounts.push_back(enabledGemTotal); + elementNames.push_back(GemSortFilterProxyModel::GetGemActiveString(GemSortFilterProxyModel::GemActive::Inactive)); + elementCounts.push_back(totalGems - enabledGemTotal); + bool wasCollapsed = false; if (m_statusFilter) { @@ -262,44 +263,51 @@ namespace O3DE::ProjectManager const QList buttons = m_statusFilter->GetButtonGroup()->buttons(); - QAbstractButton* unselectedButton = buttons[0]; - QAbstractButton* selectedButton = buttons[1]; - unselectedButton->setChecked(m_filterProxyModel->GetGemSelected() == GemSortFilterProxyModel::GemSelected::Unselected); - selectedButton->setChecked(m_filterProxyModel->GetGemSelected() == GemSortFilterProxyModel::GemSelected::Selected); + QAbstractButton* selectedButton = buttons[0]; + QAbstractButton* unselectedButton = buttons[1]; + selectedButton->setChecked(m_filterProxyModel->GetGemSelected() == GemSortFilterProxyModel::GemSelected::Selected); + unselectedButton->setChecked(m_filterProxyModel->GetGemSelected() == GemSortFilterProxyModel::GemSelected::Unselected); auto updateGemSelection = [=]([[maybe_unused]] bool checked) { - if (unselectedButton->isChecked() && !selectedButton->isChecked()) - { - m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::Unselected); - } - else if (!unselectedButton->isChecked() && selectedButton->isChecked()) + if (!unselectedButton->isChecked() && selectedButton->isChecked()) { m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::Selected); } + else if (unselectedButton->isChecked() && !selectedButton->isChecked()) + { + m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::Unselected); + } else { - m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::NoFilter); + if (unselectedButton->isChecked() && selectedButton->isChecked()) + { + m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::Both); + } + else + { + m_filterProxyModel->SetGemSelected(GemSortFilterProxyModel::GemSelected::NoFilter); + } } }; connect(unselectedButton, &QAbstractButton::toggled, this, updateGemSelection); connect(selectedButton, &QAbstractButton::toggled, this, updateGemSelection); - QAbstractButton* inactiveButton = buttons[2]; - QAbstractButton* activeButton = buttons[3]; - inactiveButton->setChecked(m_filterProxyModel->GetGemActive() == GemSortFilterProxyModel::GemActive::Inactive); - activeButton->setChecked(m_filterProxyModel->GetGemActive() == GemSortFilterProxyModel::GemActive::Active); + QAbstractButton* activeButton = buttons[2]; + QAbstractButton* inactiveButton = buttons[3]; + activeButton->setChecked(m_filterProxyModel->GetGemActive() == GemSortFilterProxyModel::GemActive::Active); + inactiveButton->setChecked(m_filterProxyModel->GetGemActive() == GemSortFilterProxyModel::GemActive::Inactive); auto updateGemActive = [=]([[maybe_unused]] bool checked) { - if (inactiveButton->isChecked() && !activeButton->isChecked()) - { - m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::Inactive); - } - else if (!inactiveButton->isChecked() && activeButton->isChecked()) + if (!inactiveButton->isChecked() && activeButton->isChecked()) { m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::Active); } + else if (inactiveButton->isChecked() && !activeButton->isChecked()) + { + m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::Inactive); + } else { m_filterProxyModel->SetGemActive(GemSortFilterProxyModel::GemActive::NoFilter); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index acdef483ae..fb228c0b4a 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -27,6 +27,14 @@ namespace O3DE::ProjectManager void GemModel::AddGem(const GemInfo& gemInfo) { + if (FindIndexByNameString(gemInfo.m_name).isValid()) + { + // do not add gems with duplicate names + // this can happen by mistake or when a gem repo has a gem with the same name as a local gem + AZ_TracePrintf("GemModel", "Ignoring duplicate gem: %s", gemInfo.m_name.toUtf8().constData()); + return; + } + QStandardItem* item = new QStandardItem(); item->setFlags(Qt::ItemIsEnabled | Qt::ItemIsSelectable); @@ -60,6 +68,7 @@ namespace O3DE::ProjectManager void GemModel::Clear() { clear(); + m_nameToIndexMap.clear(); } void GemModel::UpdateGemDependencies() @@ -276,9 +285,11 @@ namespace O3DE::ProjectManager void GemModel::SetIsAdded(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded) { + // get the gemName first, because the modelIndex data change after adding because of filters + QString gemName = modelIndex.data(RoleName).toString(); model.setData(modelIndex, isAdded, RoleIsAdded); - UpdateDependencies(model, modelIndex); + UpdateDependencies(model, gemName, isAdded); } bool GemModel::HasDependentGems(const QModelIndex& modelIndex) const @@ -294,15 +305,17 @@ namespace O3DE::ProjectManager return false; } - void GemModel::UpdateDependencies(QAbstractItemModel& model, const QModelIndex& modelIndex) + void GemModel::UpdateDependencies(QAbstractItemModel& model, const QString& gemName, bool isAdded) { GemModel* gemModel = GetSourceModel(&model); AZ_Assert(gemModel, "Failed to obtain GemModel"); + QModelIndex modelIndex = gemModel->FindIndexByNameString(gemName); + QVector dependencies = gemModel->GatherGemDependencies(modelIndex); uint32_t numChangedDependencies = 0; - if (IsAdded(modelIndex)) + if (isAdded) { for (const QModelIndex& dependency : dependencies) { @@ -324,7 +337,7 @@ namespace O3DE::ProjectManager bool hasDependentGems = gemModel->HasDependentGems(modelIndex); if (IsAddedDependency(modelIndex) != hasDependentGems) { - SetIsAddedDependency(model, modelIndex, hasDependentGems); + SetIsAddedDependency(*gemModel, modelIndex, hasDependentGems); } for (const QModelIndex& dependency : dependencies) @@ -343,7 +356,7 @@ namespace O3DE::ProjectManager } } - gemModel->emit gemStatusChanged(modelIndex, numChangedDependencies); + gemModel->emit gemStatusChanged(gemName, numChangedDependencies); } void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded) diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index 938543eb39..35231cc105 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -64,7 +64,7 @@ namespace O3DE::ProjectManager static bool NeedsToBeAdded(const QModelIndex& modelIndex, bool includeDependencies = false); static bool NeedsToBeRemoved(const QModelIndex& modelIndex, bool includeDependencies = false); static bool HasRequirement(const QModelIndex& modelIndex); - static void UpdateDependencies(QAbstractItemModel& model, const QModelIndex& modelIndex); + static void UpdateDependencies(QAbstractItemModel& model, const QString& gemName, bool isAdded); static void SetDownloadStatus(QAbstractItemModel& model, const QModelIndex& modelIndex, GemInfo::DownloadStatus status); bool DoGemsToBeAddedHaveRequirements() const; @@ -78,7 +78,7 @@ namespace O3DE::ProjectManager int TotalAddedGems(bool includeDependencies = false) const; signals: - void gemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + void gemStatusChanged(const QString& gemName, uint32_t numChangedDependencies); private: void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames); diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp index 199692f200..7ec45ac721 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.cpp @@ -50,11 +50,26 @@ namespace O3DE::ProjectManager } } - // Gem selected - if (m_gemSelectedFilter != GemSelected::NoFilter) + // Gem selected + if (m_gemSelectedFilter == GemSelected::Selected) { - const GemSelected sourceGemStatus = static_cast(GemModel::IsAdded(sourceIndex)); - if (m_gemSelectedFilter != sourceGemStatus) + if (!GemModel::NeedsToBeAdded(sourceIndex, true)) + { + return false; + } + } + // Gem unselected + else if (m_gemSelectedFilter == GemSelected::Unselected) + { + if (!GemModel::NeedsToBeRemoved(sourceIndex, true)) + { + return false; + } + } + // Gem selected or unselected + else if (m_gemSelectedFilter == GemSelected::Both) + { + if (!GemModel::NeedsToBeAdded(sourceIndex, true) && !GemModel::NeedsToBeRemoved(sourceIndex, true)) { return false; } diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h index 74b1e915eb..ab739e62f9 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemSortFilterProxyModel.h @@ -29,7 +29,8 @@ namespace O3DE::ProjectManager { NoFilter = -1, Unselected, - Selected + Selected, + Both }; enum class GemActive { diff --git a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoAddDialog.cpp b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoAddDialog.cpp index 601c62d6e1..05a14b48d3 100644 --- a/Code/Tools/ProjectManager/Source/GemRepo/GemRepoAddDialog.cpp +++ b/Code/Tools/ProjectManager/Source/GemRepo/GemRepoAddDialog.cpp @@ -27,6 +27,7 @@ namespace O3DE::ProjectManager QVBoxLayout* vLayout = new QVBoxLayout(); vLayout->setContentsMargins(30, 30, 25, 10); vLayout->setSpacing(0); + vLayout->setAlignment(Qt::AlignTop); setLayout(vLayout); QLabel* instructionTitleLabel = new QLabel(tr("Enter a valid path to add a new user repository")); @@ -41,9 +42,18 @@ namespace O3DE::ProjectManager vLayout->addWidget(instructionContextLabel); m_repoPath = new FormFolderBrowseEditWidget(tr("Repository Path"), "", this); - m_repoPath->setFixedWidth(600); + m_repoPath->setFixedSize(QSize(600, 100)); vLayout->addWidget(m_repoPath); + vLayout->addSpacing(10); + + QLabel* warningLabel = new QLabel(tr("Online repositories may contain files that could potentially harm your computer," + " please ensure you understand the risks before downloading Gems from third-party sources.")); + warningLabel->setObjectName("gemRepoAddDialogWarningLabel"); + warningLabel->setWordWrap(true); + warningLabel->setAlignment(Qt::AlignLeft); + vLayout->addWidget(warningLabel); + vLayout->addSpacing(40); QDialogButtonBox* dialogButtons = new QDialogButtonBox(); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index 314765def0..fd8fbc970a 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -133,6 +133,11 @@ namespace O3DE::ProjectManager return true; } + else + { + // If we are already on this screen still notify we are on this screen to refresh it + newScreen->NotifyCurrentScreen(); + } } return false; diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp index 6f7f7e1bed..3bfc07c5b0 100644 --- a/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp +++ b/Code/Tools/ProjectManager/Source/UpdateProjectSettingsScreen.cpp @@ -36,6 +36,7 @@ namespace O3DE::ProjectManager QLabel* projectPreviewLabel = new QLabel(tr("Select an image (PNG). Minimum %1 x %2 pixels.") .arg(QString::number(ProjectPreviewImageWidth), QString::number(ProjectPreviewImageHeight))); + projectPreviewLabel->setObjectName("projectPreviewLabel"); previewExtrasLayout->addWidget(projectPreviewLabel); m_projectPreviewImage = new QLabel(this); diff --git a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp index be72de555e..bc147fe7f6 100644 --- a/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp +++ b/Gems/AWSGameLift/Code/AWSGameLiftClient/Tests/AWSGameLiftClientLocalTicketTrackerTest.cpp @@ -108,7 +108,7 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithoutClientSe MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + WaitForProcessFinish([&](){ return matchmakingHandlerMock.m_numMatchError == 1; }); AZ_TEST_STOP_TRACE_SUPPRESSION(1); ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); @@ -122,7 +122,7 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_MultipleCallsWithou AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + WaitForProcessFinish([&](){ return matchmakingHandlerMock.m_numMatchError == 1; }); AZ_TEST_STOP_TRACE_SUPPRESSION(1); ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); @@ -140,7 +140,7 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallButWithFailedOu MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + WaitForProcessFinish([&](){ return matchmakingHandlerMock.m_numMatchError == 1; }); AZ_TEST_STOP_TRACE_SUPPRESSION(1); ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); @@ -160,7 +160,7 @@ TEST_F(AWSGameLiftClientLocalTicketTrackerTest, StartPolling_CallWithMoreThanOne MatchmakingNotificationsHandlerMock matchmakingHandlerMock; AZ_TEST_START_TRACE_SUPPRESSION; m_gameliftClientTicketTracker->StartPolling("ticket1", "player1"); - WaitForProcessFinish([](){ return ::UnitTest::TestRunner::Instance().m_numAssertsFailed == 1; }); + WaitForProcessFinish([&](){ return matchmakingHandlerMock.m_numMatchError == 1; }); AZ_TEST_STOP_TRACE_SUPPRESSION(1); ASSERT_TRUE(matchmakingHandlerMock.m_numMatchError == 1); ASSERT_FALSE(m_gameliftClientTicketTracker->IsTrackerIdle()); diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp index 56f2fdec62..e41c04a0be 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp @@ -23,6 +23,7 @@ #include #include +#include #include #include @@ -91,19 +92,31 @@ namespace AZ m_shaderAssetBuilder.BusConnect(shaderAssetBuilderDescriptor.m_busId); AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, shaderAssetBuilderDescriptor); - // Register Shader Variant Asset Builder - AssetBuilderSDK::AssetBuilderDesc shaderVariantAssetBuilderDescriptor; - shaderVariantAssetBuilderDescriptor.m_name = "Shader Variant Asset Builder"; - // Both "Shader Variant Asset Builder" and "Shader Asset Builder" produce ShaderVariantAsset products. If you update - // ShaderVariantAsset you will need to update BOTH version numbers, not just "Shader Variant Asset Builder". - shaderVariantAssetBuilderDescriptor.m_version = 26; // [AZSL] Changing inlineConstant to rootConstant keyword work. - shaderVariantAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", RPI::ShaderVariantListSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); - shaderVariantAssetBuilderDescriptor.m_busId = azrtti_typeid(); - shaderVariantAssetBuilderDescriptor.m_createJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::CreateJobs, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2); - shaderVariantAssetBuilderDescriptor.m_processJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::ProcessJob, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2); + // If, either the SettingsRegistry doesn't exist, or the property @EnableShaderVariantAssetBuilderRegistryKey is not found, + // the default is to enable the ShaderVariantAssetBuilder. + m_enableShaderVariantAssetBuilder = true; + auto settingsRegistry = AZ::SettingsRegistry::Get(); + if (settingsRegistry) + { + settingsRegistry->Get(m_enableShaderVariantAssetBuilder, EnableShaderVariantAssetBuilderRegistryKey); + } - m_shaderVariantAssetBuilder.BusConnect(shaderVariantAssetBuilderDescriptor.m_busId); - AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, shaderVariantAssetBuilderDescriptor); + if (m_enableShaderVariantAssetBuilder) + { + // Register Shader Variant Asset Builder + AssetBuilderSDK::AssetBuilderDesc shaderVariantAssetBuilderDescriptor; + shaderVariantAssetBuilderDescriptor.m_name = "Shader Variant Asset Builder"; + // Both "Shader Variant Asset Builder" and "Shader Asset Builder" produce ShaderVariantAsset products. If you update + // ShaderVariantAsset you will need to update BOTH version numbers, not just "Shader Variant Asset Builder". + shaderVariantAssetBuilderDescriptor.m_version = 26; // [AZSL] Changing inlineConstant to rootConstant keyword work. + shaderVariantAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern(AZStd::string::format("*.%s", RPI::ShaderVariantListSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); + shaderVariantAssetBuilderDescriptor.m_busId = azrtti_typeid(); + shaderVariantAssetBuilderDescriptor.m_createJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::CreateJobs, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2); + shaderVariantAssetBuilderDescriptor.m_processJobFunction = AZStd::bind(&ShaderVariantAssetBuilder::ProcessJob, &m_shaderVariantAssetBuilder, AZStd::placeholders::_1, AZStd::placeholders::_2); + + m_shaderVariantAssetBuilder.BusConnect(shaderVariantAssetBuilderDescriptor.m_busId); + AssetBuilderSDK::AssetBuilderBus::Broadcast(&AssetBuilderSDK::AssetBuilderBus::Handler::RegisterBuilderInformation, shaderVariantAssetBuilderDescriptor); + } // Register Precompiled Shader Builder AssetBuilderSDK::AssetBuilderDesc precompiledShaderBuilderDescriptor; @@ -121,7 +134,10 @@ namespace AZ void AzslShaderBuilderSystemComponent::Deactivate() { m_shaderAssetBuilder.BusDisconnect(); - m_shaderVariantAssetBuilder.BusDisconnect(); + if (m_enableShaderVariantAssetBuilder) + { + m_shaderVariantAssetBuilder.BusDisconnect(); + } m_precompiledShaderBuilder.BusDisconnect(); RHI::ShaderPlatformInterfaceRegisterBus::Handler::BusDisconnect(); diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.h b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.h index 9ff5bd8282..f502e4c329 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.h +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.h @@ -61,7 +61,17 @@ namespace AZ private: ShaderAssetBuilder m_shaderAssetBuilder; + + // The ShaderVariantAssetBuilder can be disabled with this registry key. + // By default it is enabled. A user might want to disable it when doing look development + // work with shaders or doing lots of iterative changes to shaders. In these cases + // GPU performance doesn't matter at all so it is important to not waste time + // building ShaderVariantAssets (Other than the Root ShaderVariantAsset, of course.). + static constexpr char EnableShaderVariantAssetBuilderRegistryKey[] = "/O3DE/Atom/Shaders/BuildVariants"; + bool m_enableShaderVariantAssetBuilder = true; + ShaderVariantAssetBuilder m_shaderVariantAssetBuilder; + PrecompiledShaderBuilder m_precompiledShaderBuilder; /// Contains the ShaderPlatformInterface for all registered RHIs diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp index a0205efa72..89e202a4bc 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderAssetBuilder.cpp @@ -477,8 +477,8 @@ namespace AZ preprocessorOptions.m_predefinedMacros.end(), macroDefinitionsToAdd.begin(), macroDefinitionsToAdd.end()); // Run the preprocessor. PreprocessorData output; - PreprocessFile(prependedAzslFilePath, output, preprocessorOptions, true, true); - RHI::ReportErrorMessages(ShaderAssetBuilderName, output.diagnostics); + const bool preprocessorSuccess = PreprocessFile(prependedAzslFilePath, output, preprocessorOptions, true, true); + RHI::ReportMessages(ShaderAssetBuilderName, output.diagnostics, !preprocessorSuccess); // Dump the preprocessed string as a flat AZSL file with extension .azslin, which will be given to AZSLc to generate the HLSL file. AZStd::string superVariantAzslinStemName = shaderFileName; if (!supervariantInfo.m_name.IsEmpty()) diff --git a/Gems/Atom/Asset/Shader/Registry/atom_shaders.setreg b/Gems/Atom/Asset/Shader/Registry/atom_shaders.setreg new file mode 100644 index 0000000000..31d108f47a --- /dev/null +++ b/Gems/Atom/Asset/Shader/Registry/atom_shaders.setreg @@ -0,0 +1,10 @@ +{ + "O3DE": { + "Atom": { + "Shaders": { + "BuildVariants": true + } + } + } + } +} diff --git a/Gems/Atom/Bootstrap/Code/Source/BootstrapSystemComponent.cpp b/Gems/Atom/Bootstrap/Code/Source/BootstrapSystemComponent.cpp index 8f2ff264e5..3f5761270a 100644 --- a/Gems/Atom/Bootstrap/Code/Source/BootstrapSystemComponent.cpp +++ b/Gems/Atom/Bootstrap/Code/Source/BootstrapSystemComponent.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -115,7 +116,9 @@ namespace AZ { // GFX TODO - investigate window creation being part of the GameApplication. - m_nativeWindow = AZStd::make_unique("O3DELauncher", AzFramework::WindowGeometry(0, 0, 1920, 1080)); + auto projectTitle = AZ::Utils::GetProjectName(); + + m_nativeWindow = AZStd::make_unique(projectTitle.c_str(), AzFramework::WindowGeometry(0, 0, 1920, 1080)); AZ_Assert(m_nativeWindow, "Failed to create the game window\n"); m_nativeWindow->Activate(); diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype index f4bcfb2673..d36213694b 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype @@ -1,6 +1,14 @@ { "description": "Material Type with properties used to define Enhanced PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model, with advanced features like subsurface scattering, transmission, and anisotropy.", - "version": 3, + "version": 4, + "versionUpdates": [ + { + "toVersion": 4, + "actions": [ + {"op": "rename", "from": "opacity.doubleSided", "to": "general.doubleSided"} + ] + } + ], "propertyLayout": { "groups": [ { @@ -92,6 +100,12 @@ ], "properties": { "general": [ + { + "name": "doubleSided", + "displayName": "Double-sided", + "description": "Whether to render back-faces or just front-faces.", + "type": "Bool" + }, { "name": "applySpecularAA", "displayName": "Apply Specular AA", @@ -709,12 +723,6 @@ "name": "m_opacityFactor" } }, - { - "name": "doubleSided", - "displayName": "Double-sided", - "description": "Whether to render back-faces or just front-faces.", - "type": "Bool" - }, { "name": "alphaAffectsSpecular", "displayName": "Alpha affects specular", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index 7527a7658a..e0b1949058 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -1,6 +1,14 @@ { "description": "Material Type with properties used to define Standard PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model.", - "version": 3, + "version": 4, + "versionUpdates": [ + { + "toVersion": 4, + "actions": [ + {"op": "rename", "from": "opacity.doubleSided", "to": "general.doubleSided"} + ] + } + ], "propertyLayout": { "groups": [ { @@ -72,6 +80,12 @@ ], "properties": { "general": [ + { + "name": "doubleSided", + "displayName": "Double-sided", + "description": "Whether to render back-faces or just front-faces.", + "type": "Bool" + }, { "name": "applySpecularAA", "displayName": "Apply Specular AA", @@ -650,12 +664,6 @@ "name": "m_opacityFactor" } }, - { - "name": "doubleSided", - "displayName": "Double-sided", - "description": "Whether to render back-faces or just front-faces.", - "type": "Bool" - }, { "name": "alphaAffectsSpecular", "displayName": "Alpha affects specular", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityDoubleSided.lua b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityDoubleSided.lua index 2382f3f0f0..9584698532 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityDoubleSided.lua +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityDoubleSided.lua @@ -10,14 +10,14 @@ ---------------------------------------------------------------------------------------------------- function GetMaterialPropertyDependencies() - return {"opacity.doubleSided"} + return {"general.doubleSided"} end ForwardPassIndex = 0 ForwardPassEdsIndex = 1 function Process(context) - local doubleSided = context:GetMaterialPropertyValue_bool("opacity.doubleSided") + local doubleSided = context:GetMaterialPropertyValue_bool("general.doubleSided") local lastShader = context:GetShaderCount() - 1; if(doubleSided) then diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityMode.lua b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityMode.lua index c86fbfe0b7..9315131e44 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityMode.lua +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_HandleOpacityMode.lua @@ -81,7 +81,6 @@ function ProcessEditor(context) context:SetMaterialPropertyVisibility("opacity.textureMap", mainVisibility) context:SetMaterialPropertyVisibility("opacity.textureMapUv", mainVisibility) context:SetMaterialPropertyVisibility("opacity.factor", mainVisibility) - context:SetMaterialPropertyVisibility("opacity.doubleSided", mainVisibility) if(opacityMode == OpacityMode_Blended or opacityMode == OpacityMode_TintedTransparent) then context:SetMaterialPropertyVisibility("opacity.alphaAffectsSpecular", MaterialPropertyVisibility_Enabled) diff --git a/Gems/Atom/Feature/Common/Assets/Shaders/ScreenSpace/DeferredFog.shader b/Gems/Atom/Feature/Common/Assets/Shaders/ScreenSpace/DeferredFog.shader index 5e2ca9bff5..06c991400e 100644 --- a/Gems/Atom/Feature/Common/Assets/Shaders/ScreenSpace/DeferredFog.shader +++ b/Gems/Atom/Feature/Common/Assets/Shaders/ScreenSpace/DeferredFog.shader @@ -23,8 +23,8 @@ "DrawList" : "forward", "CompilerHints" : { - "DxcDisableOptimizations" : false, - "DxcGenerateDebugInfo" : false + "DisableOptimizations" : false, + "GenerateDebugInfo" : false }, "ProgramSettings": diff --git a/Gems/Atom/Feature/Common/Assets/Textures/Default/checker_basecolor.tif b/Gems/Atom/Feature/Common/Assets/Textures/Default/checker_basecolor.tif new file mode 100644 index 0000000000..5abe5bbd49 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Textures/Default/checker_basecolor.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:57d6744696768f9fb8a5fe5fee9aa36fee1eb87a9dbc1e60d4a35ed3c39d68e6 +size 810620 diff --git a/Gems/Atom/Feature/Common/Assets/Textures/Default/checker_uv_basecolor.png b/Gems/Atom/Feature/Common/Assets/Textures/Default/checker_uv_basecolor.png new file mode 100644 index 0000000000..07e240baf9 --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/Textures/Default/checker_uv_basecolor.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:513f47f6fea5105f603170a8881b7e3b1cd2c4258636d64a6399c725032b500d +size 38689 diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignment.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignment.h index 40555bae00..21d9ec1bba 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignment.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Material/MaterialAssignment.h @@ -78,5 +78,10 @@ namespace AZ //! Find an assignment id corresponding to the lod and label substring filters MaterialAssignmentId FindMaterialAssignmentIdInModel( const Data::Instance& model, const MaterialAssignmentLodIndex lodFilter, const AZStd::string& labelFilter); + + //! Special case handling to convert script values to supported types + AZ::RPI::MaterialPropertyValue ConvertMaterialPropertyValueFromScript( + const AZ::RPI::MaterialPropertyDescriptor* propertyDescriptor, const AZStd::any& value); + } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp index ed2d8a1d9f..a9bb7271ab 100644 --- a/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/FrameCaptureSystemComponent.cpp @@ -12,7 +12,7 @@ #include #include -#include +#include #include #include diff --git a/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp b/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp index c79ceb39ba..d99ce211bf 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Material/MaterialAssignment.cpp @@ -143,28 +143,34 @@ namespace AZ { bool wasRenamed = false; Name newName; - RPI::MaterialPropertyIndex materialPropertyIndex = m_materialInstance->FindPropertyIndex(propertyPair.first, &wasRenamed, &newName); + RPI::MaterialPropertyIndex materialPropertyIndex = + m_materialInstance->FindPropertyIndex(propertyPair.first, &wasRenamed, &newName); - // FindPropertyIndex will have already reported a message about what the old and new names are. Here we just add some extra info to help the user resolve it. - AZ_Warning("MaterialAssignment", !wasRenamed, + // FindPropertyIndex will have already reported a message about what the old and new names are. Here we just add + // some extra info to help the user resolve it. + AZ_Warning( + "MaterialAssignment", !wasRenamed, "Consider running \"Apply Automatic Property Updates\" to use the latest property names.", - propertyPair.first.GetCStr(), - newName.GetCStr()); + propertyPair.first.GetCStr(), newName.GetCStr()); if (wasRenamed && m_propertyOverrides.find(newName) != m_propertyOverrides.end()) { materialPropertyIndex.Reset(); - - AZ_Warning("MaterialAssignment", false, - "Material property '%s' has been renamed to '%s', and a property override exists for both. The one with the old name will be ignored.", - propertyPair.first.GetCStr(), - newName.GetCStr()); + + AZ_Warning( + "MaterialAssignment", false, + "Material property '%s' has been renamed to '%s', and a property override exists for both. The one with " + "the old name will be ignored.", + propertyPair.first.GetCStr(), newName.GetCStr()); } if (!materialPropertyIndex.IsNull()) { + const auto propertyDescriptor = + m_materialInstance->GetMaterialPropertiesLayout()->GetPropertyDescriptor(materialPropertyIndex); + m_materialInstance->SetPropertyValue( - materialPropertyIndex, AZ::RPI::MaterialPropertyValue::FromAny(propertyPair.second)); + materialPropertyIndex, ConvertMaterialPropertyValueFromScript(propertyDescriptor, propertyPair.second)); } } } @@ -284,5 +290,58 @@ namespace AZ return MaterialAssignmentId(); } + + template + AZ::RPI::MaterialPropertyValue ConvertMaterialPropertyValueNumericType(const AZStd::any& value) + { + if (value.is()) + { + return aznumeric_cast(AZStd::any_cast(value)); + } + if (value.is()) + { + return aznumeric_cast(AZStd::any_cast(value)); + } + if (value.is()) + { + return aznumeric_cast(AZStd::any_cast(value)); + } + if (value.is()) + { + return aznumeric_cast(AZStd::any_cast(value)); + } + + return AZ::RPI::MaterialPropertyValue::FromAny(value); + } + + AZ::RPI::MaterialPropertyValue ConvertMaterialPropertyValueFromScript( + const AZ::RPI::MaterialPropertyDescriptor* propertyDescriptor, const AZStd::any& value) + { + switch (propertyDescriptor->GetDataType()) + { + case AZ::RPI::MaterialPropertyDataType::Enum: + if (value.is()) + { + return propertyDescriptor->GetEnumValue(AZStd::any_cast(value)); + } + if (value.is()) + { + return propertyDescriptor->GetEnumValue(AZ::Name(AZStd::any_cast(value))); + } + return ConvertMaterialPropertyValueNumericType(value); + case AZ::RPI::MaterialPropertyDataType::Int: + return ConvertMaterialPropertyValueNumericType(value); + case AZ::RPI::MaterialPropertyDataType::UInt: + return ConvertMaterialPropertyValueNumericType(value); + case AZ::RPI::MaterialPropertyDataType::Float: + return ConvertMaterialPropertyValueNumericType(value); + case AZ::RPI::MaterialPropertyDataType::Bool: + return ConvertMaterialPropertyValueNumericType(value); + default: + break; + } + + return AZ::RPI::MaterialPropertyValue::FromAny(value); + } } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Source/Platform/Windows/platform_windows.cmake b/Gems/Atom/Feature/Common/Code/Source/Platform/Windows/platform_windows.cmake index 7c594fc945..7a325ca97e 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Platform/Windows/platform_windows.cmake +++ b/Gems/Atom/Feature/Common/Code/Source/Platform/Windows/platform_windows.cmake @@ -5,12 +5,3 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # # - -if(LY_MONOLITHIC_GAME) # Do not use OpenImageIO in monolithic game - return() -endif() - -set(LY_BUILD_DEPENDENCIES - PRIVATE - 3rdParty::ilmbase -) diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/ShaderCompilerArguments.h b/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/ShaderCompilerArguments.h index d0960a7fde..83a868ab65 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/ShaderCompilerArguments.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/ShaderCompilerArguments.h @@ -57,12 +57,19 @@ namespace AZ AZStd::string m_azslcAdditionalFreeArguments; // note: if you add new sort of arguments here, don't forget to update HasDifferentAzslcArguments() - //! DXC - bool m_dxcDisableWarnings = false; - bool m_dxcWarningAsError = false; - bool m_dxcDisableOptimizations = false; - bool m_dxcGenerateDebugInfo = false; - uint8_t m_dxcOptimizationLevel = LevelUnset; + //! Remark: To the user, the following parameters are exposed without the + //! "Dxc" prefix because these are common options for the "main" compiler + //! for the given RHI. At the moment the only "main" compiler is Dxc, but in + //! the future AZSLc may transpile from AZSL to some other proprietary language + //! and in that case the "main" compiler won't be DXC + bool m_disableWarnings = false; + bool m_warningAsError = false; + bool m_disableOptimizations = false; + bool m_generateDebugInfo = false; + uint8_t m_optimizationLevel = LevelUnset; + //! "DxcAdditionalFreeArguments" keeps the "Dxc" prefix because these arguments + //! are specific to DXC, and it will be relevant only if DXC is the "main" compiler + //! for a given RHI, otherwise this parameter won't matter. AZStd::string m_dxcAdditionalFreeArguments; //! both diff --git a/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/Utils.h b/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/Utils.h index 0624dce5f8..d648f05b46 100644 --- a/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/Utils.h +++ b/Gems/Atom/RHI/Code/Include/Atom/RHI.Edit/Utils.h @@ -83,11 +83,12 @@ namespace AZ const AZStd::string& shaderSourcePathForDebug, const char* toolNameForLog); - //! Reports error messages to AZ_Error and/or AZ_Warning, given a text blob that potentially contains many lines of errors and warnings. - //! @param window Debug window name used for AZ Trace functions - //! @param errorMessages String that may contain many lines of errors and warnings - //! @param return true if Errors were detected and reported (Warnings don't count) - bool ReportErrorMessages(AZStd::string_view window, AZStd::string_view errorMessages); + //! Reports messages with AZ_Error or AZ_Warning (See @reportAsErrors). + //! @param window Debug window name used for AZ Trace functions. + //! @param errorMessages Message string. + //! @param reportAsErrors If true, messages are traced with AZ_Error, otherwise AZ_Warning is used. + //! @returns true If the input text blob contains at least one line with the "error" string. + bool ReportMessages(AZStd::string_view window, AZStd::string_view errorMessages, bool reportAsErrors); //! Converts from a RHI::ShaderHardwareStage to an RHI::ShaderStage ShaderStage ToRHIShaderStage(ShaderHardwareStage stageType); diff --git a/Gems/Atom/RHI/Code/Source/RHI.Edit/ShaderCompilerArguments.cpp b/Gems/Atom/RHI/Code/Source/RHI.Edit/ShaderCompilerArguments.cpp index ef330e7902..5411e6655e 100644 --- a/Gems/Atom/RHI/Code/Source/RHI.Edit/ShaderCompilerArguments.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI.Edit/ShaderCompilerArguments.cpp @@ -33,17 +33,17 @@ namespace AZ RegisterEnumerators(serializeContext); serializeContext->Class() - ->Version(2) + ->Version(3) ->Field("AzslcWarningLevel", &ShaderCompilerArguments::m_azslcWarningLevel) ->Field("AzslcWarningAsError", &ShaderCompilerArguments::m_azslcWarningAsError) ->Field("AzslcAdditionalFreeArguments", &ShaderCompilerArguments::m_azslcAdditionalFreeArguments) - ->Field("DxcDisableWarnings", &ShaderCompilerArguments::m_dxcDisableWarnings) - ->Field("DxcWarningAsError", &ShaderCompilerArguments::m_dxcWarningAsError) - ->Field("DxcDisableOptimizations", &ShaderCompilerArguments::m_dxcDisableOptimizations) - ->Field("DxcGenerateDebugInfo", &ShaderCompilerArguments::m_dxcGenerateDebugInfo) - ->Field("DxcOptimizationLevel", &ShaderCompilerArguments::m_dxcOptimizationLevel) - ->Field("DxcAdditionalFreeArguments", &ShaderCompilerArguments::m_dxcAdditionalFreeArguments) + ->Field("DisableWarnings", &ShaderCompilerArguments::m_disableWarnings) + ->Field("WarningAsError", &ShaderCompilerArguments::m_warningAsError) + ->Field("DisableOptimizations", &ShaderCompilerArguments::m_disableOptimizations) + ->Field("GenerateDebugInfo", &ShaderCompilerArguments::m_generateDebugInfo) + ->Field("OptimizationLevel", &ShaderCompilerArguments::m_optimizationLevel) ->Field("DefaultMatrixOrder", &ShaderCompilerArguments::m_defaultMatrixOrder) + ->Field("DxcAdditionalFreeArguments", &ShaderCompilerArguments::m_dxcAdditionalFreeArguments) ; } } @@ -62,13 +62,13 @@ namespace AZ } m_azslcWarningAsError = m_azslcWarningAsError || right.m_azslcWarningAsError; m_azslcAdditionalFreeArguments = CommandLineArgumentUtils::MergeCommandLineArguments(m_azslcAdditionalFreeArguments, right.m_azslcAdditionalFreeArguments); - m_dxcDisableWarnings = m_dxcDisableWarnings || right.m_dxcDisableWarnings; - m_dxcWarningAsError = m_dxcWarningAsError || right.m_dxcWarningAsError; - m_dxcDisableOptimizations = m_dxcDisableOptimizations || right.m_dxcDisableOptimizations; - m_dxcGenerateDebugInfo = m_dxcGenerateDebugInfo || right.m_dxcGenerateDebugInfo; - if (right.m_dxcOptimizationLevel != LevelUnset) + m_disableWarnings = m_disableWarnings || right.m_disableWarnings; + m_warningAsError = m_warningAsError || right.m_warningAsError; + m_disableOptimizations = m_disableOptimizations || right.m_disableOptimizations; + m_generateDebugInfo = m_generateDebugInfo || right.m_generateDebugInfo; + if (right.m_optimizationLevel != LevelUnset) { - m_dxcOptimizationLevel = right.m_dxcOptimizationLevel; + m_optimizationLevel = right.m_optimizationLevel; } m_dxcAdditionalFreeArguments = CommandLineArgumentUtils::MergeCommandLineArguments(m_dxcAdditionalFreeArguments, right.m_dxcAdditionalFreeArguments); if (right.m_defaultMatrixOrder != MatrixOrder::Default) @@ -131,21 +131,21 @@ namespace AZ AZStd::string ShaderCompilerArguments::MakeAdditionalDxcCommandLineString() const { AZStd::string arguments; - if (m_dxcDisableWarnings) + if (m_disableWarnings) { arguments += " -no-warnings"; } - else if (m_dxcWarningAsError) + else if (m_warningAsError) { arguments += " -WX"; } - if (m_dxcDisableOptimizations) + if (m_disableOptimizations) { arguments += " -Od"; } - else if (m_dxcOptimizationLevel <= 3) + else if (m_optimizationLevel <= 3) { - arguments = " -O" + AZStd::to_string(m_dxcOptimizationLevel); + arguments = " -O" + AZStd::to_string(m_optimizationLevel); } if (m_defaultMatrixOrder == MatrixOrder::Column) { diff --git a/Gems/Atom/RHI/Code/Source/RHI.Edit/Utils.cpp b/Gems/Atom/RHI/Code/Source/RHI.Edit/Utils.cpp index b4c68b3f75..dc203ef731 100644 --- a/Gems/Atom/RHI/Code/Source/RHI.Edit/Utils.cpp +++ b/Gems/Atom/RHI/Code/Source/RHI.Edit/Utils.cpp @@ -330,7 +330,7 @@ namespace AZ // Pump one last time to make sure the streams have been flushed pumpOuputStreams(); - const bool reportedErrors = ReportErrorMessages(toolNameForLog, errorMessages); + const bool reportedErrors = ReportMessages(toolNameForLog, errorMessages, exitCode != 0); if (timedOut) { @@ -367,32 +367,20 @@ namespace AZ return true; } - bool ReportErrorMessages([[maybe_unused]] AZStd::string_view window, AZStd::string_view errorMessages) + bool ReportMessages([[maybe_unused]] AZStd::string_view window, AZStd::string_view errorMessages, bool reportAsErrors) { - // There are more efficient ways to do this, but this approach is simple and gets us moving for now. - AZStd::vector lines; - AzFramework::StringFunc::Tokenize(errorMessages.data(), lines, "\n\r"); - - bool foundErrors = false; - - for (auto& line : lines) + if (reportAsErrors) { - if (AZStd::string::npos != AzFramework::StringFunc::Find(line, "error")) - { - AZ_Error(window.data(), false, "%s", line.data()); - foundErrors = true; - } - else if (AZStd::string::npos != AzFramework::StringFunc::Find(line, "warning")) - { - AZ_Warning(window.data(), false, "%s", line.data()); - } - else - { - AZ_TracePrintf(window.data(), "%s", line.data()); - } + AZ_Error(window.data(), false, "%.*s", aznumeric_cast(errorMessages.size()), errorMessages.data()); } - - return foundErrors; + else + { + // Using AZ_Warning instead of AZ_TracePrintf because this function is commonly + // used to report messages from stderr when executing applications. Applications + // when ran successfully, only output to stderr for errors or warnings. + AZ_Warning(window.data(), false, "%.*s", aznumeric_cast(errorMessages.size()), errorMessages.data()); + } + return AZStd::string::npos != AzFramework::StringFunc::Find(errorMessages, "error"); } ShaderStage ToRHIShaderStage(ShaderHardwareStage stageType) diff --git a/Gems/Atom/RHI/DX12/Code/CMakeLists.txt b/Gems/Atom/RHI/DX12/Code/CMakeLists.txt index b913ad58bf..975b838ffa 100644 --- a/Gems/Atom/RHI/DX12/Code/CMakeLists.txt +++ b/Gems/Atom/RHI/DX12/Code/CMakeLists.txt @@ -107,7 +107,7 @@ ly_add_target( Gem::Atom_RHI.Reflect Gem::Atom_RHI_DX12.Reflect 3rdParty::d3dx12 - ${AFTERMATH_BUILD_DEPENDENCY} + ${AFTERMATH_BUILD_DEPENDENCY} COMPILE_DEFINITIONS PRIVATE ${USE_NSIGHT_AFTERMATH_DEFINE} @@ -128,7 +128,6 @@ ly_add_target( BUILD_DEPENDENCIES PRIVATE AZ::AzCore - Gem::Atom_RHI.Reflect Gem::Atom_RHI.Public Gem::Atom_RHI_DX12.Reflect diff --git a/Gems/Atom/RHI/DX12/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp b/Gems/Atom/RHI/DX12/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp index f30fc72ace..ee293292e1 100644 --- a/Gems/Atom/RHI/DX12/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp +++ b/Gems/Atom/RHI/DX12/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp @@ -114,7 +114,7 @@ namespace AZ } } - if (shaderCompilerArguments.m_dxcDisableOptimizations) + if (shaderCompilerArguments.m_disableOptimizations) { // When optimizations are disabled (-Od), all resources declared in the source file are available to all stages // (when enabled only the resources which are referenced in a stage are bound to the stage) @@ -195,7 +195,7 @@ namespace AZ bool ShaderPlatformInterface::BuildHasDebugInfo(const RHI::ShaderCompilerArguments& shaderCompilerArguments) const { - return shaderCompilerArguments.m_dxcGenerateDebugInfo; + return shaderCompilerArguments.m_generateDebugInfo; } const char* ShaderPlatformInterface::GetAzslHeader(const AssetBuilderSDK::PlatformInfo& platform) const diff --git a/Gems/Atom/RHI/Metal/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp b/Gems/Atom/RHI/Metal/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp index 7e41e50892..d43d88a7e1 100644 --- a/Gems/Atom/RHI/Metal/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp +++ b/Gems/Atom/RHI/Metal/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp @@ -167,7 +167,7 @@ namespace AZ bool ShaderPlatformInterface::BuildHasDebugInfo(const RHI::ShaderCompilerArguments& shaderCompilerArguments) const { - return shaderCompilerArguments.m_dxcGenerateDebugInfo; + return shaderCompilerArguments.m_generateDebugInfo; } const char* ShaderPlatformInterface::GetAzslHeader(const AssetBuilderSDK::PlatformInfo& platform) const diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp index 8b99b7b510..c5f1060ca3 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI.Builders/ShaderPlatformInterface.cpp @@ -109,7 +109,7 @@ namespace AZ bool ShaderPlatformInterface::BuildHasDebugInfo(const RHI::ShaderCompilerArguments& shaderCompilerArguments) const { - return shaderCompilerArguments.m_dxcGenerateDebugInfo; + return shaderCompilerArguments.m_generateDebugInfo; } const char* ShaderPlatformInterface::GetAzslHeader(const AssetBuilderSDK::PlatformInfo& platform) const diff --git a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandList.cpp b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandList.cpp index 2620aa5f7b..ffa02ac3c4 100644 --- a/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandList.cpp +++ b/Gems/Atom/RHI/Vulkan/Code/Source/RHI/CommandList.cpp @@ -729,8 +729,7 @@ namespace AZ void CommandList::SetStencilRef(uint8_t stencilRef) { - vkCmdSetStencilReference(m_nativeCommandBuffer, VK_STENCIL_FACE_FRONT_BIT, static_cast(stencilRef)); - vkCmdSetStencilReference(m_nativeCommandBuffer, VK_STENCIL_FACE_BACK_BIT, static_cast(stencilRef)); + vkCmdSetStencilReference(m_nativeCommandBuffer, VK_STENCIL_FACE_FRONT_AND_BACK, aznumeric_cast(stencilRef)); } void CommandList::BindPipeline(const PipelineState* pipelineState) diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h index e7b9825ecb..be7ac9e7a4 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Pass.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,7 @@ namespace AZ struct PassRequest; struct PassValidationResults; class AttachmentReadback; + class ImageAttachmentCopy; using SortedPipelineViewTags = AZStd::set; using PassesByDrawList = AZStd::map; @@ -94,6 +96,8 @@ namespace AZ { AZ_RPI_PASS(Pass); + friend class ImageAttachmentPreviewPass; + public: using ChildPassIndex = RHI::Handle; @@ -369,6 +373,9 @@ namespace AZ void UpdateReadbackAttachment(FramePrepareParams params, bool beforeAddScopes); + // Setup ImageAttachmentCopy + void UpdateAttachmentCopy(FramePrepareParams params); + // --- Protected Members --- const Name PassNameThis{"This"}; @@ -466,6 +473,9 @@ namespace AZ AZStd::shared_ptr m_attachmentReadback; PassAttachmentReadbackOption m_readbackOption; + // For image attachment preview + AZStd::weak_ptr m_attachmentCopy; + private: // Return the Timestamp result of this pass virtual TimestampResult GetTimestampResultInternal() const; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassLibrary.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassLibrary.h index 66c3c205ab..f79da54636 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassLibrary.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassLibrary.h @@ -77,6 +77,16 @@ namespace AZ const AZStd::shared_ptr GetPassTemplate(const Name& name) const; const AZStd::vector& GetPassesForTemplate(const Name& templateName) const; + //! Removes a PassTemplate by name, only if the following two conditions are met: + //! 1- The template was NOT created from an Asset. This means the template will be erasable + //! only if it was created at runtime with C++. + //! 2- The are no instantiated Passes referencing such template. + //! If the template exists but both conditions are not met then the function will assert. + //! If a template with the given name doesn't exist the function does nothing. + //! This function should be used judiciously, and under rare circumstances. For example, + //! Applications that iteratively create and need to delete templates at runtime. + void RemovePassTemplate(const Name& name); + //! Removes a pass from both it's associated template (if it has one) and from the pass name mapping void RemovePassFromLibrary(Pass* pass); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystem.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystem.h index 8390b0f7e2..6412bf6166 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystem.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystem.h @@ -94,6 +94,7 @@ namespace AZ bool HasPassesForTemplateName(const Name& templateName) const override; bool AddPassTemplate(const Name& name, const AZStd::shared_ptr& passTemplate) override; const AZStd::shared_ptr GetPassTemplate(const Name& name) const override; + void RemovePassTemplate(const Name& name) override; void RemovePassFromLibrary(Pass* pass) override; void RegisterPass(Pass* pass) override; void UnregisterPass(Pass* pass) override; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystemInterface.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystemInterface.h index 7f944df88f..b91a31cfd5 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystemInterface.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/PassSystemInterface.h @@ -199,6 +199,9 @@ namespace AZ //! Retrieves a PassTemplate from the library virtual const AZStd::shared_ptr GetPassTemplate(const Name& name) const = 0; + //! See remarks in PassLibrary.h for the function with this name. + virtual void RemovePassTemplate(const Name& name) = 0; + //! Removes all references to the given pass from the pass library virtual void RemovePassFromLibrary(Pass* pass) = 0; 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 5fb90d044e..ec51e34897 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 @@ -13,9 +13,7 @@ #include #include -#include #include -#include #include namespace AZ @@ -29,7 +27,6 @@ namespace AZ namespace RPI { - class ImageAttachmentCopy; class RenderPass; class Query; @@ -41,8 +38,6 @@ namespace AZ { AZ_RPI_PASS(RenderPass); - friend class ImageAttachmentPreviewPass; - using ScopeQuery = AZStd::array, static_cast(ScopeQueryType::Count)>; public: @@ -143,8 +138,6 @@ namespace AZ // Readback the results from the ScopeQueries void ReadbackScopeQueryResults(); - AZStd::weak_ptr m_attachmentCopy; - // Readback results from the Timestamp queries TimestampResult m_timestampResult; // Readback results from the PipelineStatistics queries diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.h index 2e2b14a699..8ee7a44b6f 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.h @@ -78,7 +78,7 @@ namespace AZ ~ImageAttachmentPreviewPass(); //! Preview the PassAttachment of a pass' PassAttachmentBinding - void PreviewImageAttachmentForPass(RenderPass* pass, const PassAttachment* passAttachment); + void PreviewImageAttachmentForPass(Pass* pass, const PassAttachment* passAttachment); //! Set the output color attachment for this pass void SetOutputColorAttachment(RHI::Ptr outputImageAttachment); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h index 085256f6d8..76bb2a6113 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialPropertyDescriptor.h @@ -106,6 +106,9 @@ namespace AZ static constexpr uint32_t InvalidEnumValue = std::numeric_limits::max(); uint32_t GetEnumValue(const AZ::Name& enumName) const; + //! Returns the name of the enum from its index. An empty name is returned for an invalid id. + const AZ::Name& GetEnumName(uint32_t enumValue) const; + //! Returns the unique name ID of this property const Name& GetName() const; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Pass/PassBuilder.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Pass/PassBuilder.cpp index d5e243c687..6a0b10633e 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Pass/PassBuilder.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Pass/PassBuilder.cpp @@ -10,8 +10,8 @@ #include #include - #include +#include #include #include @@ -33,11 +33,27 @@ namespace AZ static const char* PassAssetExtension = "pass"; } + namespace PassBuilderNamespace + { + enum PassDependencies + { + Shader, + AttachmentImage, + Count + }; + + static const AZStd::tuple DependencyExtensionJobKeyTable[PassDependencies::Count] = + { + {".shader", "Shader Asset"}, + {".attimage", "Any Asset Builder"} + }; + } + void PassBuilder::RegisterBuilder() { AssetBuilderSDK::AssetBuilderDesc builder; builder.m_name = PassBuilderJobKey; - builder.m_version = 13; // antonmic: making .pass files declare dependency on shaders they reference + builder.m_version = 14; // making .pass files emit product dependencies for the shaders they reference so they are picked up by the asset bundler builder.m_busId = azrtti_typeid(); builder.m_createJobFunction = AZStd::bind(&PassBuilder::CreateJobs, this, AZStd::placeholders::_1, AZStd::placeholders::_2); builder.m_processJobFunction = AZStd::bind(&PassBuilder::ProcessJob, this, AZStd::placeholders::_1, AZStd::placeholders::_2); @@ -104,8 +120,27 @@ namespace AZ } } + bool SetJobKeyForExtension(const AZStd::string& filePath, FindPassReferenceAssetParams& params) + { + AZStd::string extension; + StringFunc::Path::GetExtension(filePath.c_str(), extension); + for (const auto& [dependencyExtension, jobKey] : PassBuilderNamespace::DependencyExtensionJobKeyTable) + { + if (extension == dependencyExtension) + { + params.jobKey = jobKey; + return true; + } + } + + AZ_Error(PassBuilderName, false, "PassBuilder found a dependency with extension '%s', but does not know the corresponding job key. Add the job key for that extension to SetJobKeyForExtension in PassBuilder.cpp", extension.c_str()); + params.jobKey = "Unknown"; + return false; + } + // Helper function to find all assetId's and object references - bool FindReferencedAssets(FindPassReferenceAssetParams& params, AssetBuilderSDK::JobDescriptor* job) + bool FindReferencedAssets( + FindPassReferenceAssetParams& params, AssetBuilderSDK::JobDescriptor* job, AZStd::vector* productDependencies) { SerializeContext::ErrorHandler errorLogger; errorLogger.Reset(); @@ -129,8 +164,8 @@ namespace AZ if (job != nullptr) // Create Job Phase { params.dependencySourceFile = path; - bool dependencyAddedSuccessfully = AddDependency(params, job); - success = dependencyAddedSuccessfully && success; + success &= SetJobKeyForExtension(path, params); + success &= AddDependency(params, job); } else // Process Job Phase { @@ -139,6 +174,9 @@ namespace AZ if (assetIdOutcome) { assetReference->m_assetId = assetIdOutcome.GetValue(); + productDependencies->push_back( + AssetBuilderSDK::ProductDependency{assetReference->m_assetId, AZ::Data::ProductDependencyInfo::CreateFlags(Data::AssetLoadBehavior::NoLoad)} + ); } else { @@ -223,9 +261,9 @@ namespace AZ params.passAssetSourceFile = request.m_sourceFile; params.passAssetUuid = passAssetUuid; params.serializeContext = serializeContext; - params.jobKey = "Shader Asset"; + params.jobKey = "Unknown"; - if (!FindReferencedAssets(params, &job)) + if (!FindReferencedAssets(params, &job, nullptr)) { return; } @@ -287,9 +325,10 @@ namespace AZ params.passAssetSourceFile = request.m_sourceFile; params.passAssetUuid = passAssetUuid; params.serializeContext = serializeContext; - params.jobKey = "Shader Asset"; + params.jobKey = "Unknown"; - if (!FindReferencedAssets(params, nullptr)) + AZStd::vector productDependencies; + if (!FindReferencedAssets(params, nullptr, &productDependencies)) { return; } @@ -313,6 +352,7 @@ namespace AZ // --- Save output product(s) to response --- AssetBuilderSDK::JobProduct jobProduct(destPath, PassAsset::RTTI_Type(), 0); + jobProduct.m_dependencies = productDependencies; jobProduct.m_dependenciesHandled = true; response.m_outputProducts.push_back(jobProduct); response.m_resultCode = AssetBuilderSDK::ProcessJobResult_Success; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp index 5aed6b2993..1467b017d5 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp @@ -93,31 +93,28 @@ namespace AZ // Note that the only kind of property update currently supported is rename... + PropertyGroupMap newPropertyGroups; for (auto& groupPair : m_properties) { PropertyMap& propertyMap = groupPair.second; - PropertyMap newPropertyMap; - for (auto& propertyPair : propertyMap) { MaterialPropertyId propertyId{groupPair.first, propertyPair.first}; + if (materialTypeSourceData.ApplyPropertyRenames(propertyId, m_materialTypeVersion)) { - newPropertyMap[propertyId.GetPropertyName().GetStringView()] = propertyPair.second; changesWereApplied = true; } - else - { - newPropertyMap[propertyPair.first] = propertyPair.second; - } + + newPropertyGroups[propertyId.GetGroupName().GetStringView()][propertyId.GetPropertyName().GetStringView()] = propertyPair.second; } - - propertyMap = newPropertyMap; } if (changesWereApplied) { + m_properties = AZStd::move(newPropertyGroups); + AZ_Warning("MaterialSourceData", false, "This material is based on version '%u' of '%s', but the material type is now at version '%u'. " "Automatic updates are available. Consider updating the .material source file.", diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp index 74250647c3..08f57c7cd3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp @@ -164,16 +164,14 @@ namespace AZ const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName, uint32_t materialTypeVersion) const { auto groupIter = m_propertyLayout.m_properties.find(groupName); - if (groupIter == m_propertyLayout.m_properties.end()) + if (groupIter != m_propertyLayout.m_properties.end()) { - return nullptr; - } - - for (const PropertyDefinition& property : groupIter->second) - { - if (property.m_name == propertyName) + for (const PropertyDefinition& property : groupIter->second) { - return &property; + if (property.m_name == propertyName) + { + return &property; + } } } @@ -185,16 +183,14 @@ namespace AZ // Do the search again with the new names groupIter = m_propertyLayout.m_properties.find(propertyId.GetGroupName().GetStringView()); - if (groupIter == m_propertyLayout.m_properties.end()) + if (groupIter != m_propertyLayout.m_properties.end()) { - return nullptr; - } - - for (const PropertyDefinition& property : groupIter->second) - { - if (property.m_name == propertyId.GetPropertyName().GetStringView()) + for (const PropertyDefinition& property : groupIter->second) { - return &property; + if (property.m_name == propertyId.GetPropertyName().GetStringView()) + { + return &property; + } } } 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 3c1de28d6a..d04a35a10b 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Pass.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include @@ -1215,6 +1216,12 @@ namespace AZ m_queueState = PassQueueState::NoQueue; InitializeInternal(); + + // Need to recreate the dest attachment because the source attachment might be changed + if (!m_attachmentCopy.expired()) + { + m_attachmentCopy.lock()->InvalidateDestImage(); + } m_state = PassState::Initialized; } @@ -1301,6 +1308,9 @@ namespace AZ // readback attachment with output state UpdateReadbackAttachment(params, false); + // update attachment copy for preview + UpdateAttachmentCopy(params); + UpdateConnectedOutputBindings(); } @@ -1489,6 +1499,14 @@ namespace AZ } } + void Pass::UpdateAttachmentCopy(FramePrepareParams params) + { + if (!m_attachmentCopy.expired()) + { + m_attachmentCopy.lock()->FrameBegin(params); + } + } + bool Pass::IsTimestampQueryEnabled() const { return m_flags.m_timestampQueryEnabled; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassLibrary.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassLibrary.cpp index 6a6f5f3ff9..13b2fa391f 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassLibrary.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassLibrary.cpp @@ -236,6 +236,19 @@ namespace AZ return true; } + void PassLibrary::RemovePassTemplate(const Name& name) + { + auto itr = m_templateEntries.find(name); + if (itr != m_templateEntries.end()) + { + AZ_Assert(itr->second.m_passes.empty(), "Can not delete PassTemplate '%s' because there are %zu Passes referencing it", + name.GetCStr(), itr->second.m_passes.size()); + AZ_Assert(!itr->second.m_mappingAssetId.IsValid(), "Can not delete PassTemplate '%s' because it was created from an asset", + name.GetCStr()); + m_templateEntries.erase(itr); + } + } + void PassLibrary::RemovePassFromLibrary(Pass* pass) { if (m_isShuttingDown) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp index 7f2948c13a..39d0e879e1 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/PassSystem.cpp @@ -466,6 +466,11 @@ namespace AZ return m_passLibrary.GetPassTemplate(name); } + void PassSystem::RemovePassTemplate(const Name& name) + { + m_passLibrary.RemovePassTemplate(name); + } + void PassSystem::RemovePassFromLibrary(Pass* pass) { m_passLibrary.RemovePassFromLibrary(pass); 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 b8115dff10..8353762c0f 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/RenderPass.cpp @@ -177,12 +177,6 @@ namespace AZ } } } - - // Need to recreate the dest attachment because the source attachment might be changed - if (!m_attachmentCopy.expired()) - { - m_attachmentCopy.lock()->InvalidateDestImage(); - } } void RenderPass::FrameBeginInternal(FramePrepareParams params) @@ -196,11 +190,7 @@ namespace AZ // Read back the ScopeQueries submitted from previous frames ReadbackScopeQueryResults(); - - if (!m_attachmentCopy.expired()) - { - m_attachmentCopy.lock()->FrameBegin(params); - } + CollectSrgs(); PassSystemInterface::Get()->IncrementFrameRenderPassCount(); diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.cpp index 74d52fd64d..fc1ea2ce1b 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/Pass/Specific/ImageAttachmentPreviewPass.cpp @@ -14,7 +14,7 @@ #include #include #include -#include +#include #include #include #include @@ -131,7 +131,7 @@ namespace AZ Data::AssetBus::Handler::BusDisconnect(); } - void ImageAttachmentPreviewPass::PreviewImageAttachmentForPass(RenderPass* pass, const PassAttachment* passAttachment) + void ImageAttachmentPreviewPass::PreviewImageAttachmentForPass(Pass* pass, const PassAttachment* passAttachment) { if (passAttachment->GetAttachmentType() != RHI::AttachmentType::Image) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp b/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp index e70885daa1..fda8f967da 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Public/RPIUtils.cpp @@ -119,7 +119,12 @@ namespace AZ AzFramework::AssetSystem::AssetStatus status = AzFramework::AssetSystem::AssetStatus_Unknown; AzFramework::AssetSystemRequestBus::BroadcastResult( status, &AzFramework::AssetSystemRequestBus::Events::CompileAssetSync, path); - AZ_Error("RPIUtils", status == AzFramework::AssetSystem::AssetStatus_Compiled, "Could not compile image at '%s'", path.data()); + + // When running with no Asset Processor (for example in release), CompileAssetSync will return AssetStatus_Unknown. + AZ_Error( + "RPIUtils", + status == AzFramework::AssetSystem::AssetStatus_Compiled || status == AzFramework::AssetSystem::AssetStatus_Unknown, + "Could not compile image at '%s'", path.data()); Data::AssetId streamingImageAssetId; Data::AssetCatalogRequestBus::BroadcastResult( diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp index e9d8a42641..36f4947e3d 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp @@ -208,7 +208,7 @@ namespace AZ return; } - const uint32_t originalVersion = m_materialTypeVersion; + [[maybe_unused]] const uint32_t originalVersion = m_materialTypeVersion; bool changesWereApplied = false; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp index 700ac41003..5d0a88a6d3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialPropertyDescriptor.cpp @@ -203,6 +203,16 @@ namespace AZ return InvalidEnumValue; } + + const AZ::Name& MaterialPropertyDescriptor::GetEnumName(uint32_t enumValue) const + { + if (enumValue < m_enumNames.size()) + { + return m_enumNames.at(enumValue); + } + static AZ::Name EmptyName = AZ::Name(); + return EmptyName; + } } // namespace RPI } // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp index 692087d93b..84757d58ab 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Shader/ShaderAsset.cpp @@ -264,7 +264,6 @@ namespace AZ { // When rebuilding shaders we may be in a state where the ShaderAsset and root ShaderVariantAsset have been rebuilt and reloaded, but some (or all) // shader variants haven't been built yet. Since we want to use the latest version of the shader code, ignore the old variants and fall back to the newer root variant instead. - AZ_Warning("ShaderAsset", false, "ShaderAsset and ShaderVariantAsset are out of sync; defaulting to root shader variant. (This is common while reloading shaders)."); return GetRootVariant(supervariantIndex); } } diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp index acce52ae8e..fa8eed35de 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp @@ -105,6 +105,13 @@ namespace UnitTest {"op": "rename", "from": "general.testColorNameB", "to": "general.testColorNameC"} ] }, + { + "toVersion": 6, + "actions": [ + {"op": "rename", "from": "oldGroup.MyFloat", "to": "general.MyFloat"}, + {"op": "rename", "from": "oldGroup.MyIntOldName", "to": "general.MyInt"} + ] + }, { "toVersion": 10, "actions": [ @@ -751,6 +758,72 @@ namespace UnitTest material.ApplyVersionUpdates(); } + TEST_F(MaterialSourceDataTests, Load_MaterialTypeVersionUpdate_MovePropertiesToAnotherGroup) + { + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 3, + "properties": { + "oldGroup": { + "MyFloat": 1.2, + "MyIntOldName": 5 + } + } + } + )"; + + MaterialSourceData material; + JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson); + + EXPECT_EQ(AZ::JsonSerializationResult::Tasks::ReadField, loadResult.m_jsonResultCode.GetTask()); + EXPECT_EQ(AZ::JsonSerializationResult::Processing::Completed, loadResult.m_jsonResultCode.GetProcessing()); + + // Initially, the loaded material data will match the .material file exactly. This gives us the accurate representation of + // what's actually saved on disk. + + EXPECT_NE(material.m_properties["oldGroup"].find("MyFloat"), material.m_properties["oldGroup"].end()); + EXPECT_NE(material.m_properties["oldGroup"].find("MyIntOldName"), material.m_properties["oldGroup"].end()); + EXPECT_EQ(material.m_properties["general"].find("MyFloat"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("MyInt"), material.m_properties["general"].end()); + + float myFloat = material.m_properties["oldGroup"]["MyFloat"].m_value.GetValue(); + EXPECT_EQ(myFloat, 1.2f); + + int32_t myInt = material.m_properties["oldGroup"]["MyIntOldName"].m_value.GetValue(); + EXPECT_EQ(myInt, 5); + + EXPECT_EQ(3, material.m_materialTypeVersion); + + // Then we force the material data to update to the latest material type version specification + ErrorMessageFinder warningFinder; // Note this finds errors and warnings, and we're looking for a warning. + warningFinder.AddExpectedErrorMessage("Automatic updates are available. Consider updating the .material source file"); + warningFinder.AddExpectedErrorMessage("This material is based on version '3'"); + warningFinder.AddExpectedErrorMessage("material type is now at version '10'"); + material.ApplyVersionUpdates(); + warningFinder.CheckExpectedErrorsFound(); + + // Now the material data should match the latest material type. + // Look for the property under the latest name in the material type, not the name used in the .material file. + + EXPECT_EQ(material.m_properties["oldGroup"].find("MyFloat"), material.m_properties["oldGroup"].end()); + EXPECT_EQ(material.m_properties["oldGroup"].find("MyIntOldName"), material.m_properties["oldGroup"].end()); + EXPECT_NE(material.m_properties["general"].find("MyFloat"), material.m_properties["general"].end()); + EXPECT_NE(material.m_properties["general"].find("MyInt"), material.m_properties["general"].end()); + + myFloat = material.m_properties["general"]["MyFloat"].m_value.GetValue(); + EXPECT_EQ(myFloat, 1.2f); + + myInt = material.m_properties["general"]["MyInt"].m_value.GetValue(); + EXPECT_EQ(myInt, 5); + + EXPECT_EQ(10, material.m_materialTypeVersion); + + // Calling ApplyVersionUpdates() again should not report the warning again, since the material has already been updated. + warningFinder.Reset(); + material.ApplyVersionUpdates(); + } + TEST_F(MaterialSourceDataTests, Load_MaterialTypeVersionPartialUpdate) { // This case is similar to Load_MaterialTypeVersionUpdate but we start at a later diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp index b811e630d0..2fe7b1dbe5 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp @@ -1346,7 +1346,8 @@ namespace UnitTest { "toVersion": 7, "actions": [ - { "op": "rename", "from": "general.bazA", "to": "otherGroup.bazB" } + { "op": "rename", "from": "general.bazA", "to": "otherGroup.bazB" }, + { "op": "rename", "from": "onlyOneProperty.bopA", "to": "otherGroup.bopB" } // This tests a group 'onlyOneProperty' that no longer exists in the material type ] } ], @@ -1370,6 +1371,10 @@ namespace UnitTest { "name": "bazB", "type": "Float" + }, + { + "name": "bopB", + "type": "Float" } ] } @@ -1386,13 +1391,16 @@ namespace UnitTest const MaterialTypeSourceData::PropertyDefinition* foo = materialType.FindProperty("general", "fooC"); const MaterialTypeSourceData::PropertyDefinition* bar = materialType.FindProperty("general", "barC"); const MaterialTypeSourceData::PropertyDefinition* baz = materialType.FindProperty("otherGroup", "bazB"); + const MaterialTypeSourceData::PropertyDefinition* bop = materialType.FindProperty("otherGroup", "bopB"); EXPECT_TRUE(foo); EXPECT_TRUE(bar); EXPECT_TRUE(baz); + EXPECT_TRUE(bop); EXPECT_EQ(foo->m_name, "fooC"); EXPECT_EQ(bar->m_name, "barC"); EXPECT_EQ(baz->m_name, "bazB"); + EXPECT_EQ(bop->m_name, "bopB"); // Now try doing the property lookup using old versions of the name and make sure the same property can be found @@ -1401,12 +1409,15 @@ namespace UnitTest EXPECT_EQ(bar, materialType.FindProperty("general", "barA")); EXPECT_EQ(bar, materialType.FindProperty("general", "barB")); EXPECT_EQ(baz, materialType.FindProperty("general", "bazA")); + EXPECT_EQ(bop, materialType.FindProperty("onlyOneProperty", "bopA")); EXPECT_EQ(nullptr, materialType.FindProperty("general", "fooX")); EXPECT_EQ(nullptr, materialType.FindProperty("general", "barX")); EXPECT_EQ(nullptr, materialType.FindProperty("general", "bazX")); EXPECT_EQ(nullptr, materialType.FindProperty("general", "bazB")); EXPECT_EQ(nullptr, materialType.FindProperty("otherGroup", "bazA")); + EXPECT_EQ(nullptr, materialType.FindProperty("onlyOneProperty", "bopB")); + EXPECT_EQ(nullptr, materialType.FindProperty("otherGroup", "bopA")); } TEST_F(MaterialTypeSourceDataTests, FindPropertyUsingOldName_Error_UnsupportedVersionUpdate) diff --git a/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/009_Opacity_Opaque_DoubleSided.material b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/009_Opacity_Opaque_DoubleSided.material new file mode 100644 index 0000000000..a26bf6e045 --- /dev/null +++ b/Gems/Atom/TestData/TestData/Materials/StandardPbrTestCases/009_Opacity_Opaque_DoubleSided.material @@ -0,0 +1,14 @@ +{ + "description": "", + "materialType": "Materials/Types/StandardPBR.materialtype", + "parentMaterial": "", + "propertyLayoutVersion": 3, + "properties": { + "baseColor": { + "textureMap": "Textures/Default/checker_uv_basecolor.png" + }, + "general": { + "doubleSided": true + } + } +} \ No newline at end of file diff --git a/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick_ForwardPass.shader b/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick_ForwardPass.shader index 4f1c45d235..6418cc392e 100644 --- a/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick_ForwardPass.shader +++ b/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick_ForwardPass.shader @@ -24,7 +24,7 @@ }, "CompilerHints" : { - "DxcDisableOptimizations" : false + "DisableOptimizations" : false }, "ProgramSettings": diff --git a/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR_ForwardPass.shader b/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR_ForwardPass.shader index 13ba0ce547..f87a56daa2 100644 --- a/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR_ForwardPass.shader +++ b/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR_ForwardPass.shader @@ -24,7 +24,7 @@ }, "CompilerHints" : { - "DxcDisableOptimizations" : false + "DisableOptimizations" : false }, "ProgramSettings": diff --git a/Gems/Atom/TestData/TestData/Objects/tube.fbx b/Gems/Atom/TestData/TestData/Objects/tube.fbx new file mode 100644 index 0000000000..f9034e7641 --- /dev/null +++ b/Gems/Atom/TestData/TestData/Objects/tube.fbx @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b2ecc32cd3052f3cb5836c8be7bf5cba54d98f46e6a0eeac95aaef00a123411a +size 27340 diff --git a/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/_dev_shaderball_00_basecolor.png b/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/_dev_shaderball_00_basecolor.png index 415ca3e521..07e240baf9 100644 --- a/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/_dev_shaderball_00_basecolor.png +++ b/Gems/Atom/Tools/MaterialEditor/Assets/MaterialEditor/ViewportModels/_dev_shaderball_00_basecolor.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:93a7e033d9fb0fcac221647322bde03716643d789390f79078c4fcc37ecfd005 -size 68327 +oid sha256:513f47f6fea5105f603170a8881b7e3b1cd2c4258636d64a6399c725032b500d +size 38689 diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.h index 1bed2b5715..7413390357 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.h @@ -249,7 +249,7 @@ namespace AZ // Controls how often the timestamp data is refreshed RefreshType m_refreshType = RefreshType::Realtime; - AZStd::sys_time_t m_lastUpdateTimeMicroSecond; + AZStd::sys_time_t m_lastUpdateTimeMicroSecond = 0; }; diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl index 80dcace2df..405848aa35 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiGpuProfiler.inl @@ -651,7 +651,7 @@ namespace AZ if (m_refreshType == RefreshType::OncePerSecond) { auto now = AZStd::GetTimeNowMicroSecond(); - if (m_lastUpdateTimeMicroSecond == 0 || now - m_lastUpdateTimeMicroSecond > 1000000) + if (now - m_lastUpdateTimeMicroSecond > 1000000) { needEnable = true; m_lastUpdateTimeMicroSecond = now; diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.h b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.h index 12e9776ae2..0e942bca58 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.h +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.h @@ -39,6 +39,8 @@ namespace AZ bool m_showAttachments = false; AZ::RPI::Pass* m_selectedPass = nullptr; + AZ::RPI::Pass* m_lastSelectedPass = nullptr; + AZ::Name m_selectedPassPath; AZ::RHI::AttachmentId m_attachmentId; AZ::Name m_slotName; bool m_selectedChanged = false; diff --git a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.inl b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.inl index fa649ed47b..55e457926a 100644 --- a/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.inl +++ b/Gems/Atom/Utils/Code/Include/Atom/Utils/ImGuiPassTree.inl @@ -31,7 +31,7 @@ namespace AZ::Render { - inline AZ::RPI::PassAttachment* FindPassAttachment(AZ::RPI::RenderPass* pass, AZ::RHI::AttachmentId attachmentId) + inline AZ::RPI::PassAttachment* FindPassAttachment(AZ::RPI::Pass* pass, AZ::RHI::AttachmentId attachmentId) { for (auto& binding : pass->GetAttachmentBindings()) { @@ -47,6 +47,10 @@ namespace AZ::Render { using namespace AZ; + // always set m_selectedPass to empty and use m_selectedPassPath to find it when render the pass tree + m_selectedPass = nullptr; + bool needSaveAttachment = false; + ImGui::SetNextWindowSize(ImVec2(200.f, 200.f), ImGuiCond_FirstUseEver); if (ImGui::Begin("PassTree View", &draw, ImGuiWindowFlags_None)) { @@ -83,60 +87,16 @@ namespace AZ::Render if (Scriptable_ImGui::Button("Save Attachment")) { - m_attachmentReadbackInfo = ""; - if (!m_readback) - { - m_readback = AZStd::make_shared(AZ::RHI::ScopeId{ "AttachmentReadback" }); - m_readback->SetCallback(AZStd::bind(&ImGuiPassTree::ReadbackCallback, this, AZStd::placeholders::_1)); - } - - if (m_selectedPass && !m_slotName.IsEmpty()) - { - bool readbackResult = m_selectedPass->ReadbackAttachment(m_readback, m_slotName); - if (!readbackResult) - { - AZ_Error("ImGuiPassTree", false, "Failed to readback attachment from pass [%s] slot [%s]", m_selectedPass->GetName().GetCStr(), m_slotName.GetCStr()); - } - } + needSaveAttachment = true; } ImGui::TextWrapped("%s", m_attachmentReadbackInfo.c_str()); } - if (m_previewAttachment && m_selectedChanged) - { - m_selectedChanged = false; - if (!m_attachmentId.IsEmpty() && m_selectedPass) - { - AZ::RPI::RenderPass* renderPass = azrtti_cast(m_selectedPass); - if (renderPass) - { - if (!m_previewPass->GetParent()) - { - RPI::PassSystemInterface::Get()->GetRootPass()->AddChild(m_previewPass); - } - AZ::RPI::PassAttachment* attachment = FindPassAttachment(renderPass, m_attachmentId); - if (attachment) - { - // Reset output attachment to empty so the preview will use pass's owner render pipeline's output - m_previewPass->SetOutputColorAttachment(nullptr); - m_previewPass->PreviewImageAttachmentForPass(renderPass, attachment); - } - } - else - { - m_previewPass->ClearPreviewAttachment(); - if (m_previewPass->GetParent()) - { - m_previewPass->QueueForRemoval(); - } - } - } - } - ImGui::End(); // Draw the hierarchical view + // It will assign m_seletedPass if there is a pass matches m_seletedPassPath ImGui::SetNextWindowPos(ImVec2(300, 60), ImGuiCond_FirstUseEver); ImGui::SetNextWindowSize(ImVec2(300, 500), ImGuiCond_FirstUseEver); if (ImGui::Begin("PassTree", nullptr, ImGuiWindowFlags_None)) @@ -144,6 +104,63 @@ namespace AZ::Render DrawTreeView(rootPass); } ImGui::End(); + + // It's possible that the pass pointer changed but selected pass path wasn't changed + if (m_selectedPass != m_lastSelectedPass) + { + m_selectedChanged = true; + if (m_selectedPass == nullptr) + { + m_selectedPassPath = AZ::Name{}; + } + } + m_lastSelectedPass = m_selectedPass; + + if (m_previewAttachment && m_selectedChanged) + { + m_selectedChanged = false; + if (!m_attachmentId.IsEmpty() && m_selectedPass) + { + if (!m_previewPass->GetParent()) + { + RPI::PassSystemInterface::Get()->GetRootPass()->AddChild(m_previewPass); + } + AZ::RPI::PassAttachment* attachment = FindPassAttachment(m_selectedPass, m_attachmentId); + if (attachment) + { + // Reset output attachment to empty so the preview will use pass's owner render pipeline's output + m_previewPass->SetOutputColorAttachment(nullptr); + m_previewPass->PreviewImageAttachmentForPass(m_selectedPass, attachment); + } + } + else + { + m_previewPass->ClearPreviewAttachment(); + if (m_previewPass->GetParent()) + { + m_previewPass->QueueForRemoval(); + } + } + } + + if (needSaveAttachment) + { + m_attachmentReadbackInfo = ""; + if (!m_readback) + { + m_readback = AZStd::make_shared(AZ::RHI::ScopeId{ "AttachmentReadback" }); + m_readback->SetCallback(AZStd::bind(&ImGuiPassTree::ReadbackCallback, this, AZStd::placeholders::_1)); + } + + if (m_selectedPass && !m_slotName.IsEmpty()) + { + bool readbackResult = m_selectedPass->ReadbackAttachment(m_readback, m_slotName); + if (!readbackResult) + { + AZ_Error("ImGuiPassTree", false, "Failed to readback attachment from pass [%s] slot [%s]", m_selectedPass->GetName().GetCStr(), m_slotName.GetCStr()); + } + } + } } inline void ImGuiPassTree::DrawPassAttachments(AZ::RPI::Pass* pass) @@ -202,6 +219,7 @@ namespace AZ::Render if (Scriptable_ImGui::Selectable(label.c_str(), m_attachmentId == binding.m_attachment->GetAttachmentId())) { + m_selectedPassPath = pass->GetPathName(); m_selectedPass = pass; m_attachmentId = binding.m_attachment->GetAttachmentId(); m_slotName = binding.m_name; @@ -232,9 +250,9 @@ namespace AZ::Render if (!m_showAttachments) { // Only draw the leaf pass as selectable if we are not showing attachments as its children - if (Scriptable_ImGui::Selectable(pass->GetName().GetCStr(), m_selectedPass == pass)) + if (Scriptable_ImGui::Selectable(pass->GetName().GetCStr(), m_selectedPassPath == pass->GetPathName())) { - m_selectedPass = pass; + m_selectedPassPath = pass->GetPathName(); m_attachmentId = AZ::RHI::AttachmentId{}; m_slotName = AZ::Name{}; m_selectedChanged = true; @@ -244,13 +262,13 @@ namespace AZ::Render { // Draw the pass as a tree node which has attachments as its children ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_DefaultOpen - | ((m_selectedPass == pass) ? ImGuiTreeNodeFlags_Selected : 0); + | ((m_selectedPassPath == pass->GetPathName()) ? ImGuiTreeNodeFlags_Selected : 0); bool nodeOpen = Scriptable_ImGui::TreeNodeEx(pass->GetName().GetCStr(), flags); if (ImGui::IsItemClicked()) { - m_selectedPass = pass; + m_selectedPassPath = pass->GetPathName(); m_attachmentId = AZ::RHI::AttachmentId{}; m_slotName = AZ::Name{}; m_selectedChanged = true; @@ -259,7 +277,6 @@ namespace AZ::Render if (nodeOpen) { DrawPassAttachments(pass); - Scriptable_ImGui::TreePop(); } } @@ -268,13 +285,13 @@ namespace AZ::Render { // For a ParentPasse, draw it as a tree node ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_OpenOnDoubleClick | ImGuiTreeNodeFlags_DefaultOpen - | ((m_selectedPass == pass) ? ImGuiTreeNodeFlags_Selected : 0); + | ((m_selectedPassPath == pass->GetPathName()) ? ImGuiTreeNodeFlags_Selected : 0); bool nodeOpen = ImGui::TreeNodeEx(pass->GetName().GetCStr(), flags); if (ImGui::IsItemClicked()) { - m_selectedPass = pass; + m_selectedPassPath = pass->GetPathName(); m_attachmentId = AZ::RHI::AttachmentId{}; m_slotName = AZ::Name{}; m_selectedChanged = true; @@ -282,7 +299,10 @@ namespace AZ::Render if (nodeOpen) { - DrawPassAttachments(pass); + if (m_showAttachments) + { + DrawPassAttachments(pass); + } for (const auto& child : asParent->GetChildren()) { DrawTreeView(child.get()); @@ -296,6 +316,12 @@ namespace AZ::Render { ImGui::PopStyleColor(); } + + // set m_selectedPass if pass path matches + if (pass->GetPathName() == m_selectedPassPath) + { + m_selectedPass = pass; + } } inline void ImGuiPassTree::ReadbackCallback(const AZ::RPI::AttachmentReadback::ReadbackResult& readbackResult) @@ -364,7 +390,9 @@ namespace AZ::Render m_previewAttachment = false; m_showAttachments = false; + m_selectedPassPath = AZ::Name{}; m_selectedPass = nullptr; + m_lastSelectedPass = nullptr; m_attachmentId = AZ::RHI::AttachmentId{}; m_slotName = AZ::Name{}; m_selectedChanged = false; diff --git a/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp b/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp index 7d659ebb7a..91844c9b2f 100644 --- a/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp +++ b/Gems/AtomLyIntegration/AtomViewportDisplayInfo/Code/Source/AtomViewportDisplayInfoSystemComponent.cpp @@ -130,7 +130,8 @@ namespace AZ::Render } AZ::RPI::ViewportContextPtr viewportContext = GetViewportContext(); - if (!m_fontDrawInterface || !viewportContext || !viewportContext->GetRenderScene()) + if (!m_fontDrawInterface || !viewportContext || !viewportContext->GetRenderScene() || + !AZ::Interface::Get()) { return; } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt b/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt index e68681315e..95331a2f3f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/CMakeLists.txt @@ -86,6 +86,8 @@ if(PAL_TRAIT_BUILD_HOST_TOOLS) FILES_CMAKE atomlyintegration_commonfeatures_editor_files.cmake ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}_files.cmake + PLATFORM_INCLUDE_FILES + ${pal_source_dir}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake INCLUDE_DIRECTORIES PRIVATE . diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h index 293080367d..d8d0c2dbc3 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/Material/MaterialComponentBus.h @@ -60,52 +60,8 @@ namespace AZ virtual void ClearMaterialOverride(const MaterialAssignmentId& materialAssignmentId) = 0; //! Set a material property override value wrapped by an AZStd::any virtual void SetPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZStd::any& value) = 0; - //! Set a material property override value to a bool - virtual void SetPropertyOverrideBool(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const bool& value) = 0; - //! Set a material property override value to a integer - virtual void SetPropertyOverrideInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const int32_t& value) = 0; - //! Set a material property override value to a unsigned integer - virtual void SetPropertyOverrideUInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const uint32_t& value) = 0; - //! Set a material property override value to a float - virtual void SetPropertyOverrideFloat(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const float& value) = 0; - //! Set a material property override value to a Vector2 - virtual void SetPropertyOverrideVector2(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector2& value) = 0; - //! Set a material property override value to a Vector3 - virtual void SetPropertyOverrideVector3(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector3& value) = 0; - //! Set a material property override value to a Vector4 - virtual void SetPropertyOverrideVector4(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector4& value) = 0; - //! Set a material property override value to a color - virtual void SetPropertyOverrideColor(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Color& value) = 0; - //! Set a material property override value to an image asset - virtual void SetPropertyOverrideImageAsset(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Data::Asset& value) = 0; - //! Set a material property override value to an image instance - virtual void SetPropertyOverrideImageInstance(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Data::Instance& value) = 0; - //! Set a material property override value to a string - virtual void SetPropertyOverrideString(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZStd::string& value) = 0; //! Get a material property override value wrapped by an AZStd::any virtual AZStd::any GetPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a bool - virtual bool GetPropertyOverrideBool(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as an integer - virtual int32_t GetPropertyOverrideInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as an unsigned integer - virtual uint32_t GetPropertyOverrideUInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a float - virtual float GetPropertyOverrideFloat(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a Vector2 - virtual AZ::Vector2 GetPropertyOverrideVector2(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a Vector3 - virtual AZ::Vector3 GetPropertyOverrideVector3(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a Vector4 - virtual AZ::Vector4 GetPropertyOverrideVector4(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a Color - virtual AZ::Color GetPropertyOverrideColor(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as an image asset - virtual AZ::Data::Asset GetPropertyOverrideImageAsset(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as an image instance - virtual AZ::Data::Instance GetPropertyOverrideImageInstance(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; - //! Get a material property override value as a string - virtual AZStd::string GetPropertyOverrideString(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const = 0; //! Clear property override for a specific material assignment virtual void ClearPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) = 0; //! Clear property overrides for a specific material assignment @@ -122,6 +78,21 @@ namespace AZ const MaterialAssignmentId& materialAssignmentId, const AZ::RPI::MaterialModelUvOverrideMap& modelUvOverrides) = 0; //! Get Model UV overrides for a specific material assignment virtual AZ::RPI::MaterialModelUvOverrideMap GetModelUvOverrides(const MaterialAssignmentId& materialAssignmentId) const = 0; + + //! Set material property override value with a specific type + template + void SetPropertyOverrideT(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const T& value) + { + SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); + } + + //! Get material property override value with a specific type + template + T GetPropertyOverrideT(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const + { + const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); + return !value.empty() && value.is() ? AZStd::any_cast(value) : T{}; + } }; using MaterialComponentRequestBus = EBus; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp index 9af3b41b12..bfa9f1d36f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentInspector.cpp @@ -314,12 +314,21 @@ namespace AZ AtomToolsFramework::ConvertToPropertyConfig(propertyConfig, propertyDefinition); + const auto& propertyIndex = + m_editData.m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id); + propertyConfig.m_groupName = groupDisplayName; - const auto& propertyIndex = m_editData.m_materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(propertyConfig.m_id); propertyConfig.m_showThumbnail = true; - propertyConfig.m_defaultValue = AtomToolsFramework::ConvertToEditableType(m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]); - propertyConfig.m_parentValue = AtomToolsFramework::ConvertToEditableType(m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]); - propertyConfig.m_originalValue = AtomToolsFramework::ConvertToEditableType(m_editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]); + + propertyConfig.m_defaultValue = AtomToolsFramework::ConvertToEditableType( + m_editData.m_materialTypeAsset->GetDefaultPropertyValues()[propertyIndex.GetIndex()]); + + // There is no explicit parent material here. Material instance property overrides replace the values from the + // assigned material asset. Its values should be treated as parent, for comparison, in this case. + propertyConfig.m_parentValue = AtomToolsFramework::ConvertToEditableType( + m_editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]); + propertyConfig.m_originalValue = AtomToolsFramework::ConvertToEditableType( + m_editData.m_materialAsset->GetPropertyValues()[propertyIndex.GetIndex()]); group.m_properties.emplace_back(propertyConfig); } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp index e53010d747..adaebfb889 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialSystemComponent.cpp @@ -196,6 +196,7 @@ namespace AZ { AZ_UNUSED(entityId); AZ_UNUSED(materialAssignmentId); + AZ_Warning( "EditorMaterialSystemComponent", false, "RenderMaterialPreview capture failed for entity %s slot %s.", entityId.ToString().c_str(), materialAssignmentId.ToString().c_str()); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp index 07082a87d5..0ccdae28de 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.cpp @@ -54,29 +54,29 @@ namespace AZ ->Event("GetMaterialOverride", &MaterialComponentRequestBus::Events::GetMaterialOverride) ->Event("ClearMaterialOverride", &MaterialComponentRequestBus::Events::ClearMaterialOverride) ->Event("SetPropertyOverride", &MaterialComponentRequestBus::Events::SetPropertyOverride) - ->Event("SetPropertyOverrideBool", &MaterialComponentRequestBus::Events::SetPropertyOverrideBool) - ->Event("SetPropertyOverrideInt32", &MaterialComponentRequestBus::Events::SetPropertyOverrideInt32) - ->Event("SetPropertyOverrideUInt32", &MaterialComponentRequestBus::Events::SetPropertyOverrideUInt32) - ->Event("SetPropertyOverrideFloat", &MaterialComponentRequestBus::Events::SetPropertyOverrideFloat) - ->Event("SetPropertyOverrideVector2", &MaterialComponentRequestBus::Events::SetPropertyOverrideVector2) - ->Event("SetPropertyOverrideVector3", &MaterialComponentRequestBus::Events::SetPropertyOverrideVector3) - ->Event("SetPropertyOverrideVector4", &MaterialComponentRequestBus::Events::SetPropertyOverrideVector4) - ->Event("SetPropertyOverrideColor", &MaterialComponentRequestBus::Events::SetPropertyOverrideColor) - ->Event("SetPropertyOverrideImageAsset", &MaterialComponentRequestBus::Events::SetPropertyOverrideImageAsset) - ->Event("SetPropertyOverrideImageInstance", &MaterialComponentRequestBus::Events::SetPropertyOverrideImageInstance) - ->Event("SetPropertyOverrideString", &MaterialComponentRequestBus::Events::SetPropertyOverrideString) + ->Event("SetPropertyOverrideBool", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideInt32", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideUInt32", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideFloat", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideVector2", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideVector3", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideVector4", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideColor", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideImage", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideString", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) + ->Event("SetPropertyOverrideEnum", &MaterialComponentRequestBus::Events::SetPropertyOverrideT) ->Event("GetPropertyOverride", &MaterialComponentRequestBus::Events::GetPropertyOverride) - ->Event("GetPropertyOverrideBool", &MaterialComponentRequestBus::Events::GetPropertyOverrideBool) - ->Event("GetPropertyOverrideInt32", &MaterialComponentRequestBus::Events::GetPropertyOverrideInt32) - ->Event("GetPropertyOverrideUInt32", &MaterialComponentRequestBus::Events::GetPropertyOverrideUInt32) - ->Event("GetPropertyOverrideFloat", &MaterialComponentRequestBus::Events::GetPropertyOverrideFloat) - ->Event("GetPropertyOverrideVector2", &MaterialComponentRequestBus::Events::GetPropertyOverrideVector2) - ->Event("GetPropertyOverrideVector3", &MaterialComponentRequestBus::Events::GetPropertyOverrideVector3) - ->Event("GetPropertyOverrideVector4", &MaterialComponentRequestBus::Events::GetPropertyOverrideVector4) - ->Event("GetPropertyOverrideColor", &MaterialComponentRequestBus::Events::GetPropertyOverrideColor) - ->Event("GetPropertyOverrideImageAsset", &MaterialComponentRequestBus::Events::GetPropertyOverrideImageAsset) - ->Event("GetPropertyOverrideImageInstance", &MaterialComponentRequestBus::Events::GetPropertyOverrideImageInstance) - ->Event("GetPropertyOverrideString", &MaterialComponentRequestBus::Events::GetPropertyOverrideString) + ->Event("GetPropertyOverrideBool", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideInt32", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideUInt32", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideFloat", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideVector2", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideVector3", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideVector4", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideColor", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideImage", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideString", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) + ->Event("GetPropertyOverrideEnum", &MaterialComponentRequestBus::Events::GetPropertyOverrideT) ->Event("ClearPropertyOverride", &MaterialComponentRequestBus::Events::ClearPropertyOverride) ->Event("ClearPropertyOverrides", &MaterialComponentRequestBus::Events::ClearPropertyOverrides) ->Event("ClearAllPropertyOverrides", &MaterialComponentRequestBus::Events::ClearAllPropertyOverrides) @@ -121,8 +121,13 @@ namespace AZ MaterialComponentRequestBus::Handler::BusDisconnect(); MaterialReceiverNotificationBus::Handler::BusDisconnect(); TickBus::Handler::BusDisconnect(); + ReleaseMaterials(); + // Sending notification to wipe any previously assigned material overrides + MaterialComponentNotificationBus::Event( + m_entityId, &MaterialComponentNotifications::OnMaterialsUpdated, MaterialAssignmentMap()); + m_queuedMaterialUpdateNotification = false; m_entityId = AZ::EntityId(AZ::EntityId::InvalidEntityId); } @@ -221,6 +226,11 @@ namespace AZ if (!anyQueued) { ReleaseMaterials(); + + // If no other materials were loaded, the notification must still be sent in case there are externally managed material + // instances in the configuration + MaterialComponentNotificationBus::Event( + m_entityId, &MaterialComponentNotifications::OnMaterialsUpdated, m_configuration.m_materials); } } @@ -268,8 +278,6 @@ namespace AZ { materialPair.second.Release(); } - - MaterialComponentNotificationBus::Event(m_entityId, &MaterialComponentNotifications::OnMaterialsUpdated, m_configuration.m_materials); } MaterialAssignmentMap MaterialComponentController::GetOriginalMaterialAssignments() const @@ -499,76 +507,6 @@ namespace AZ QueuePropertyChanges(materialAssignmentId); } - void MaterialComponentController::SetPropertyOverrideBool( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const bool& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideInt32( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const int32_t& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideUInt32( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const uint32_t& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideFloat( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const float& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideVector2( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector2& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideVector3( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector3& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideVector4( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector4& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideColor( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Color& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideImageAsset( - const MaterialAssignmentId& materialAssignmentId, - const AZStd::string& propertyName, - const AZ::Data::Asset& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideImageInstance( - const MaterialAssignmentId& materialAssignmentId, - const AZStd::string& propertyName, - const AZ::Data::Instance& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - - void MaterialComponentController::SetPropertyOverrideString( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZStd::string& value) - { - SetPropertyOverride(materialAssignmentId, propertyName, AZStd::any(value)); - } - AZStd::any MaterialComponentController::GetPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const { const auto materialIt = m_configuration.m_materials.find(materialAssignmentId); @@ -586,83 +524,6 @@ namespace AZ return propertyIt->second; } - bool MaterialComponentController::GetPropertyOverrideBool( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : false; - } - - int32_t MaterialComponentController::GetPropertyOverrideInt32( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : 0; - } - - uint32_t MaterialComponentController::GetPropertyOverrideUInt32( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : 0; - } - - float MaterialComponentController::GetPropertyOverrideFloat( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : 0.0f; - } - - AZ::Vector2 MaterialComponentController::GetPropertyOverrideVector2( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : AZ::Vector2::CreateZero(); - } - - AZ::Vector3 MaterialComponentController::GetPropertyOverrideVector3( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : AZ::Vector3::CreateZero(); - } - - AZ::Vector4 MaterialComponentController::GetPropertyOverrideVector4( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : AZ::Vector4::CreateZero(); - } - - AZ::Color MaterialComponentController::GetPropertyOverrideColor( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : AZ::Color::CreateZero(); - } - - AZ::Data::Asset MaterialComponentController::GetPropertyOverrideImageAsset( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is>() ? AZStd::any_cast>(value) : AZ::Data::Asset(); - } - - AZ::Data::Instance MaterialComponentController::GetPropertyOverrideImageInstance( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is>() ? AZStd::any_cast>(value) : AZ::Data::Instance(); - } - - AZStd::string MaterialComponentController::GetPropertyOverrideString( - const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const - { - const AZStd::any& value = GetPropertyOverride(materialAssignmentId, propertyName); - return !value.empty() && value.is() ? AZStd::any_cast(value) : AZStd::string(); - } - void MaterialComponentController::ClearPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) { auto materialIt = m_configuration.m_materials.find(materialAssignmentId); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h index ec542c377c..74b1cfda4d 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/MaterialComponentController.h @@ -65,33 +65,8 @@ namespace AZ void SetMaterialOverride(const MaterialAssignmentId& materialAssignmentId, const AZ::Data::AssetId& materialAssetId) override; AZ::Data::AssetId GetMaterialOverride(const MaterialAssignmentId& materialAssignmentId) const override; void ClearMaterialOverride(const MaterialAssignmentId& materialAssignmentId) override; - void SetPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZStd::any& value) override; - void SetPropertyOverrideBool(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const bool& value) override; - void SetPropertyOverrideInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const int32_t& value) override; - void SetPropertyOverrideUInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const uint32_t& value) override; - void SetPropertyOverrideFloat(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const float& value) override; - void SetPropertyOverrideVector2(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector2& value) override; - void SetPropertyOverrideVector3(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector3& value) override; - void SetPropertyOverrideVector4(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Vector4& value) override; - void SetPropertyOverrideColor(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Color& value) override; - void SetPropertyOverrideImageAsset(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Data::Asset& value) override; - void SetPropertyOverrideImageInstance(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZ::Data::Instance& value) override; - void SetPropertyOverrideString(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName, const AZStd::string& value) override; - AZStd::any GetPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - bool GetPropertyOverrideBool(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - int32_t GetPropertyOverrideInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - uint32_t GetPropertyOverrideUInt32(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - float GetPropertyOverrideFloat(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZ::Vector2 GetPropertyOverrideVector2(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZ::Vector3 GetPropertyOverrideVector3(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZ::Vector4 GetPropertyOverrideVector4(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZ::Color GetPropertyOverrideColor(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZ::Data::Asset GetPropertyOverrideImageAsset(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZ::Data::Instance GetPropertyOverrideImageInstance(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - AZStd::string GetPropertyOverrideString(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) const override; - void ClearPropertyOverride(const MaterialAssignmentId& materialAssignmentId, const AZStd::string& propertyName) override; void ClearPropertyOverrides(const MaterialAssignmentId& materialAssignmentId) override; void ClearAllPropertyOverrides() override; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Android/platform_android.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Android/platform_android.cmake new file mode 100644 index 0000000000..7a325ca97e --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Android/platform_android.cmake @@ -0,0 +1,7 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/AppleTV/platform_appletv.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/AppleTV/platform_appletv.cmake new file mode 100644 index 0000000000..5cd1fb5a22 --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/AppleTV/platform_appletv.cmake @@ -0,0 +1,8 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Linux/platform_linux.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Linux/platform_linux.cmake new file mode 100644 index 0000000000..7a325ca97e --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Linux/platform_linux.cmake @@ -0,0 +1,7 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Mac/platform_mac.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Mac/platform_mac.cmake new file mode 100644 index 0000000000..7a325ca97e --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Mac/platform_mac.cmake @@ -0,0 +1,7 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Windows/platform_windows.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Windows/platform_windows.cmake new file mode 100644 index 0000000000..3beda63de7 --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/Windows/platform_windows.cmake @@ -0,0 +1,13 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +if(NOT LY_MONOLITHIC_GAME) # Do not use OpenImageIO in monolithic game + set(LY_RUNTIME_DEPENDENCIES + 3rdParty::OpenImageIO + ) +endif() diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/iOS/platform_ios.cmake b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/iOS/platform_ios.cmake new file mode 100644 index 0000000000..7a325ca97e --- /dev/null +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Platform/iOS/platform_ios.cmake @@ -0,0 +1,7 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp index f6186be235..facf7a7b12 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportRenderer.cpp @@ -128,10 +128,10 @@ namespace EMStudio AZ_Assert(m_gridEntity != nullptr, "Failed to create grid entity."); AZ::Render::GridComponentConfig gridConfig; - gridConfig.m_gridSize = 4.0f; + gridConfig.m_gridSize = 20.0f; gridConfig.m_axisColor = AZ::Color(0.5f, 0.5f, 0.5f, 1.0f); gridConfig.m_primaryColor = AZ::Color(0.3f, 0.3f, 0.3f, 1.0f); - gridConfig.m_secondaryColor = AZ::Color(0.5f, 0.1f, 0.1f, 1.0f); + gridConfig.m_secondaryColor = AZ::Color(0.5f, 0.5f, 0.5f, 1.0f); auto gridComponent = m_gridEntity->CreateComponent(AZ::Render::GridComponentTypeId); gridComponent->SetConfiguration(gridConfig); diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp index 7af5c1607a..c6e349236f 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.cpp @@ -6,10 +6,11 @@ * */ -#include +#include #include #include #include +#include #include #include @@ -166,6 +167,26 @@ namespace EMStudio GetViewportContext()->SetCameraTransform(AZ::Transform::CreateLookAt(cameraPosition, targetPosition)); } + void AnimViewportWidget::OnTick(float deltaTime, AZ::ScriptTimePoint time) + { + RenderViewportWidget::OnTick(deltaTime, time); + CalculateCameraProjection(); + } + + void AnimViewportWidget::CalculateCameraProjection() + { + auto viewportContext = GetViewportContext(); + auto windowSize = viewportContext->GetViewportSize(); + // Prevent devided by zero + const float height = AZStd::max(aznumeric_cast(windowSize.m_height), 1.0f); + const float aspectRatio = aznumeric_cast(windowSize.m_width) / height; + + AZ::Matrix4x4 viewToClipMatrix; + AZ::MakePerspectiveFovMatrixRH(viewToClipMatrix, AZ::Constants::HalfPi, aspectRatio, DepthNear, DepthFar, true); + + viewportContext->GetDefaultView()->SetViewToClipMatrix(viewToClipMatrix); + } + void AnimViewportWidget::ToggleRenderFlag(EMotionFX::ActorRenderFlag flag) { m_renderFlags[flag] = !m_renderFlags[flag]; diff --git a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h index 8aa316a8ba..e2099ea2ab 100644 --- a/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h +++ b/Gems/AtomLyIntegration/EMotionFXAtom/Code/Tools/EMStudio/AnimViewportWidget.h @@ -30,6 +30,9 @@ namespace EMStudio EMotionFX::ActorRenderFlagBitset GetRenderFlags() const; private: + void OnTick(float deltaTime, AZ::ScriptTimePoint time) override; + + void CalculateCameraProjection(); void SetupCameras(); void SetupCameraController(); @@ -42,6 +45,8 @@ namespace EMStudio void ToggleRenderFlag(EMotionFX::ActorRenderFlag flag); static constexpr float CameraDistance = 2.0f; + static constexpr float DepthNear = 0.01f; + static constexpr float DepthFar = 100.0f; AZStd::unique_ptr m_renderer; AZStd::shared_ptr m_rotateCamera; diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index 161160e16f..74ba99c26c 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -311,17 +311,17 @@ namespace AZ m_forceClearRenderData = true; } - bool HairFeatureProcessor::HasHairParentPass() + bool HairFeatureProcessor::HasHairParentPass(RPI::RenderPipeline* renderPipeline) { - RPI::PassFilter passFilter = RPI::PassFilter::CreateWithPassName(HairParentPassName, GetParentScene()); + RPI::PassFilter passFilter = RPI::PassFilter::CreateWithPassName(HairParentPassName, renderPipeline); RPI::Pass* pass = RPI::PassSystemInterface::Get()->FindFirstPass(passFilter); - return pass; + return pass ? true : false; } void HairFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr renderPipeline) { // Proceed only if this is the main pipeline that contains the parent pass - if (!HasHairParentPass()) + if (!HasHairParentPass(renderPipeline.get())) { return; } @@ -335,7 +335,7 @@ namespace AZ void HairFeatureProcessor::OnRenderPipelineRemoved([[maybe_unused]] RPI::RenderPipeline* renderPipeline) { // Proceed only if this is the main pipeline that contains the parent pass - if (!HasHairParentPass()) + if (!HasHairParentPass(renderPipeline)) { return; } @@ -347,7 +347,7 @@ namespace AZ void HairFeatureProcessor::OnRenderPipelinePassesChanged(RPI::RenderPipeline* renderPipeline) { // Proceed only if this is the main pipeline that contains the parent pass - if (!HasHairParentPass()) + if (!HasHairParentPass(renderPipeline)) { return; } @@ -623,3 +623,4 @@ namespace AZ } // namespace Hair } // namespace Render } // namespace AZ + diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h index f810967824..70e37a7863 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.h @@ -165,7 +165,7 @@ namespace AZ void EnablePasses(bool enable); - bool HasHairParentPass(); + bool HasHairParentPass(RPI::RenderPipeline* renderPipeline); //! The following will serve to register the FP in the Thumbnail system AZStd::vector m_hairFeatureProcessorRegistryName; diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/AnimGraphNodeGroupCommands.h b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/AnimGraphNodeGroupCommands.h index 014d3b0398..3623bd5cd5 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/AnimGraphNodeGroupCommands.h +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/AnimGraphNodeGroupCommands.h @@ -60,7 +60,7 @@ namespace CommandSystem const char* GetDescription() const override; MCore::Command* Create() override { - return new CommandAnimGraphAdjustNodeGroup(this); + return aznew CommandAnimGraphAdjustNodeGroup(this); } static AZStd::vector GenerateNodeNameVector(EMotionFX::AnimGraph* animGraph, const AZStd::vector& nodeIDs); diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/CommandManager.cpp b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/CommandManager.cpp index be4626f592..2c646b3504 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/CommandManager.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/CommandManager.cpp @@ -104,7 +104,7 @@ namespace CommandSystem RegisterCommand(new CommandMotionSetAdjustMotion()); // register node group commands - RegisterCommand(new CommandAdjustNodeGroup()); + RegisterCommand(aznew CommandAdjustNodeGroup()); RegisterCommand(new CommandAddNodeGroup()); RegisterCommand(new CommandRemoveNodeGroup()); @@ -134,7 +134,7 @@ namespace CommandSystem RegisterCommand(aznew CommandAdjustTransitionCondition()); RegisterCommand(new CommandAnimGraphAddNodeGroup()); RegisterCommand(new CommandAnimGraphRemoveNodeGroup()); - RegisterCommand(new CommandAnimGraphAdjustNodeGroup()); + RegisterCommand(aznew CommandAnimGraphAdjustNodeGroup()); RegisterCommand(new CommandAnimGraphAddGroupParameter()); RegisterCommand(new CommandAnimGraphRemoveGroupParameter()); RegisterCommand(new CommandAnimGraphAdjustGroupParameter()); diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/NodeGroupCommands.h b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/NodeGroupCommands.h index 6c57877bd5..640a670e44 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/NodeGroupCommands.h +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/NodeGroupCommands.h @@ -63,7 +63,7 @@ namespace CommandSystem const char* GetDescription() const override; MCore::Command* Create() override { - return new CommandAdjustNodeGroup(this); + return aznew CommandAdjustNodeGroup(this); } private: diff --git a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp index e51c9ece0b..d7c7bf69e5 100644 --- a/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/CommandSystem/Source/RagdollCommands.cpp @@ -105,6 +105,8 @@ namespace EMotionFX *jointTypeId, parentBindRotationWorld, nodeBindRotationWorld, boneDirection, exampleRotationsLocal); AZ_Assert(jointLimitConfig, "Could not create joint limit configuration."); + jointLimitConfig->SetPropertyVisibility(AzPhysics::JointConfiguration::PropertyVisibility::ParentLocalRotation, true); + jointLimitConfig->SetPropertyVisibility(AzPhysics::JointConfiguration::PropertyVisibility::ChildLocalRotation, true); return jointLimitConfig; } } diff --git a/Gems/EMotionFX/Code/Source/Integration/Components/ActorComponent.cpp b/Gems/EMotionFX/Code/Source/Integration/Components/ActorComponent.cpp index 8520896dd2..e38e15d124 100644 --- a/Gems/EMotionFX/Code/Source/Integration/Components/ActorComponent.cpp +++ b/Gems/EMotionFX/Code/Source/Integration/Components/ActorComponent.cpp @@ -576,7 +576,7 @@ namespace EMotionFX // The configuration stores some debug option. When that is enabled, we override it on top of the render flags. m_debugRenderFlags[RENDER_AABB] = m_debugRenderFlags[RENDER_AABB] || m_configuration.m_renderBounds; - m_debugRenderFlags[RENDER_SKELETON] = m_debugRenderFlags[RENDER_SKELETON] || m_configuration.m_renderSkeleton; + m_debugRenderFlags[RENDER_LINESKELETON] = m_debugRenderFlags[RENDER_LINESKELETON] || m_configuration.m_renderSkeleton; m_debugRenderFlags[RENDER_EMFX_DEBUG] = true; m_renderActorInstance->DebugDraw(m_debugRenderFlags); } diff --git a/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp b/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp index d0274b3c68..78470a1fa5 100644 --- a/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp +++ b/Gems/EMotionFX/Code/Source/Integration/Editor/Components/EditorActorComponent.cpp @@ -606,7 +606,7 @@ namespace EMotionFX m_renderActorInstance->UpdateBounds(); m_debugRenderFlags[RENDER_AABB] = m_renderBounds; - m_debugRenderFlags[RENDER_SKELETON] = m_renderSkeleton; + m_debugRenderFlags[RENDER_LINESKELETON] = m_renderSkeleton; m_debugRenderFlags[RENDER_EMFX_DEBUG] = true; m_renderActorInstance->DebugDraw(m_debugRenderFlags); } diff --git a/Gems/InAppPurchases/Code/inapppurchases_files.cmake b/Gems/InAppPurchases/Code/inapppurchases_files.cmake index 5eab792206..2669cbf930 100644 --- a/Gems/InAppPurchases/Code/inapppurchases_files.cmake +++ b/Gems/InAppPurchases/Code/inapppurchases_files.cmake @@ -10,6 +10,7 @@ set(FILES Include/InAppPurchases/InAppPurchasesBus.h Include/InAppPurchases/InAppPurchasesInterface.h Include/InAppPurchases/InAppPurchasesResponseBus.h + Source/InAppPurchasesSystemComponent.h Source/InAppPurchasesSystemComponent.cpp Source/InAppPurchasesInterface.cpp ) diff --git a/Gems/LyShine/Code/Editor/ViewportInteraction.cpp b/Gems/LyShine/Code/Editor/ViewportInteraction.cpp index e6bdb4d8d5..a39a7b9cac 100644 --- a/Gems/LyShine/Code/Editor/ViewportInteraction.cpp +++ b/Gems/LyShine/Code/Editor/ViewportInteraction.cpp @@ -739,14 +739,32 @@ void ViewportInteraction::MouseWheelEvent(QWheelEvent* ev) bool ViewportInteraction::KeyPressEvent(QKeyEvent* ev) { - if (ev->key() == Qt::Key_Space) + switch (ev->key()) { + case Qt::Key_Space: if (!ev->isAutoRepeat()) { ActivateSpaceBar(); } - return true; + case Qt::Key_Up: + Nudge(ViewportInteraction::NudgeDirection::Up, + (ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow); + return true; + case Qt::Key_Down: + Nudge(ViewportInteraction::NudgeDirection::Down, + (ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow); + return true; + case Qt::Key_Left: + Nudge(ViewportInteraction::NudgeDirection::Left, + (ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow); + return true; + case Qt::Key_Right: + Nudge(ViewportInteraction::NudgeDirection::Right, + (ev->modifiers() & Qt::ShiftModifier) ? ViewportInteraction::NudgeSpeed::Fast : ViewportInteraction::NudgeSpeed::Slow); + return true; + default: + break; } return false; diff --git a/Gems/LyShine/Code/Editor/ViewportWidget.cpp b/Gems/LyShine/Code/Editor/ViewportWidget.cpp index e172d43b60..bf7447585c 100644 --- a/Gems/LyShine/Code/Editor/ViewportWidget.cpp +++ b/Gems/LyShine/Code/Editor/ViewportWidget.cpp @@ -220,6 +220,7 @@ ViewportWidget::ViewportWidget(EditorWindow* parent) InitUiRenderer(); SetupShortcuts(); + installEventFilter(m_editorWindow); // Setup a timer for the maximum refresh rate we want. // Refresh is actually triggered by interaction events and by the IdleUpdate. This avoids the UI @@ -258,6 +259,8 @@ ViewportWidget::~ViewportWidget() LyShinePassDataRequestBus::Handler::BusDisconnect(); AZ::RPI::ViewportContextNotificationBus::Handler::BusDisconnect(); + removeEventFilter(m_editorWindow); + m_uiRenderer.reset(); // Notify LyShine that this is no longer a valid UiRenderer. @@ -688,9 +691,9 @@ void ViewportWidget::wheelEvent(QWheelEvent* ev) Refresh(); } -bool ViewportWidget::event(QEvent* ev) +bool ViewportWidget::eventFilter([[maybe_unused]] QObject* watched, QEvent* event) { - if (ev->type() == QEvent::ShortcutOverride) + if (event->type() == QEvent::ShortcutOverride) { // When a shortcut is matched, Qt's event processing sends out a shortcut override event // to allow other systems to override it. If it's not overridden, then the key events @@ -698,40 +701,48 @@ bool ViewportWidget::event(QEvent* ev) // handler. In our case this causes a problem in preview mode for the Key_Delete event. // So, if we are preview mode avoid treating Key_Delete as a shortcut. - QKeyEvent* keyEvent = static_cast(ev); + QKeyEvent* keyEvent = static_cast(event); int key = keyEvent->key(); // Override the space bar shortcut so that the key gets handled by the viewport's KeyPress/KeyRelease // events when the viewport has the focus. The space bar is set up as a shortcut in order to give the // viewport the focus and activate the space bar when another widget has the focus. Once the shortcut // is pressed and focus is given to the viewport, the viewport takes over handling the space bar via - // the KeyPress/KeyRelease events - if (key == Qt::Key_Space) + // the KeyPress/KeyRelease events. + // Also ignore nudge shortcuts in edit/preview mode so that the KeyPressEvent will be sent. + switch (key) { - ev->accept(); + case Qt::Key_Space: + case Qt::Key_Up: + case Qt::Key_Down: + case Qt::Key_Left: + case Qt::Key_Right: + { + event->accept(); return true; } + default: + { + break; + } + } UiEditorMode editorMode = m_editorWindow->GetEditorMode(); if (editorMode == UiEditorMode::Preview) { - switch (key) + if (key == Qt::Key_Delete) { - case Qt::Key_Delete: - // Ignore nudge shortcuts in preview mode so that the KeyPressEvent will be sent - case Qt::Key_Up: - case Qt::Key_Down: - case Qt::Key_Left: - case Qt::Key_Right: - { - ev->accept(); + event->accept(); return true; } - break; - }; } } - + + return false; +} + +bool ViewportWidget::event(QEvent* ev) +{ bool result = RenderViewportWidget::event(ev); return result; } @@ -742,8 +753,7 @@ void ViewportWidget::keyPressEvent(QKeyEvent* event) if (editorMode == UiEditorMode::Edit) { // in Edit mode just send input to ViewportInteraction - bool handled = m_viewportInteraction->KeyPressEvent(event); - if (!handled) + if (!m_viewportInteraction->KeyPressEvent(event)) { RenderViewportWidget::keyPressEvent(event); } @@ -1246,115 +1256,6 @@ void ViewportWidget::SetupShortcuts() { // Actions with shortcuts are created instead of direct shortcuts because the shortcut dispatcher only looks for matching actions - // Create nudge shortcuts that are active across the entire UI Editor window. Any widgets (such as the spin box widget) that - // handle the same keys and want the shortcut to be ignored need to handle that with a shortcut override event. - // In preview mode, the nudge shortcuts are ignored via the shortcut override event. KeyPressEvents are sent instead, - // and passed along to the canvas - - // Nudge up - { - QAction* action = new QAction("Up", this); - action->setShortcut(QKeySequence(Qt::Key_Up)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Up, ViewportInteraction::NudgeSpeed::Slow); - }); - addAction(action); - } - - // Nudge up fast - { - QAction* action = new QAction("Up Fast", this); - action->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_Up)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Up, ViewportInteraction::NudgeSpeed::Fast); - }); - addAction(action); - } - - // Nudge down - { - QAction* action = new QAction("Down", this); - action->setShortcut(QKeySequence(Qt::Key_Down)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Down, ViewportInteraction::NudgeSpeed::Slow); - }); - addAction(action); - } - - // Nudge down fast - { - QAction* action = new QAction("Down Fast", this); - action->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_Down)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Down, ViewportInteraction::NudgeSpeed::Fast); - }); - addAction(action); - } - - // Nudge left - { - QAction* action = new QAction("Left", this); - action->setShortcut(QKeySequence(Qt::Key_Left)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Left, ViewportInteraction::NudgeSpeed::Slow); - }); - addAction(action); - } - - // Nudge left fast - { - QAction* action = new QAction("Left Fast", this); - action->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_Left)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Left, ViewportInteraction::NudgeSpeed::Fast); - }); - addAction(action); - } - - // Nudge right - { - QAction* action = new QAction("Right", this); - action->setShortcut(QKeySequence(Qt::Key_Right)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Right, ViewportInteraction::NudgeSpeed::Slow); - }); - addAction(action); - } - - // Nudge right fast - { - QAction* action = new QAction("Right Fast", this); - action->setShortcut(QKeySequence(Qt::SHIFT + Qt::Key_Right)); - QObject::connect(action, - &QAction::triggered, - [this]() - { - m_viewportInteraction->Nudge(ViewportInteraction::NudgeDirection::Right, ViewportInteraction::NudgeSpeed::Fast); - }); - addAction(action); - } - // Give the viewport focus and activate the space bar { QAction* action = new QAction("Viewport Focus", this); diff --git a/Gems/LyShine/Code/Editor/ViewportWidget.h b/Gems/LyShine/Code/Editor/ViewportWidget.h index 620cb8fb35..722335aa93 100644 --- a/Gems/LyShine/Code/Editor/ViewportWidget.h +++ b/Gems/LyShine/Code/Editor/ViewportWidget.h @@ -122,12 +122,15 @@ protected: void wheelEvent(QWheelEvent* ev) override; //! Prevents shortcuts from interfering with preview mode. + bool eventFilter(QObject* watched, QEvent* event) override; + + //! Handle events from Qt. bool event(QEvent* ev) override; - //! Key press event from Qt + //! Key press event from Qt. void keyPressEvent(QKeyEvent* event) override; - //! Key release event from Qt + //! Key release event from Qt. void keyReleaseEvent(QKeyEvent* event) override; void focusOutEvent(QFocusEvent* ev) override; diff --git a/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp b/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp index d2a307ecaf..59d764f297 100644 --- a/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp +++ b/Gems/LyShine/Code/Source/World/UiCanvasAssetRefComponent.cpp @@ -239,15 +239,16 @@ void UiCanvasAssetRefComponent::Activate() //////////////////////////////////////////////////////////////////////////////////////////////////// void UiCanvasAssetRefComponent::Deactivate() { -#if !defined(DEDICATED_SERVER) - if (m_canvasEntityId.IsValid()) + if (!gEnv->IsDedicated()) { - gEnv->pLyShine->ReleaseCanvasDeferred(m_canvasEntityId); - m_canvasEntityId.SetInvalid(); - } + if (m_canvasEntityId.IsValid()) + { + gEnv->pLyShine->ReleaseCanvasDeferred(m_canvasEntityId); + m_canvasEntityId.SetInvalid(); + } - UiCanvasAssetRefBus::Handler::BusDisconnect(); - UiCanvasRefBus::Handler::BusDisconnect(); - UiCanvasManagerNotificationBus::Handler::BusDisconnect(); -#endif + UiCanvasAssetRefBus::Handler::BusDisconnect(); + UiCanvasRefBus::Handler::BusDisconnect(); + UiCanvasManagerNotificationBus::Handler::BusDisconnect(); + } } diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerEditorServerBus.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerEditorServerBus.h new file mode 100644 index 0000000000..1c6d2152bb --- /dev/null +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerEditorServerBus.h @@ -0,0 +1,27 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#pragma once + +#include + +namespace Multiplayer +{ + class MultiplayerEditorServerRequests : public AZ::EBusTraits + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Single; + static const AZ::EBusAddressPolicy AddressPolicy = AZ::EBusAddressPolicy::Single; + + //! Sends a packet that initializes a local server launched from the editor. + //! The editor will package the data required for loading the current editor level on the editor-server; data includes entities and asset data. + //! @param connection The connection to the editor-server + virtual void SendEditorServerLevelDataPacket(AzNetworking::IConnection* connection) = 0; + }; + using MultiplayerEditorServerRequestBus = AZ::EBus; +} // namespace Multiplayer diff --git a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml index dd553a2413..b8b880d03c 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/MultiplayerEditor.AutoPackets.xml @@ -4,13 +4,15 @@ - - + + + + - + diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp index 59782b1f4d..aa90b1b17b 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp @@ -45,6 +45,7 @@ namespace Multiplayer void NetworkHierarchyChildComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("NetworkHierarchyChildComponent")); + provided.push_back(AZ_CRC_CE("MultiplayerInputDriver")); } void NetworkHierarchyChildComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 76f4bddb1a..1404484d5c 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -53,6 +53,7 @@ namespace Multiplayer void NetworkHierarchyRootComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("NetworkHierarchyRootComponent")); + provided.push_back(AZ_CRC_CE("MultiplayerInputDriver")); } void NetworkHierarchyRootComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp index a30ab207b4..d2b0ca7095 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -17,7 +18,6 @@ #include #include #include -#include #include #include @@ -35,13 +35,28 @@ namespace Multiplayer m_networkEditorInterface->SetTimeoutMs(AZ::TimeMs{ 0 }); // Disable timeouts on this network interface if (editorsv_isDedicated) { - uint16_t editorServerPort = DefaultServerEditorPort; - if (auto console = AZ::Interface::Get(); console) + uint16_t editorsv_port = DefaultServerEditorPort; + const auto console = AZ::Interface::Get(); + if (console->GetCvarValue("editorsv_port", editorsv_port) != AZ::GetValueResult::Success) { - console->GetCvarValue("editorsv_port", editorServerPort); + AZ_Assert( false, + "MultiplayerEditorConnection failed! Could not find the editorsv_port cvar; we may not be able to connect to the editor's port! Please update this code to use a valid cvar!") + } + + AZ_Assert(m_networkEditorInterface, "MP Editor Network Interface was unregistered before Editor Server could start listening.") + + // Check if there's already an Editor out there waiting to connect + const ConnectionId editorServerToEditorConnectionId = m_networkEditorInterface->Connect(IpAddress(LocalHost.data(), editorsv_port, ProtocolType::Tcp)); + + // If there wasn't an Editor waiting for this server to start, then assume this is an editor-server launched by hand... listen and wait for the editor to request a connection + if (editorServerToEditorConnectionId == InvalidConnectionId) + { + m_networkEditorInterface->Listen(editorsv_port); + } + else + { + m_networkEditorInterface->SendReliablePacket(editorServerToEditorConnectionId, MultiplayerEditorPackets::EditorServerReadyForLevelData()); } - AZ_Assert(m_networkEditorInterface, "MP Editor Network Interface was unregistered before Editor Server could start listening."); - m_networkEditorInterface->Listen(editorServerPort); } } @@ -49,7 +64,7 @@ namespace Multiplayer ( [[maybe_unused]] AzNetworking::IConnection* connection, [[maybe_unused]] const IPacketHeader& packetHeader, - [[maybe_unused]] MultiplayerEditorPackets::EditorServerInit& packet + [[maybe_unused]] MultiplayerEditorPackets::EditorServerLevelData& packet ) { // Editor Server Init is intended for non-release targets @@ -76,7 +91,7 @@ namespace Multiplayer AZ::Data::AssetData* assetDatum = AZ::Utils::LoadObjectFromStream(m_byteStream, nullptr); if (!assetDatum) { - AZLOG_ERROR("EditorServerInit packet contains no asset data. Asset: %s", assetHint.c_str()); + AZLOG_ERROR("EditorServerLevelData packet contains no asset data. Asset: %s", assetHint.c_str()) return false; } assetSize = m_byteStream.GetCurPos() - assetSize; @@ -105,18 +120,21 @@ namespace Multiplayer // Load the level via the root spawnable that was registered const AZ::CVarFixedString loadLevelString = "LoadLevel Root.spawnable"; - AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + const auto console = AZ::Interface::Get(); + console->PerformCommand(loadLevelString.c_str()); // Setup the normal multiplayer connection AZ::Interface::Get()->InitializeMultiplayer(MultiplayerAgentType::DedicatedServer); INetworkInterface* networkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MpNetworkInterfaceName)); - uint16_t serverPort = DefaultServerPort; - if (auto console = AZ::Interface::Get(); console) + uint16_t sv_port = DefaultServerPort; + if (console->GetCvarValue("sv_port", sv_port) != AZ::GetValueResult::Success) { - console->GetCvarValue("sv_port", serverPort); + AZ_Assert(false, + "MultiplayerEditorConnection::HandleRequest for EditorServerLevelData failed! Could not find the sv_port cvar; we won't be able to listen on the correct port for incoming network messages! Please update this code to use a valid cvar!") } - networkInterface->Listen(serverPort); + + networkInterface->Listen(sv_port); AZLOG_INFO("Editor Server completed asset receive, responding to Editor..."); return connection->SendReliablePacket(MultiplayerEditorPackets::EditorServerReady()); @@ -125,6 +143,15 @@ namespace Multiplayer return true; } + bool MultiplayerEditorConnection::HandleRequest( + [[maybe_unused]] AzNetworking::IConnection* connection, + [[maybe_unused]] const AzNetworking::IPacketHeader& packetHeader, + [[maybe_unused]] MultiplayerEditorPackets::EditorServerReadyForLevelData& packet) + { + MultiplayerEditorServerRequestBus::Broadcast(&MultiplayerEditorServerRequestBus::Events::SendEditorServerLevelDataPacket, connection); + return true; + } + bool MultiplayerEditorConnection::HandleRequest ( [[maybe_unused]] AzNetworking::IConnection* connection, @@ -132,23 +159,29 @@ namespace Multiplayer [[maybe_unused]] MultiplayerEditorPackets::EditorServerReady& packet ) { - if (connection->GetConnectionRole() == ConnectionRole::Connector) - { - // Receiving this packet means Editor sync is done, disconnect - connection->Disconnect(AzNetworking::DisconnectReason::TerminatedByClient, AzNetworking::TerminationEndpoint::Local); + // Receiving this packet means Editor sync is done, disconnect + connection->Disconnect(AzNetworking::DisconnectReason::TerminatedByClient, AzNetworking::TerminationEndpoint::Local); + const auto console = AZ::Interface::Get(); + AZ::CVarFixedString editorsv_serveraddr = AZ::CVarFixedString(LocalHost); + uint16_t sv_port = DefaultServerEditorPort; - if (auto console = AZ::Interface::Get(); console) - { - AZ::CVarFixedString remoteAddress; - uint16_t remotePort; - if (console->GetCvarValue("editorsv_serveraddr", remoteAddress) != AZ::GetValueResult::ConsoleVarNotFound && - console->GetCvarValue("sv_port", remotePort) != AZ::GetValueResult::ConsoleVarNotFound) - { - // Connect the Editor to the editor server for Multiplayer simulation - AZ::Interface::Get()->Connect(remoteAddress.c_str(), remotePort); - } - } + if (console->GetCvarValue("sv_port", sv_port) != AZ::GetValueResult::Success) + { + AZ_Assert(false, + "MultiplayerEditorConnection::HandleRequest for EditorServerReady failed! Could not find the sv_port cvar; we may not be able to " + "connect to the correct port for incoming network messages! Please update this code to use a valid cvar!") } + + if (console->GetCvarValue("editorsv_serveraddr", editorsv_serveraddr) != AZ::GetValueResult::Success) + { + AZ_Assert(false, + "MultiplayerEditorConnection::HandleRequest for EditorServerReady failed! Could not find the editorsv_serveraddr cvar; we may not be able to " + "connect to the correct port for incoming network messages! Please update this code to use a valid cvar!") + } + + // Connect the Editor to the editor server for Multiplayer simulation + AZ::Interface::Get()->Connect(editorsv_serveraddr.c_str(), sv_port); + return true; } @@ -171,26 +204,5 @@ namespace Multiplayer { return MultiplayerEditorPackets::DispatchPacket(connection, packetHeader, serializer, *this); } - - void MultiplayerEditorConnection::OnPacketLost([[maybe_unused]] IConnection* connection, [[maybe_unused]] PacketId packetId) - { - ; - } - - void MultiplayerEditorConnection::OnDisconnect([[maybe_unused]] AzNetworking::IConnection* connection, [[maybe_unused]] DisconnectReason reason, [[maybe_unused]] TerminationEndpoint endpoint) - { - bool editorLaunch = false; - if (auto console = AZ::Interface::Get(); console) - { - console->GetCvarValue("editorsv_launch", editorLaunch); - } - - if (editorsv_isDedicated && editorLaunch && m_networkEditorInterface->GetConnectionSet().GetConnectionCount() == 1) - { - if (m_networkEditorInterface->GetPort() != 0) - { - m_networkEditorInterface->StopListening(); - } - } - } + } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h index b93c830cd0..3828d751f0 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorConnection.h @@ -10,8 +10,6 @@ #include -#include -#include #include #include #include @@ -33,7 +31,8 @@ namespace Multiplayer MultiplayerEditorConnection(); ~MultiplayerEditorConnection() = default; - bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerInit& packet); + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerReadyForLevelData& packet); + bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerLevelData& packet); bool HandleRequest(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, MultiplayerEditorPackets::EditorServerReady& packet); //! IConnectionListener interface @@ -41,8 +40,8 @@ namespace Multiplayer AzNetworking::ConnectResult ValidateConnect(const AzNetworking::IpAddress& remoteAddress, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; void OnConnect(AzNetworking::IConnection* connection) override; AzNetworking::PacketDispatchResult OnPacketReceived(AzNetworking::IConnection* connection, const AzNetworking::IPacketHeader& packetHeader, AzNetworking::ISerializer& serializer) override; - void OnPacketLost(AzNetworking::IConnection* connection, AzNetworking::PacketId packetId) override; - void OnDisconnect(AzNetworking::IConnection* connection, AzNetworking::DisconnectReason reason, AzNetworking::TerminationEndpoint endpoint) override; + void OnPacketLost([[maybe_unused]]AzNetworking::IConnection* connection, [[maybe_unused]]AzNetworking::PacketId packetId) override {} + void OnDisconnect([[maybe_unused]]AzNetworking::IConnection* connection, [[maybe_unused]]AzNetworking::DisconnectReason reason, [[maybe_unused]]AzNetworking::TerminationEndpoint endpoint) override {} //! @} private: diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp index 55a05e33a7..2651981bce 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.cpp @@ -71,6 +71,7 @@ namespace Multiplayer { AzFramework::GameEntityContextEventBus::Handler::BusConnect(); AzToolsFramework::EditorEvents::Bus::Handler::BusConnect(); + MultiplayerEditorServerRequestBus::Handler::BusConnect(); AZ::Interface::Get()->AddServerAcceptanceReceivedHandler(m_serverAcceptanceReceivedHandler); } @@ -78,6 +79,7 @@ namespace Multiplayer { AzToolsFramework::EditorEvents::Bus::Handler::BusDisconnect(); AzFramework::GameEntityContextEventBus::Handler::BusDisconnect(); + MultiplayerEditorServerRequestBus::Handler::BusDisconnect(); } void MultiplayerEditorSystemComponent::NotifyRegisterViews() @@ -107,8 +109,8 @@ namespace Multiplayer m_serverProcess->TerminateProcess(0); m_serverProcess = nullptr; } - INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MpEditorInterfaceName)); - if (editorNetworkInterface) + + if (INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MpEditorInterfaceName))) { editorNetworkInterface->Disconnect(m_editorConnId, AzNetworking::DisconnectReason::TerminatedByClient); } @@ -191,53 +193,49 @@ namespace Multiplayer } const AZ::CVarFixedString remoteAddress = editorsv_serveraddr; - if (editorsv_launch && LocalHost == remoteAddress) + if (editorsv_launch) { + if (LocalHost != remoteAddress) + { + AZ_Warning( + "MultiplayerEditor", false, + "Launching EditorServer skipped because incompatible cvars. editorsv_launch=true, meaning you want to launch an editor-server on this machine, but the editorsv_serveraddr is %s instead of the local address (127.0.0.1). " + "Please either set editorsv_launch=false and keep the remote editor-server, or set editorsv_launch=true and editorsv_serveraddr=127.0.0.1.", + remoteAddress.c_str()) + return; + } + + // Begin listening for MPEditor packets before we launch the editor-server. + // The editor-server will send us (the editor) an "EditorServerReadyForLevelData" packet to let us know it's ready to receive data. + INetworkInterface* editorNetworkInterface = + AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MpEditorInterfaceName)); + AZ_Assert(editorNetworkInterface, "MP Editor Network Interface was unregistered before Editor could connect."); + editorNetworkInterface->Listen(editorsv_port); + + // Launch the editor-server m_serverProcess = LaunchEditorServer(); } - - // Spawnable library needs to be rebuilt since now we have newly registered in-memory spawnable assets - AZ::Interface::Get()->BuildSpawnablesList(); - - // Now that the server has launched, attempt to connect the NetworkInterface - INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MpEditorInterfaceName)); - AZ_Assert(editorNetworkInterface, "MP Editor Network Interface was unregistered before Editor could connect."); - m_editorConnId = editorNetworkInterface->Connect( - AzNetworking::IpAddress(remoteAddress.c_str(), editorsv_port, AzNetworking::ProtocolType::Tcp)); - - if (m_editorConnId == AzNetworking::InvalidConnectionId) + else { - AZ_Warning( - "MultiplayerEditor", false, - "Could not connect to server targeted by Editor. If using a local server, check that it's built and editorsv_launch is true."); - return; - } + // Editorsv_launch=false, so we're expecting an editor-server already exists. + // Connect to the editor-server and then send the EditorServerLevelData packet. + INetworkInterface* editorNetworkInterface = AZ::Interface::Get()->RetrieveNetworkInterface(AZ::Name(MpEditorInterfaceName)); + AZ_Assert(editorNetworkInterface, "MP Editor Network Interface was unregistered before Editor could connect.") + + m_editorConnId = editorNetworkInterface->Connect(AzNetworking::IpAddress(remoteAddress.c_str(), editorsv_port, AzNetworking::ProtocolType::Tcp)); - // Read the buffer into EditorServerInit packets until we've flushed the whole thing - byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); - - while (byteStream.GetCurPos() < byteStream.GetLength()) - { - MultiplayerEditorPackets::EditorServerInit packet; - auto& outBuffer = packet.ModifyAssetData(); - - // Size the packet's buffer appropriately - size_t readSize = outBuffer.GetCapacity(); - size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); - if (byteStreamSize < readSize) + if (m_editorConnId == AzNetworking::InvalidConnectionId) { - readSize = byteStreamSize; + AZ_Warning( + "MultiplayerEditor", false, + "Editor multiplayer game-mode failed! Could not connect to an editor-server. editorsv_launch is false so we're assuming you're running your own editor-server at editorsv_serveraddr(%s) on editorsv_port(%i). " + "Either set editorsv_launch=true so the editor launches an editor-server for you, or launch your own editor-server by hand before entering game-mode. Remember editor-servers must use editorsv_isDedicated=true.", + remoteAddress.c_str(), + static_cast < uint16_t>(editorsv_port)) + return; } - outBuffer.Resize(readSize); - byteStream.Read(readSize, outBuffer.GetBuffer()); - - // If we've run out of buffer, mark that we're done - if (byteStream.GetCurPos() == byteStream.GetLength()) - { - packet.SetLastUpdate(true); - } - editorNetworkInterface->SendReliablePacket(m_editorConnId, packet); + SendEditorServerLevelDataPacket(editorNetworkInterface->GetConnectionSet().GetConnection(m_editorConnId)); } } } @@ -253,4 +251,64 @@ namespace Multiplayer // but since we're in Editor, we're already in the level. AZ::Interface::Get()->SendReadyForEntityUpdates(true); } + + void MultiplayerEditorSystemComponent::SendEditorServerLevelDataPacket(AzNetworking::IConnection* connection) + { + const auto prefabEditorEntityOwnershipInterface = AZ::Interface::Get(); + if (!prefabEditorEntityOwnershipInterface) + { + AZ_Error("MultiplayerEditor", prefabEditorEntityOwnershipInterface != nullptr, "PrefabEditorEntityOwnershipInterface unavailable") + return; + } + + const AZStd::vector>& assetData = prefabEditorEntityOwnershipInterface->GetPlayInEditorAssetData(); + + AZStd::vector buffer; + AZ::IO::ByteContainerStream byteStream(&buffer); + + // Serialize Asset information and AssetData into a potentially large buffer + for (const auto& asset : assetData) + { + AZ::Data::AssetId assetId = asset.GetId(); + AZStd::string assetHint = asset.GetHint(); + auto hintSize = aznumeric_cast(assetHint.size()); + + byteStream.Write(sizeof(AZ::Data::AssetId), reinterpret_cast(&assetId)); + byteStream.Write(sizeof(uint32_t), reinterpret_cast(&hintSize)); + byteStream.Write(assetHint.size(), assetHint.data()); + AZ::Utils::SaveObjectToStream(byteStream, AZ::DataStream::ST_BINARY, asset.GetData(), asset.GetData()->GetType()); + } + + // Spawnable library needs to be rebuilt since now we have newly registered in-memory spawnable assets + AZ::Interface::Get()->BuildSpawnablesList(); + + // Read the buffer into EditorServerLevelData packets until we've flushed the whole thing + byteStream.Seek(0, AZ::IO::GenericStream::SeekMode::ST_SEEK_BEGIN); + + while (byteStream.GetCurPos() < byteStream.GetLength()) + { + MultiplayerEditorPackets::EditorServerLevelData editorServerLevelDataPacket; + auto& outBuffer = editorServerLevelDataPacket.ModifyAssetData(); + + // Size the packet's buffer appropriately + size_t readSize = outBuffer.GetCapacity(); + const size_t byteStreamSize = byteStream.GetLength() - byteStream.GetCurPos(); + if (byteStreamSize < readSize) + { + readSize = byteStreamSize; + } + + outBuffer.Resize(readSize); + byteStream.Read(readSize, outBuffer.GetBuffer()); + + // If we've run out of buffer, mark that we're done + if (byteStream.GetCurPos() == byteStream.GetLength()) + { + editorServerLevelDataPacket.SetLastUpdate(true); + } + + connection->SendReliablePacket(editorServerLevelDataPacket); + } + } + } diff --git a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h index 4e4c6b677f..a008afc873 100644 --- a/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/Editor/MultiplayerEditorSystemComponent.h @@ -9,7 +9,7 @@ #pragma once #include - +#include #include #include @@ -35,6 +35,7 @@ namespace Multiplayer , private AzFramework::GameEntityContextEventBus::Handler , private AzToolsFramework::EditorEvents::Bus::Handler , private IEditorNotifyListener + , private MultiplayerEditorServerRequestBus::Handler { public: AZ_COMPONENT(MultiplayerEditorSystemComponent, "{9F335CC0-5574-4AD3-A2D8-2FAEF356946C}"); @@ -73,6 +74,11 @@ namespace Multiplayer void OnGameEntitiesReset() override; //! @} + //! MultiplayerEditorServerRequestBus::Handler + //! @{ + void SendEditorServerLevelDataPacket(AzNetworking::IConnection* connection) override; + //! @} + IEditor* m_editor = nullptr; AzFramework::ProcessWatcher* m_serverProcess = nullptr; AzNetworking::ConnectionId m_editorConnId; diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index a799278203..1376083443 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -13,6 +13,7 @@ set(FILES Include/Multiplayer/MultiplayerConstants.h Include/Multiplayer/MultiplayerStats.h Include/Multiplayer/MultiplayerTypes.h + Include/Multiplayer/MultiplayerEditorServerBus.h Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h Include/Multiplayer/Components/MultiplayerComponent.h Include/Multiplayer/Components/MultiplayerComponentRegistry.h diff --git a/Gems/PhysX/Code/CMakeLists.txt b/Gems/PhysX/Code/CMakeLists.txt index 1acfae3bfd..bb3d7c08a2 100644 --- a/Gems/PhysX/Code/CMakeLists.txt +++ b/Gems/PhysX/Code/CMakeLists.txt @@ -11,12 +11,13 @@ add_subdirectory(NumericalMethods) ly_get_list_relative_pal_filename(pal_source_dir ${CMAKE_CURRENT_LIST_DIR}/Source/Platform/${PAL_PLATFORM_NAME}) include(${pal_source_dir}/PAL_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) # for PAL_TRAIT_PHYSX_SUPPORTED -set(PHYSX_ENABLE_RUNNING_BENCHMARKS OFF CACHE BOOL "Adds a target to allow running of the physx benchmarks.") +set(LY_PHYSX_ENABLE_RUNNING_BENCHMARKS OFF CACHE BOOL "Adds a target to allow running of the physx benchmarks.") if(PAL_TRAIT_PHYSX_SUPPORTED) set(physx_dependency 3rdParty::PhysX) set(physx_files physx_files.cmake) set(physx_shared_files physx_shared_files.cmake) + set(physx_mock_files physx_mocks_files.cmake) set(physx_editor_files physx_editor_files.cmake) else() set(physx_files physx_unsupported_files.cmake) @@ -151,6 +152,17 @@ endif() # Tests ################################################################################ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) + ly_add_target( + NAME PhysX.Mocks HEADERONLY + NAMESPACE Gem + OUTPUT_NAME PhysX.Mocks.Gem + FILES_CMAKE + physx_mocks_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Mocks + ) + ly_add_target( NAME PhysX.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -185,7 +197,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) # Only add the physx benchmarks if this flag is set. The benchmark code is still built, as it is part of the PhysX.Tests project. # Currently jenkins has a 1500sec(25min) timeout, our benchmarks can sometimes take over 1500sec and cause a build failure for timeout. # Jenkins currently doesn't upload the results of the benchmarks, so this is ok. - if(PHYSX_ENABLE_RUNNING_BENCHMARKS) + if(LY_PHYSX_ENABLE_RUNNING_BENCHMARKS) ly_add_googlebenchmark( NAME Gem::PhysX.Benchmarks TARGET Gem::PhysX.Tests @@ -213,6 +225,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzToolsFrameworkTestCommon Gem::PhysX.Static + Gem::PhysX.Mocks Gem::PhysX.Editor.Static RUNTIME_DEPENDENCIES Gem::LmbrCentral.Editor diff --git a/Gems/PhysX/Code/Mocks/PhysX/MockPhysXHeightfieldProviderComponent.h b/Gems/PhysX/Code/Mocks/PhysX/MockPhysXHeightfieldProviderComponent.h new file mode 100644 index 0000000000..7e500881c0 --- /dev/null +++ b/Gems/PhysX/Code/Mocks/PhysX/MockPhysXHeightfieldProviderComponent.h @@ -0,0 +1,74 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#pragma once + +#include +#include +#include +#include +#include + +namespace UnitTest +{ + class MockPhysXHeightfieldProviderComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockPhysXHeightfieldProviderComponent, "{C5F7CCCF-FDB2-40DF-992D-CF028F4A1B59}"); + + static void Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1); + } + } + + void Activate() override + { + } + + void Deactivate() override + { + } + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + }; + + class MockPhysXHeightfieldProvider + : protected Physics::HeightfieldProviderRequestsBus::Handler + { + public: + MockPhysXHeightfieldProvider(AZ::EntityId entityId) + { + Physics::HeightfieldProviderRequestsBus::Handler::BusConnect(entityId); + } + + ~MockPhysXHeightfieldProvider() + { + Physics::HeightfieldProviderRequestsBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD0(GetHeightsAndMaterials, AZStd::vector()); + MOCK_CONST_METHOD0(GetHeightfieldGridSpacing, AZ::Vector2()); + MOCK_CONST_METHOD2(GetHeightfieldGridSize, void(int32_t&, int32_t&)); + MOCK_CONST_METHOD2(GetHeightfieldHeightBounds, void(float&, float&)); + MOCK_CONST_METHOD0(GetHeightfieldTransform, AZ::Transform()); + MOCK_CONST_METHOD0(GetMaterialList, AZStd::vector()); + MOCK_CONST_METHOD0(GetHeights, AZStd::vector()); + MOCK_CONST_METHOD1(UpdateHeights, AZStd::vector(const AZ::Aabb& dirtyRegion)); + MOCK_CONST_METHOD1(UpdateHeightsAndMaterials, AZStd::vector(const AZ::Aabb& dirtyRegion)); + MOCK_CONST_METHOD0(GetHeightfieldAabb, AZ::Aabb()); + }; + +} // namespace UnitTest diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp index 832145d016..59b659a0bf 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.cpp @@ -348,19 +348,7 @@ namespace PhysX LmbrCentral::BoxShapeComponentRequestsBus::EventResult(boxDimensions, GetEntityId(), &LmbrCentral::BoxShapeComponentRequests::GetBoxDimensions); - if (m_shapeType != ShapeType::Box) - { - m_shapeConfigs.clear(); - m_shapeConfigs.emplace_back(AZStd::make_shared(boxDimensions)); - - m_shapeType = ShapeType::Box; - } - else - { - Physics::BoxShapeConfiguration& configuration = - static_cast(*m_shapeConfigs.back()); - configuration = Physics::BoxShapeConfiguration(boxDimensions); - } + SetShapeConfig(ShapeType::Box, Physics::BoxShapeConfiguration(boxDimensions)); m_shapeConfigs.back()->m_scale = scale; m_geometryCache.m_boxDimensions = scale * boxDimensions; @@ -374,19 +362,7 @@ namespace PhysX const Physics::CapsuleShapeConfiguration& capsuleShapeConfig = Utils::ConvertFromLmbrCentralCapsuleConfig(lmbrCentralCapsuleShapeConfig); - if (m_shapeType != ShapeType::Capsule) - { - m_shapeConfigs.clear(); - m_shapeConfigs.emplace_back(AZStd::make_shared(capsuleShapeConfig)); - - m_shapeType = ShapeType::Capsule; - } - else - { - Physics::CapsuleShapeConfiguration& configuration = - static_cast(*m_shapeConfigs.back()); - configuration = capsuleShapeConfig; - } + SetShapeConfig(ShapeType::Capsule, capsuleShapeConfig); m_shapeConfigs.back()->m_scale = scale; const float scalarScale = scale.GetMaxElement(); @@ -400,19 +376,7 @@ namespace PhysX LmbrCentral::SphereShapeComponentRequestsBus::EventResult(radius, GetEntityId(), &LmbrCentral::SphereShapeComponentRequests::GetRadius); - if (m_shapeType != ShapeType::Sphere) - { - m_shapeConfigs.clear(); - m_shapeConfigs.emplace_back(AZStd::make_shared(radius)); - - m_shapeType = ShapeType::Sphere; - } - else - { - Physics::SphereShapeConfiguration& configuration = - static_cast(*m_shapeConfigs.back()); - configuration = Physics::SphereShapeConfiguration(radius); - } + SetShapeConfig(ShapeType::Sphere, Physics::SphereShapeConfiguration(radius)); m_shapeConfigs.back()->m_scale = scale; m_geometryCache.m_radius = scale.GetMaxElement() * radius; @@ -455,19 +419,7 @@ namespace PhysX if (shapeConfig.has_value()) { - if (m_shapeType != ShapeType::Cylinder) - { - m_shapeConfigs.clear(); - m_shapeConfigs.push_back(AZStd::make_shared(shapeConfig.value())); - - m_shapeType = ShapeType::Cylinder; - } - else - { - Physics::CookedMeshShapeConfiguration& configuration = - static_cast(*m_shapeConfigs.back()); - configuration = Physics::CookedMeshShapeConfiguration(shapeConfig.value()); - } + SetShapeConfig(ShapeType::Cylinder, shapeConfig.value()); CreateStaticEditorCollider(); } diff --git a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h index 431b34b1ae..3422d4959f 100644 --- a/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h +++ b/Gems/PhysX/Code/Source/EditorShapeColliderComponent.h @@ -8,6 +8,7 @@ #pragma once +#include #include #include #include @@ -90,12 +91,15 @@ namespace PhysX void UpdateBoxConfig(const AZ::Vector3& scale); void UpdateCapsuleConfig(const AZ::Vector3& scale); void UpdateSphereConfig(const AZ::Vector3& scale); + void UpdateCylinderConfig(const AZ::Vector3& scale); void UpdatePolygonPrismDecomposition(); void UpdatePolygonPrismDecomposition(const AZ::PolygonPrismPtr polygonPrismPtr); - void RefreshUiProperties(); + // Helper function to set a specific shape configuration + template + void SetShapeConfig(ShapeType shapeType, const ConfigType& shapeConfig); - void UpdateCylinderConfig(const AZ::Vector3& scale); + void RefreshUiProperties(); AZ::u32 OnSubdivisionCountChange(); AZ::Crc32 SubdivisionCountVisibility(); @@ -154,4 +158,28 @@ namespace PhysX AZ::NonUniformScaleChangedEvent::Handler m_nonUniformScaleChangedHandler; //!< Responds to changes in non-uniform scale. AZ::Vector3 m_currentNonUniformScale = AZ::Vector3::CreateOne(); //!< Caches the current non-uniform scale. }; + + template + void EditorShapeColliderComponent::SetShapeConfig(ShapeType shapeType, const ConfigType& shapeConfig) + { + if (m_shapeType != shapeType) + { + m_shapeConfigs.clear(); + m_shapeType = shapeType; + } + + if (m_shapeConfigs.empty()) + { + m_shapeConfigs.emplace_back(AZStd::make_shared(shapeConfig)); + } + else + { + AZ_Assert(m_shapeConfigs.back()->GetShapeType() == shapeConfig.GetShapeType(), + "Expected Physics shape configuration with shape type %d but found one with shape type %d.", + static_cast(shapeConfig.GetShapeType()), static_cast(m_shapeConfigs.back()->GetShapeType())); + ConfigType& configuration = + static_cast(*m_shapeConfigs.back()); + configuration = shapeConfig; + } + } } // namespace PhysX diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index b4e1e768c3..fecea57d9c 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -102,7 +102,7 @@ namespace PhysX const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; const float heightScale{ 1.0f / scaleFactor }; - const uint8_t physxMaximumMaterialIndex = 0x7f; + [[maybe_unused]] constexpr uint8_t physxMaximumMaterialIndex = 0x7f; // Delete the cached heightfield object if it is there, and create a new one and save in the shape configuration heightfieldConfig.SetCachedNativeHeightfield(nullptr); diff --git a/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp new file mode 100644 index 0000000000..ef112d8623 --- /dev/null +++ b/Gems/PhysX/Code/Tests/EditorHeightfieldColliderComponentTests.cpp @@ -0,0 +1,215 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using ::testing::NiceMock; +using ::testing::Return; + +namespace PhysXEditorTests +{ + AZStd::vector GetSamples() + { + AZStd::vector samples{ { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 2.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 1.5f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 1.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 0.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight }, + { 3.0f, Physics::QuadMeshType::SubdivideUpperLeftToBottomRight } }; + return samples; + } + + EntityPtr SetupHeightfieldComponent() + { + // create an editor entity with a shape collider component and a box shape component + EntityPtr editorEntity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); + editorEntity->CreateComponent(); + editorEntity->CreateComponent(LmbrCentral::EditorAxisAlignedBoxShapeComponentTypeId); + editorEntity->CreateComponent(); + AZ::ComponentApplicationBus::Broadcast( + &AZ::ComponentApplicationRequests::RegisterComponentDescriptor, + UnitTest::MockPhysXHeightfieldProviderComponent::CreateDescriptor()); + return editorEntity; + } + + void CleanupHeightfieldComponent() + { + AZ::ComponentApplicationBus::Broadcast( + &AZ::ComponentApplicationRequests::UnregisterComponentDescriptor, + UnitTest::MockPhysXHeightfieldProviderComponent::CreateDescriptor()); + } + + void SetupMockMethods(NiceMock& mockShapeRequests) + { + ON_CALL(mockShapeRequests, GetHeightfieldTransform).WillByDefault(Return(AZ::Transform::CreateTranslation({ 1, 2, 0 }))); + ON_CALL(mockShapeRequests, GetHeightfieldGridSpacing).WillByDefault(Return(AZ::Vector2(1, 1))); + ON_CALL(mockShapeRequests, GetHeightsAndMaterials).WillByDefault(Return(GetSamples())); + ON_CALL(mockShapeRequests, GetHeightfieldGridSize) + .WillByDefault( + [](int32_t& numColumns, int32_t& numRows) + { + numColumns = 3; + numRows = 3; + }); + ON_CALL(mockShapeRequests, GetHeightfieldHeightBounds) + .WillByDefault( + [](float& x, float& y) + { + x = -3.0f; + y = 3.0f; + }); + } + + EntityPtr TestCreateActiveGameEntityFromEditorEntity(AZ::Entity* editorEntity) + { + EntityPtr gameEntity = AZStd::make_unique(); + AzToolsFramework::ToolsApplicationRequestBus::Broadcast( + &AzToolsFramework::ToolsApplicationRequests::PreExportEntity, *editorEntity, *gameEntity); + gameEntity->Init(); + return gameEntity; + } + + + TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentDependenciesSatisfiedEntityIsValid) + { + EntityPtr entity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); + entity->CreateComponent(); + entity->CreateComponent(LmbrCentral::EditorAxisAlignedBoxShapeComponentTypeId); + entity->CreateComponent()->CreateDescriptor(); + + // the entity should be in a valid state because the shape component and + // the Terrain Physics Collider Component requirement is satisfied. + AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); + EXPECT_TRUE(sortOutcome.IsSuccess()); + } + + TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentDependenciesMissingEntityIsInvalid) + { + EntityPtr entity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); + entity->CreateComponent(); + + // the entity should not be in a valid state because the heightfield collider component requires + // a shape component and the Terrain Physics Collider Component + AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); + EXPECT_FALSE(sortOutcome.IsSuccess()); + EXPECT_TRUE(sortOutcome.GetError().m_code == AZ::Entity::DependencySortResult::MissingRequiredService); + } + + TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentMultipleHeightfieldColliderComponentsEntityIsInvalid) + { + EntityPtr entity = CreateInactiveEditorEntity("HeightfieldColliderComponentEditorEntity"); + entity->CreateComponent(); + entity->CreateComponent(LmbrCentral::EditorAxisAlignedBoxShapeComponentTypeId); + + // adding a second heightfield collider component should make the entity invalid + entity->CreateComponent(); + + AZ::Entity::DependencySortOutcome sortOutcome = entity->EvaluateDependenciesGetDetails(); + EXPECT_FALSE(sortOutcome.IsSuccess()); + EXPECT_TRUE(sortOutcome.GetError().m_code == AZ::Entity::DependencySortResult::HasIncompatibleServices); + } + + TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentHeightfieldColliderWithCorrectComponentsCorrectRuntimeComponents) + { + EntityPtr editorEntity = SetupHeightfieldComponent(); + NiceMock mockShapeRequests(editorEntity->GetId()); + SetupMockMethods(mockShapeRequests); + editorEntity->Activate(); + + EntityPtr gameEntity = TestCreateActiveGameEntityFromEditorEntity(editorEntity.get()); + NiceMock mockShapeRequests2(gameEntity->GetId()); + SetupMockMethods(mockShapeRequests2); + gameEntity->Activate(); + + // check that the runtime entity has the expected components + EXPECT_TRUE(gameEntity->FindComponent() != nullptr); + EXPECT_TRUE(gameEntity->FindComponent() != nullptr); + EXPECT_TRUE(gameEntity->FindComponent(LmbrCentral::AxisAlignedBoxShapeComponentTypeId) != nullptr); + + CleanupHeightfieldComponent(); + } + + TEST_F(PhysXEditorFixture, EditorHeightfieldColliderComponentHeightfieldColliderWithAABoxCorrectRuntimeGeometry) + { + EntityPtr editorEntity = SetupHeightfieldComponent(); + NiceMock mockShapeRequests(editorEntity->GetId()); + SetupMockMethods(mockShapeRequests); + editorEntity->Activate(); + + EntityPtr gameEntity = TestCreateActiveGameEntityFromEditorEntity(editorEntity.get()); + NiceMock mockShapeRequests2(gameEntity->GetId()); + SetupMockMethods(mockShapeRequests2); + gameEntity->Activate(); + + AzPhysics::SimulatedBody* staticBody = nullptr; + AzPhysics::SimulatedBodyComponentRequestsBus::EventResult( + staticBody, gameEntity->GetId(), &AzPhysics::SimulatedBodyComponentRequests::GetSimulatedBody); + const auto* pxRigidStatic = static_cast(staticBody->GetNativePointer()); + + PHYSX_SCENE_READ_LOCK(pxRigidStatic->getScene()); + + // there should be a single shape on the rigid body and it should be a heightfield + EXPECT_EQ(pxRigidStatic->getNbShapes(), 1); + + physx::PxShape* shape = nullptr; + pxRigidStatic->getShapes(&shape, 1, 0); + EXPECT_EQ(shape->getGeometryType(), physx::PxGeometryType::eHEIGHTFIELD); + + physx::PxHeightFieldGeometry heightfieldGeometry; + shape->getHeightFieldGeometry(heightfieldGeometry); + + physx::PxHeightField* heightfield = heightfieldGeometry.heightField; + + int32_t numRows{ 0 }; + int32_t numColumns{ 0 }; + Physics::HeightfieldProviderRequestsBus::Event( + gameEntity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); + EXPECT_EQ(numColumns, heightfield->getNbColumns()); + EXPECT_EQ(numRows, heightfield->getNbRows()); + + for (int sampleRow = 0; sampleRow < numRows; ++sampleRow) + { + for (int sampleColumn = 0; sampleColumn < numColumns; ++sampleColumn) + { + float minHeightBounds{ 0.0f }; + float maxHeightBounds{ 0.0f }; + Physics::HeightfieldProviderRequestsBus::Event( + gameEntity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, + maxHeightBounds); + + AZStd::vector samples; + Physics::HeightfieldProviderRequestsBus::EventResult( + samples, gameEntity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); + const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f }; + const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; + + physx::PxHeightFieldSample samplePhysX = heightfield->getSample(sampleRow, sampleColumn); + Physics::HeightMaterialPoint samplePhysics = samples[sampleRow * numColumns + sampleColumn]; + EXPECT_EQ(samplePhysX.height, azlossy_cast(samplePhysics.m_height * scaleFactor)); + } + } + CleanupHeightfieldComponent(); + } + +} // namespace PhysXEditorTests + diff --git a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp index fce427d9d3..06ba4f3e9c 100644 --- a/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp +++ b/Gems/PhysX/Code/Tests/ShapeColliderComponentTests.cpp @@ -320,7 +320,7 @@ namespace PhysXEditorTests TEST_F(PhysXEditorFixture, EditorShapeColliderComponent_ShapeColliderWithCylinderWithNullHeight_HandledGracefully) { - ValidateInvalidEditorShapeColliderComponentParams(0.f, 1.f); + ValidateInvalidEditorShapeColliderComponentParams(1.f, 0.f); } TEST_F(PhysXEditorFixture, EditorShapeColliderComponent_ShapeColliderWithCylinderWithNullRadiusAndNullHeight_HandledGracefully) @@ -338,6 +338,44 @@ namespace PhysXEditorTests ValidateInvalidEditorShapeColliderComponentParams(0.f, -1.f); } + TEST_F(PhysXEditorFixture, EditorShapeColliderComponent_ShapeColliderWithCylinderSwitchingFromNullHeightToValidHeight_HandledGracefully) + { + // create an editor entity with a shape collider component and a cylinder shape component + EntityPtr editorEntity = CreateInactiveEditorEntity("ShapeColliderComponentEditorEntity"); + editorEntity->CreateComponent(); + editorEntity->CreateComponent(LmbrCentral::EditorCylinderShapeComponentTypeId); + editorEntity->Activate(); + + const float validRadius = 1.0f; + const float nullHeight = 0.0f; + const float validHeight = 1.0f; + + LmbrCentral::CylinderShapeComponentRequestsBus::Event(editorEntity->GetId(), + &LmbrCentral::CylinderShapeComponentRequests::SetRadius, validRadius); + + { + UnitTest::ErrorHandler dimensionWarningHandler("Negative or zero cylinder dimensions are invalid"); + UnitTest::ErrorHandler colliderWarningHandler("No Collider or Shape information found when creating Rigid body"); + + LmbrCentral::CylinderShapeComponentRequestsBus::Event(editorEntity->GetId(), + &LmbrCentral::CylinderShapeComponentRequests::SetHeight, nullHeight); + + EXPECT_EQ(dimensionWarningHandler.GetExpectedWarningCount(), 1); + EXPECT_EQ(colliderWarningHandler.GetExpectedWarningCount(), 1); + } + + { + UnitTest::ErrorHandler dimensionWarningHandler("Negative or zero cylinder dimensions are invalid"); + UnitTest::ErrorHandler colliderWarningHandler("No Collider or Shape information found when creating Rigid body"); + + LmbrCentral::CylinderShapeComponentRequestsBus::Event(editorEntity->GetId(), + &LmbrCentral::CylinderShapeComponentRequests::SetHeight, validHeight); + + EXPECT_EQ(dimensionWarningHandler.GetExpectedWarningCount(), 0); + EXPECT_EQ(colliderWarningHandler.GetExpectedWarningCount(), 0); + } + } + TEST_F(PhysXEditorFixture, EditorShapeColliderComponent_ShapeColliderWithBoxAndRigidBody_CorrectRuntimeComponents) { // create an editor entity with a shape collider component and a box shape component diff --git a/Gems/PhysX/Code/physx_editor_tests_files.cmake b/Gems/PhysX/Code/physx_editor_tests_files.cmake index 36fb139514..953e2a167e 100644 --- a/Gems/PhysX/Code/physx_editor_tests_files.cmake +++ b/Gems/PhysX/Code/physx_editor_tests_files.cmake @@ -18,6 +18,7 @@ set(FILES Tests/PolygonPrismMeshUtilsTest.cpp Tests/PhysXColliderComponentModeTests.cpp Tests/ShapeColliderComponentTests.cpp + Tests/EditorHeightfieldColliderComponentTests.cpp Tests/TestColliderComponent.h Tests/SystemComponentTest.cpp Tests/RigidBodyComponentTests.cpp diff --git a/Gems/PhysX/Code/physx_mocks_files.cmake b/Gems/PhysX/Code/physx_mocks_files.cmake new file mode 100644 index 0000000000..49843eeb6f --- /dev/null +++ b/Gems/PhysX/Code/physx_mocks_files.cmake @@ -0,0 +1,11 @@ +# +# Copyright (c) Contributors to the Open 3D Engine Project. +# For complete copyright and license terms please see the LICENSE at the root of this distribution. +# +# SPDX-License-Identifier: Apache-2.0 OR MIT +# +# + +set(FILES + Mocks/PhysX/MockPhysXHeightfieldProviderComponent.h +) diff --git a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype index 01b862c4f8..235079d95e 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype @@ -92,13 +92,58 @@ { "id": "heightmapImage", "displayName": "Heightmap Image", - "description": "Heightmap of the terrain, controlled by the runtime.", + "description": "Heightmap of the terrain. Controlled by the runtime.", + "visibility": "Hidden", "type": "Image", "connection": { "type": "ShaderInput", "id": "m_heightmapImage" } }, + { + "id": "detailMaterialIdImage", + "displayName": "Detail Material Id Image", + "description": "Texture containing detail material Ids and weights. Controlled by the runtime.", + "visibility": "Hidden", + "type": "Image", + "connection": { + "type": "ShaderInput", + "id": "m_detailMaterialIdImage" + } + }, + { + "id": "detailMaterialIdCenter", + "displayName": "Detail Material Id Image Center", + "description": "The center position of the detail material Id image. Controlled by the runtime.", + "visibility": "Hidden", + "type": "Vector2", + "connection": { + "type": "ShaderInput", + "id": "m_detailMaterialIdImageCenter" + } + }, + { + "id": "detailAabb", + "displayName": "Detail material bounds in 2d", + "description": "The 2d world space bounds of the detail id material. Controlled by the runtime.", + "visibility": "Hidden", + "type": "Vector4", + "connection": { + "type": "ShaderInput", + "id": "m_detailAabb" + } + }, + { + "id": "detailHalfPixelUv", + "displayName": "Detail texture half pixel uv size", + "description": "Uv size of a half pixel in the detail material id texture. Controlled by the runtime.", + "visibility": "Hidden", + "type": "float", + "connection": { + "type": "ShaderInput", + "id": "m_detailHalfPixelUv" + } + }, { "id": "detailTextureMultiplier", "displayName": "Detail Texture UV Multiplier", diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli index 72c1af953c..dc6f65207b 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainCommon.azsli @@ -93,10 +93,15 @@ ShaderResourceGroup ObjectSrg : SRG_PerObject ShaderResourceGroup TerrainMaterialSrg : SRG_PerMaterial { Texture2D m_heightmapImage; + Texture2D m_detailMaterialIdImage; + float2 m_detailMaterialIdImageCenter; float m_detailTextureMultiplier; float m_detailFadeDistance; float m_detailFadeLength; + float4 m_detailAabb; + float m_detailHalfPixelUv; + Sampler HeightmapSampler { MinFilter = Linear; @@ -117,6 +122,15 @@ ShaderResourceGroup TerrainMaterialSrg : SRG_PerMaterial MaxAnisotropy = 16; }; + Sampler m_detailSampler + { + AddressU = Wrap; + AddressV = Wrap; + MinFilter = Point; + MagFilter = Point; + MipFilter = Point; + }; + // Base Color float3 m_baseColor; float m_baseColorFactor; diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl index 750cd2fb29..a15ad931f6 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl @@ -17,6 +17,7 @@ #include #include #include +#include struct VSOutput { @@ -29,6 +30,8 @@ struct VSOutput float2 m_uv : UV1; }; +option bool o_debugDetailMaterialIds = false; + VSOutput TerrainPBR_MainPassVS(VertexInput IN) { VSOutput OUT; @@ -121,6 +124,48 @@ ForwardPassOutput TerrainPBR_MainPassPS(VSOutput IN) float3 detailColor = GetBaseColorInput(TerrainMaterialSrg::m_baseColorMap, TerrainMaterialSrg::m_sampler, detailUv, TerrainMaterialSrg::m_baseColor.rgb, o_baseColor_useTexture); float3 blendedColor = BlendBaseColor(lerp(detailColor, TerrainMaterialSrg::m_baseColor.rgb, detailFactor), macroColor, TerrainMaterialSrg::m_baseColorFactor, o_baseColorTextureBlendMode, o_baseColor_useTexture); + // ------- Debug detail materials using random colors ------- + // This assigns a random color to each material, turns off any kind of distance fading, and draws a black line at the texture edges. + if (o_debugDetailMaterialIds) + { + float2 detailRegionMin = TerrainMaterialSrg::m_detailAabb.xy; + float2 detailRegionMax = TerrainMaterialSrg::m_detailAabb.zw; + float2 detailRegionUv = (surface.position.xy - detailRegionMin) / (detailRegionMax - detailRegionMin); + if (all(detailRegionUv > TerrainMaterialSrg::m_detailHalfPixelUv) && all(detailRegionUv < 1.0 - TerrainMaterialSrg::m_detailHalfPixelUv)) + { + detailRegionUv += TerrainMaterialSrg::m_detailMaterialIdImageCenter - (0.5); + + uint material1 = TerrainMaterialSrg::m_detailMaterialIdImage.GatherRed(TerrainMaterialSrg::m_detailSampler, detailRegionUv, 0).r; + uint material2 = TerrainMaterialSrg::m_detailMaterialIdImage.GatherGreen(TerrainMaterialSrg::m_detailSampler, detailRegionUv, 0).r; + float blend = float(TerrainMaterialSrg::m_detailMaterialIdImage.GatherBlue(TerrainMaterialSrg::m_detailSampler, detailRegionUv, 0).r) / 0xFF; + + float3 material1Color = float3(0.1, 0.1, 0.1); + float3 material2Color = float3(0.1, 0.1, 0.1); + + // Get a reasonably random hue for the material id + if (material1 != 255) + { + float hue1 = (material1 * 25043 % 256) / 256.0; + material1Color = HsvToRgb(float3(hue1, 1.0, 1.0)); + } + if (material2 != 255) + { + float hue2 = (material2 * 25043 % 256) / 256.0; + material2Color = HsvToRgb(float3(hue2, 1.0, 1.0)); + } + + blendedColor = lerp(material1Color, material2Color, blend); + float seamBlend = 0.0; + const float halfLineWidth = 1.0 / 2048.0; + if (any(abs(detailRegionUv) % 1.0 < halfLineWidth) || any(abs(detailRegionUv) % 1.0 > 1.0 - halfLineWidth)) + { + seamBlend = 1.0; + } + blendedColor = lerp(blendedColor, float3(0.0, 0.0, 0.0), seamBlend); // draw texture seams + blendedColor = pow(blendedColor , 2.2); + } + } + // ------- Specular ------- float specularF0Factor = GetSpecularInput(TerrainMaterialSrg::m_specularF0Map, TerrainMaterialSrg::m_sampler, detailUv, TerrainMaterialSrg::m_specularF0Factor, o_specularF0_useTexture); specularF0Factor = lerp(specularF0Factor, 0.5, detailFactor); diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h index 53d93be8ea..f9607fdafb 100644 --- a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h @@ -34,7 +34,8 @@ namespace UnitTest MOCK_METHOD1(RegisterArea, void(AZ::EntityId areaId)); MOCK_METHOD1(UnregisterArea, void(AZ::EntityId areaId)); - MOCK_METHOD1(RefreshArea, void(AZ::EntityId areaId)); + MOCK_METHOD2(RefreshArea, + void(AZ::EntityId areaId, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask)); }; class MockTerrainDataNotificationListener : public AzFramework::Terrain::TerrainDataNotificationBus::Handler diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp index 01ad0bb77f..231d5abc28 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp @@ -119,7 +119,9 @@ namespace Terrain LmbrCentral::DependencyNotificationBus::Handler::BusDisconnect(); // Since this height data will no longer exist, notify the terrain system to refresh the area. - TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId()); + TerrainSystemServiceRequestBus::Broadcast( + &TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId(), + AzFramework::Terrain::TerrainDataNotifications::HeightData); } bool TerrainHeightGradientListComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) @@ -176,7 +178,9 @@ namespace Terrain void TerrainHeightGradientListComponent::OnCompositionChanged() { RefreshMinMaxHeights(); - TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId()); + TerrainSystemServiceRequestBus::Broadcast( + &TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId(), + AzFramework::Terrain::TerrainDataNotifications::HeightData); } void TerrainHeightGradientListComponent::RefreshMinMaxHeights() diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp index 00ed9c004f..c3803c25e8 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp @@ -157,6 +157,12 @@ namespace Terrain void TerrainLayerSpawnerComponent::RefreshArea() { - TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId()); + using Terrain = AzFramework::Terrain::TerrainDataNotifications; + + // Notify the terrain system that the entire layer has changed, so both height and surface data can be affected. + TerrainSystemServiceRequestBus::Broadcast( + &TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId(), + static_cast(Terrain::HeightData | Terrain::SurfaceData) + ); } } diff --git a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp index 5b76f15d74..748221d69e 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainSurfaceGradientListComponent.cpp @@ -184,7 +184,9 @@ namespace Terrain void TerrainSurfaceGradientListComponent::OnCompositionChanged() { - TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId()); + TerrainSystemServiceRequestBus::Broadcast( + &TerrainSystemServiceRequestBus::Events::RefreshArea, GetEntityId(), + AzFramework::Terrain::TerrainDataNotifications::SurfaceData); } } // namespace Terrain diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp index d8b7308ef7..bd65cf6abc 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldComponent.cpp @@ -32,16 +32,22 @@ namespace Terrain AZ::EditContext* edit = serialize->GetEditContext(); if (edit) { - edit->Class( - "Terrain World Component", "Data required for the terrain system to run") + edit->Class("Terrain World Component", "Data required for the terrain system to run") ->ClassElement(AZ::Edit::ClassElements::EditorData, "") ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZStd::vector({ AZ_CRC_CE("Level") })) ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) ->Attribute(AZ::Edit::Attributes::AutoExpand, true) ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_worldMin, "World Bounds (Min)", "") + // Temporary constraint until the rest of the Terrain system is updated to support larger worlds. + ->Attribute(AZ::Edit::Attributes::Min, -2048.0f) + ->Attribute(AZ::Edit::Attributes::Max, 2048.0f) ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_worldMax, "World Bounds (Max)", "") - ->DataElement(AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_heightQueryResolution, "Height Query Resolution (m)", "") + // Temporary constraint until the rest of the Terrain system is updated to support larger worlds. + ->Attribute(AZ::Edit::Attributes::Min, -2048.0f) + ->Attribute(AZ::Edit::Attributes::Max, 2048.0f) + ->DataElement( + AZ::Edit::UIHandlers::Default, &TerrainWorldConfig::m_heightQueryResolution, "Height Query Resolution (m)", "") ; } } diff --git a/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h b/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h index 354b2fde34..140b830da1 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainWorldRendererComponent.h @@ -46,7 +46,7 @@ namespace Terrain WorldSizeCount, }; - WorldSize m_worldSize; + WorldSize m_worldSize = WorldSize::_1024Meters; }; diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp index 271919070c..3c6a213b02 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.cpp @@ -123,20 +123,23 @@ namespace Terrain { surfaceMaterialMapping.m_active = false; surfaceMaterialMapping.m_materialAsset.QueueLoad(); - AZ::Data::AssetBus::Handler::BusConnect(surfaceMaterialMapping.m_materialAsset.GetId()); + AZ::Data::AssetBus::MultiHandler::BusConnect(surfaceMaterialMapping.m_materialAsset.GetId()); } } + + // Announce initial shape using OnShapeChanged + OnShapeChanged(LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); } void TerrainSurfaceMaterialsListComponent::Deactivate() { TerrainAreaMaterialRequestBus::Handler::BusDisconnect(); + AZ::Data::AssetBus::MultiHandler::BusDisconnect(); for (auto& surfaceMaterialMapping : m_configuration.m_surfaceMaterials) { if (surfaceMaterialMapping.m_materialAsset.GetId().IsValid()) { - AZ::Data::AssetBus::Handler::BusDisconnect(surfaceMaterialMapping.m_materialAsset.GetId()); surfaceMaterialMapping.m_materialAsset.Release(); surfaceMaterialMapping.m_materialInstance.reset(); surfaceMaterialMapping.m_activeMaterialAssetId = AZ::Data::AssetId(); @@ -202,7 +205,7 @@ namespace Terrain // Don't disconnect from the AssetBus if this material is mapped more than once. if (CountMaterialIDInstances(surfaceMaterialMapping.m_activeMaterialAssetId) == 1) { - AZ::Data::AssetBus::Handler::BusDisconnect(surfaceMaterialMapping.m_activeMaterialAssetId); + AZ::Data::AssetBus::MultiHandler::BusDisconnect(surfaceMaterialMapping.m_activeMaterialAssetId); } surfaceMaterialMapping.m_activeMaterialAssetId = AZ::Data::AssetId(); @@ -273,12 +276,14 @@ namespace Terrain &TerrainAreaMaterialNotificationBus::Events::OnTerrainSurfaceMaterialMappingRegionChanged, GetEntityId(), oldAabb, m_cachedAabb); } - - const AZStd::vector& TerrainSurfaceMaterialsListComponent::GetSurfaceMaterialMappings( - AZ::Aabb& region) const + + const AZ::Aabb& TerrainSurfaceMaterialsListComponent::GetTerrainSurfaceMaterialRegion() const { - region = m_cachedAabb; + return m_cachedAabb; + } + const AZStd::vector& TerrainSurfaceMaterialsListComponent::GetSurfaceMaterialMappings() const + { return m_configuration.m_surfaceMaterials; } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h index 7c36033c41..0e32cb12c0 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/Components/TerrainSurfaceMaterialsListComponent.h @@ -53,7 +53,7 @@ namespace Terrain class TerrainSurfaceMaterialsListComponent : public AZ::Component , private TerrainAreaMaterialRequestBus::Handler - , private AZ::Data::AssetBus::Handler + , private AZ::Data::AssetBus::MultiHandler , private LmbrCentral::ShapeComponentNotificationsBus::Handler { public: @@ -86,7 +86,8 @@ namespace Terrain ////////////////////////////////////////////////////////////////////////// // TerrainAreaMaterialRequestBus - const AZStd::vector& GetSurfaceMaterialMappings(AZ::Aabb& region) const override; + const AZ::Aabb& GetTerrainSurfaceMaterialRegion() const override; + const AZStd::vector& GetSurfaceMaterialMappings() const override; ////////////////////////////////////////////////////////////////////////// // AZ::Data::AssetBus::Handler diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h index dfedf0b34a..ae36a2639e 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainAreaMaterialRequestBus.h @@ -9,13 +9,9 @@ #pragma once #include - #include - #include -#include - namespace Terrain { //! This bus provides retrieval of information from Terrain Surfaces. @@ -30,8 +26,11 @@ namespace Terrain virtual ~TerrainAreaMaterialRequests() = default; + //! Get the Aabb for the region where a TerrainSurfaceMaterialMapping exists + virtual const AZ::Aabb& GetTerrainSurfaceMaterialRegion() const = 0; + //! Get the Material asset assigned to a particular surface tag. - virtual const AZStd::vector& GetSurfaceMaterialMappings(AZ::Aabb& region) const = 0; + virtual const AZStd::vector& GetSurfaceMaterialMappings() const = 0; }; using TerrainAreaMaterialRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp index e6aed28897..a96fa63c13 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.cpp @@ -7,11 +7,13 @@ */ #include +#include +#include +#include #include #include #include -#include #include @@ -45,12 +47,49 @@ namespace Terrain { [[maybe_unused]] const char* TerrainFPName = "TerrainFeatureProcessor"; const char* TerrainHeightmapChars = "TerrainHeightmap"; + const char* TerrainDetailChars = "TerrainDetail"; } namespace MaterialInputs { // Terrain material static const char* const HeightmapImage("settings.heightmapImage"); + static const char* const DetailMaterialIdImage("settings.detailMaterialIdImage"); + static const char* const DetailCenter("settings.detailMaterialIdCenter"); + static const char* const DetailAabb("settings.detailAabb"); + static const char* const DetailHalfPixelUv("settings.detailHalfPixelUv"); + } + + namespace DetailMaterialInputs + { + static const char* const BaseColorMap("baseColor.textureMap"); + static const char* const BaseColorUseTexture("baseColor.useTexture"); + static const char* const BaseColorFactor("baseColor.factor"); + static const char* const BaseColorBlendMode("baseColor.textureBlendMode"); + static const char* const MetallicMap("metallic.textureMap"); + static const char* const MetallicUseTexture("metallic.useTexture"); + static const char* const MetallicFactor("metallic.factor"); + static const char* const RoughnessMap("roughness.textureMap"); + static const char* const RoughnessUseTexture("roughness.useTexture"); + static const char* const RoughnessFactor("roughness.factor"); + static const char* const RoughnessUpperBound("roughness.lowerBound"); + static const char* const RoughnessLowerBound("roughness.upperBound"); + static const char* const SpecularF0Map("specularF0.textureMap"); + static const char* const SpecularF0UseTexture("specularF0.useTexture"); + static const char* const SpecularF0Factor("specularF0.factor"); + static const char* const NormalMap("normal.textureMap"); + static const char* const NormalUseTexture("normal.useTexture"); + static const char* const NormalFactor("normal.factor"); + static const char* const NormalFlipX("normal.flipX"); + static const char* const NormalFlipY("normal.flipY"); + static const char* const DiffuseOcclusionMap("occlusion.diffuseTextureMap"); + static const char* const DiffuseOcclusionUseTexture("occlusion.diffuseUseTexture"); + static const char* const DiffuseOcclusionFactor("occlusion.diffuseFactor"); + static const char* const HeightMap("parallax.textureMap"); + static const char* const HeightUseTexture("parallax.useTexture"); + static const char* const HeightFactor("parallax.factor"); + static const char* const HeightOffset("parallax.offset"); + static const char* const HeightBlendFactor("parallax.blendFactor"); } namespace ShaderInputs @@ -62,6 +101,17 @@ namespace Terrain static const char* const MacroColorMap("m_macroColorMap"); static const char* const MacroNormalMap("m_macroNormalMap"); } + + AZ_CVAR(bool, + r_terrainDebugDetailMaterials, + false, + [](const bool& value) + { + AZ::RPI::ShaderSystemInterface::Get()->SetGlobalShaderOption(AZ::Name{ "o_debugDetailMaterialIds" }, AZ::RPI::ShaderOptionValue{ value }); + }, + AZ::ConsoleFunctorFlags::Null, + "Turns on debugging for detail material ids for terrain." + ); void TerrainFeatureProcessor::Reflect(AZ::ReflectContext* context) @@ -78,6 +128,12 @@ namespace Terrain { Initialize(); AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + + m_handleGlobalShaderOptionUpdate = AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler + { + [this](const AZ::Name&, AZ::RPI::ShaderOptionValue) { m_forceRebuildDrawPackets = true; } + }; + AZ::RPI::ShaderSystemInterface::Get()->Connect(m_handleGlobalShaderOptionUpdate); } void TerrainFeatureProcessor::Initialize() @@ -139,11 +195,18 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) { - if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) == 0) + if ((dataChangedMask & (TerrainDataChangedMask::HeightData | TerrainDataChangedMask::Settings)) != 0) { - return; + TerrainHeightOrSettingsUpdated(dirtyRegion); } + if ((dataChangedMask & TerrainDataChangedMask::SurfaceData) != 0) + { + TerrainSurfaceDataUpdated(dirtyRegion); + } + } + void TerrainFeatureProcessor::TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion) + { AZ::Aabb worldBounds = AZ::Aabb::CreateNull(); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( worldBounds, &AzFramework::Terrain::TerrainDataRequests::GetTerrainAabb); @@ -155,9 +218,11 @@ namespace Terrain const AZ::Transform transform = AZ::Transform::CreateTranslation(worldBounds.GetCenter()); - AZ::Vector2 queryResolution = AZ::Vector2(1.0f); + AZ::Vector2 queryResolution2D = AZ::Vector2(1.0f); AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( - queryResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + queryResolution2D, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + // Currently query resolution is multidimensional but the rendering system only supports this changing in one dimension. + float queryResolution = queryResolution2D.GetX(); // Sectors need to be rebuilt if the world bounds change in the x/y, or the sample spacing changes. m_areaData.m_rebuildSectors = m_areaData.m_rebuildSectors || @@ -165,22 +230,22 @@ namespace Terrain m_areaData.m_terrainBounds.GetMin().GetY() != worldBounds.GetMin().GetY() || m_areaData.m_terrainBounds.GetMax().GetX() != worldBounds.GetMax().GetX() || m_areaData.m_terrainBounds.GetMax().GetY() != worldBounds.GetMax().GetY() || - m_areaData.m_sampleSpacing != queryResolution.GetX(); + m_areaData.m_sampleSpacing != queryResolution; m_areaData.m_transform = transform; m_areaData.m_terrainBounds = worldBounds; - m_areaData.m_heightmapImageWidth = aznumeric_cast(worldBounds.GetXExtent() / queryResolution.GetX()); - m_areaData.m_heightmapImageHeight = aznumeric_cast(worldBounds.GetYExtent() / queryResolution.GetY()); - m_areaData.m_updateWidth = aznumeric_cast(m_dirtyRegion.GetXExtent() / queryResolution.GetX()); - m_areaData.m_updateHeight = aznumeric_cast(m_dirtyRegion.GetYExtent() / queryResolution.GetY()); - // Currently query resolution is multidimensional but the rendering system only supports this changing in one dimension. - m_areaData.m_sampleSpacing = queryResolution.GetX(); + m_areaData.m_sampleSpacing = queryResolution; m_areaData.m_heightmapUpdated = true; } - + + void TerrainFeatureProcessor::TerrainSurfaceDataUpdated(const AZ::Aabb& dirtyRegion) + { + m_dirtyDetailRegion.AddAabb(dirtyRegion); + } + void TerrainFeatureProcessor::OnTerrainMacroMaterialCreated(AZ::EntityId entityId, const MacroMaterialData& newMaterialData) { - MacroMaterialData& materialData = FindOrCreateMacroMaterial(entityId); + MacroMaterialData& materialData = FindOrCreateByEntityId(entityId, m_macroMaterials); UpdateMacroMaterialData(materialData, newMaterialData); @@ -197,14 +262,14 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& newMaterialData) { - MacroMaterialData& data = FindOrCreateMacroMaterial(entityId); + MacroMaterialData& data = FindOrCreateByEntityId(entityId, m_macroMaterials); UpdateMacroMaterialData(data, newMaterialData); } void TerrainFeatureProcessor::OnTerrainMacroMaterialRegionChanged( AZ::EntityId entityId, [[maybe_unused]] const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) { - MacroMaterialData& materialData = FindOrCreateMacroMaterial(entityId); + MacroMaterialData& materialData = FindOrCreateByEntityId(entityId, m_macroMaterials); for (SectorData& sectorData : m_sectorData) { bool overlapsOld = sectorData.m_aabb.Overlaps(materialData.m_bounds); @@ -236,7 +301,7 @@ namespace Terrain void TerrainFeatureProcessor::OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) { - MacroMaterialData* materialData = FindMacroMaterial(entityId); + const MacroMaterialData* materialData = FindByEntityId(entityId, m_macroMaterials); if (materialData) { @@ -255,37 +320,498 @@ namespace Terrain } m_areaData.m_macroMaterialsUpdated = true; - RemoveMacroMaterial(entityId); + RemoveByEntityId(entityId, m_macroMaterials); + } + + void TerrainFeatureProcessor::OnTerrainSurfaceMaterialMappingCreated(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) + { + DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions); + + // Validate that the surface tag is new + for (DetailMaterialSurface& surface : materialRegion.m_materialsForSurfaces) + { + if (surface.m_surfaceTag == surfaceTag) + { + AZ_Error(TerrainFPName, false, "Already have a surface material mapping for this surface tag."); + return; + } + } + + uint16_t detailMaterialId = CreateOrUpdateDetailMaterial(material); + materialRegion.m_materialsForSurfaces.push_back({ surfaceTag, detailMaterialId }); + m_dirtyDetailRegion.AddAabb(materialRegion.m_region); + } + + void TerrainFeatureProcessor::OnTerrainSurfaceMaterialMappingDestroyed(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag) + { + DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions); + + for (DetailMaterialSurface& surface : materialRegion.m_materialsForSurfaces) + { + if (surface.m_surfaceTag == surfaceTag) + { + if (surface.m_surfaceTag != materialRegion.m_materialsForSurfaces.back().m_surfaceTag) + { + AZStd::swap(surface, materialRegion.m_materialsForSurfaces.back()); + } + materialRegion.m_materialsForSurfaces.pop_back(); + m_dirtyDetailRegion.AddAabb(materialRegion.m_region); + return; + } + } + AZ_Error(TerrainFPName, false, "Could not find surface tag to destroy for OnTerrainSurfaceMaterialMappingDestroyed()."); + } + + void TerrainFeatureProcessor::OnTerrainSurfaceMaterialMappingChanged(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) + { + DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions); + + bool found = false; + uint16_t materialId = CreateOrUpdateDetailMaterial(material); + for (DetailMaterialSurface& surface : materialRegion.m_materialsForSurfaces) + { + if (surface.m_surfaceTag == surfaceTag) + { + found = true; + surface.m_detailMaterialId = materialId; + break; + } + } + + if (!found) + { + materialRegion.m_materialsForSurfaces.push_back({ surfaceTag, materialId }); + } + m_dirtyDetailRegion.AddAabb(materialRegion.m_region); + } + + void TerrainFeatureProcessor::OnTerrainSurfaceMaterialMappingRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) + { + DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions); + materialRegion.m_region = newRegion; + m_dirtyDetailRegion.AddAabb(oldRegion); + m_dirtyDetailRegion.AddAabb(newRegion); + } + + uint16_t TerrainFeatureProcessor::CreateOrUpdateDetailMaterial(MaterialInstance material) + { + static constexpr uint16_t InvalidDetailMaterial = 0xFFFF; + uint16_t detailMaterialId = InvalidDetailMaterial; + + for (DetailMaterialData& detailMaterial : m_detailMaterials.GetDataVector()) + { + if (detailMaterial.m_assetId == material->GetAssetId()) + { + UpdateDetailMaterialData(detailMaterial, material); + detailMaterialId = m_detailMaterials.GetIndexForData(&detailMaterial); + break; + } + } + + if (detailMaterialId == InvalidDetailMaterial) + { + detailMaterialId = m_detailMaterials.GetFreeSlotIndex(); + UpdateDetailMaterialData(m_detailMaterials.GetData(detailMaterialId), material); + } + return detailMaterialId; + } + + void TerrainFeatureProcessor::UpdateDetailMaterialData(DetailMaterialData& materialData, MaterialInstance material) + { + if (materialData.m_materialChangeId != material->GetCurrentChangeId()) + { + materialData = DetailMaterialData(); + DetailTextureFlags& flags = materialData.m_properties.m_flags; + materialData.m_materialChangeId = material->GetCurrentChangeId(); + materialData.m_assetId = material->GetAssetId(); + + auto getIndex = [&](const char* const indexName) -> AZ::RPI::MaterialPropertyIndex + { + const AZ::RPI::MaterialPropertyIndex index = material->FindPropertyIndex(AZ::Name(indexName)); + AZ_Warning(TerrainFPName, index.IsValid(), "Failed to find shader input constant %s.", indexName); + return index; + }; + + auto applyProperty = [&](const char* const indexName, auto& ref) -> void + { + const auto index = getIndex(indexName); + if (index.IsValid()) + { + using TypeRefRemoved = AZStd::remove_cvref_t; + ref = material->GetPropertyValue(index).GetValue(); + } + }; + + auto applyFlag = [&](const char* const indexName, DetailTextureFlags flagToSet) -> void + { + const auto index = getIndex(indexName); + if (index.IsValid()) + { + bool flagValue = material->GetPropertyValue(index).GetValue(); + flags = DetailTextureFlags(flagValue ? flags | flagToSet : flags); + } + }; + + auto getEnumName = [&](const char* const indexName) -> const AZStd::string_view + { + const auto index = getIndex(indexName); + if (index.IsValid()) + { + uint32_t enumIndex = material->GetPropertyValue(index).GetValue(); + const AZ::Name& enumName = material->GetMaterialPropertiesLayout()->GetPropertyDescriptor(index)->GetEnumName(enumIndex); + return enumName.GetStringView(); + } + return ""; + }; + + using namespace DetailMaterialInputs; + applyProperty(BaseColorMap, materialData.m_colorImage); + applyFlag(BaseColorUseTexture, DetailTextureFlags::UseTextureBaseColor); + applyProperty(BaseColorFactor, materialData.m_properties.m_baseColorFactor); + + const AZStd::string_view& blendModeString = getEnumName(BaseColorBlendMode); + if (blendModeString == "Multiply") + { + flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeMultiply); + } + else if (blendModeString == "LinearLight") + { + flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeLinearLight); + } + else if (blendModeString == "Lerp") + { + flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeLerp); + } + else if (blendModeString == "Overlay") + { + flags = DetailTextureFlags(flags | DetailTextureFlags::BlendModeOverlay); + } + + applyProperty(MetallicMap, materialData.m_metalnessImage); + applyFlag(MetallicUseTexture, DetailTextureFlags::UseTextureMetallic); + applyProperty(MetallicFactor, materialData.m_properties.m_metalFactor); + + applyProperty(RoughnessMap, materialData.m_roughnessImage); + applyFlag(RoughnessUseTexture, DetailTextureFlags::UseTextureRoughness); + + if ((flags & DetailTextureFlags::UseTextureRoughness) > 0) + { + float lowerBound = 0.0; + float upperBound = 1.0; + applyProperty(RoughnessLowerBound, lowerBound); + applyProperty(RoughnessUpperBound, upperBound); + materialData.m_properties.m_roughnessBias = lowerBound; + materialData.m_properties.m_roughnessScale = upperBound - lowerBound; + } + else + { + materialData.m_properties.m_roughnessBias = 0.0; + applyProperty(RoughnessFactor, materialData.m_properties.m_roughnessScale); + } + + applyProperty(SpecularF0Map, materialData.m_specularF0Image); + applyFlag(SpecularF0UseTexture, DetailTextureFlags::UseTextureSpecularF0); + applyProperty(SpecularF0Factor, materialData.m_properties.m_specularF0Factor); + + applyProperty(NormalMap, materialData.m_normalImage); + applyFlag(NormalUseTexture, DetailTextureFlags::UseTextureNormal); + applyProperty(NormalFactor, materialData.m_properties.m_normalFactor); + applyFlag(NormalFlipX, DetailTextureFlags::FlipNormalX); + applyFlag(NormalFlipY, DetailTextureFlags::FlipNormalY); + + applyProperty(DiffuseOcclusionMap, materialData.m_occlusionImage); + applyFlag(DiffuseOcclusionUseTexture, DetailTextureFlags::UseTextureOcclusion); + applyProperty(DiffuseOcclusionFactor, materialData.m_properties.m_occlusionFactor); + + applyProperty(HeightMap, materialData.m_heightImage); + applyFlag(HeightUseTexture, DetailTextureFlags::UseTextureHeight); + applyProperty(HeightFactor, materialData.m_properties.m_heightFactor); + applyProperty(HeightOffset, materialData.m_properties.m_heightOffset); + applyProperty(HeightBlendFactor, materialData.m_properties.m_heightBlendFactor); + + } + } + + void TerrainFeatureProcessor::CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter) + { + if (!m_detailTextureImage) + { + // If the m_detailTextureImage doesn't exist, create it and populate the entire texture + + const AZ::Data::Instance imagePool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool(); + AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D( + AZ::RHI::ImageBindFlags::ShaderRead, DetailTextureSize, DetailTextureSize, AZ::RHI::Format::R8G8B8A8_UINT + ); + const AZ::Name TerrainDetailName = AZ::Name(TerrainDetailChars); + m_detailTextureImage = AZ::RPI::AttachmentImage::Create(*imagePool.get(), imageDescriptor, TerrainDetailName, nullptr, nullptr); + AZ_Error(TerrainFPName, m_detailTextureImage, "Failed to initialize the detail texture image."); + + UpdateDetailTexture(newBounds, newBounds, newCenter); + } + else + { + // If the new bounds of the detail texture are different than the old bounds, then the edges of the texture need to be updated. + + int32_t offsetX = m_detailTextureBounds.m_min.m_x - newBounds.m_min.m_x; + + // Horizontal edge update + if (newBounds.m_min.m_x != m_detailTextureBounds.m_min.m_x) + { + Aabb2i updateBounds; + if (newBounds.m_min.m_x < m_detailTextureBounds.m_min.m_x) + { + updateBounds.m_min.m_x = newBounds.m_min.m_x; + updateBounds.m_max.m_x = m_detailTextureBounds.m_min.m_x; + } + else + { + updateBounds.m_min.m_x = m_detailTextureBounds.m_max.m_x; + updateBounds.m_max.m_x = newBounds.m_max.m_x; + } + updateBounds.m_min.m_y = newBounds.m_min.m_y; + updateBounds.m_max.m_y = newBounds.m_max.m_y; + UpdateDetailTexture(updateBounds, newBounds, newCenter); + } + + // Vertical edge update + if (newBounds.m_min.m_y != m_detailTextureBounds.m_min.m_y) + { + Aabb2i updateBounds; + // Don't update areas that have already been updated in the horizontal update. + updateBounds.m_min.m_x = newBounds.m_min.m_x + AZ::GetMax(0, offsetX); + updateBounds.m_max.m_x = newBounds.m_max.m_x + AZ::GetMin(0, offsetX); + if (newBounds.m_min.m_y < m_detailTextureBounds.m_min.m_y) + { + updateBounds.m_min.m_y = newBounds.m_min.m_y; + updateBounds.m_max.m_y = m_detailTextureBounds.m_min.m_y; + } + else + { + updateBounds.m_min.m_y = m_detailTextureBounds.m_max.m_y; + updateBounds.m_max.m_y = newBounds.m_max.m_y; + } + UpdateDetailTexture(updateBounds, newBounds, newCenter); + } + + if (m_dirtyDetailRegion.IsValid()) + { + // If any regions are marked as dirty, then they should be updated. + + AZ::Vector3 currentMin = AZ::Vector3(newBounds.m_min.m_x * DetailTextureScale, newBounds.m_min.m_y * DetailTextureScale, -0.5f); + AZ::Vector3 currentMax = AZ::Vector3(newBounds.m_max.m_x * DetailTextureScale, newBounds.m_max.m_y * DetailTextureScale, 0.5f); + AZ::Aabb detailTextureCoverage = AZ::Aabb::CreateFromMinMax(currentMin, currentMax); + AZ::Vector3 previousMin = AZ::Vector3(m_detailTextureBounds.m_min.m_x * DetailTextureScale, m_detailTextureBounds.m_min.m_y * DetailTextureScale, -0.5f); + AZ::Vector3 previousMax = AZ::Vector3(m_detailTextureBounds.m_max.m_x * DetailTextureScale, m_detailTextureBounds.m_max.m_y * DetailTextureScale, 0.5f); + AZ::Aabb previousCoverage = AZ::Aabb::CreateFromMinMax(previousMin, previousMax); + + // Area of texture not already updated by camera movement above. + AZ::Aabb clampedCoverage = previousCoverage.GetClamped(detailTextureCoverage); + + // Clamp the dirty region to the area of the detail texture that is visible and not already updated. + clampedCoverage.Clamp(m_dirtyDetailRegion); + + if (clampedCoverage.IsValid()) + { + Aabb2i updateBounds; + updateBounds.m_min.m_x = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMin().GetX() / DetailTextureScale)); + updateBounds.m_min.m_y = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMin().GetY() / DetailTextureScale)); + updateBounds.m_max.m_x = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMax().GetX() / DetailTextureScale)); + updateBounds.m_max.m_y = aznumeric_cast(AZStd::roundf(clampedCoverage.GetMax().GetY() / DetailTextureScale)); + if (updateBounds.m_min.m_x < updateBounds.m_max.m_x && updateBounds.m_min.m_y < updateBounds.m_max.m_y) + { + UpdateDetailTexture(updateBounds, newBounds, newCenter); + } + } + } + } + + } + + uint8_t TerrainFeatureProcessor::CalculateUpdateRegions(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel, + AZStd::array& textureSpaceAreas, AZStd::array& scaledWorldSpaceAreas) + { + Vector2i centerOffset = { centerPixel.m_x - DetailTextureSizeHalf, centerPixel.m_y - DetailTextureSizeHalf }; + + int32_t quadrantXOffset = centerPixel.m_x < DetailTextureSizeHalf ? DetailTextureSize : -DetailTextureSize; + int32_t quadrantYOffset = centerPixel.m_y < DetailTextureSizeHalf ? DetailTextureSize : -DetailTextureSize; + + uint8_t numQuadrants = 0; + + // For each of the 4 quadrants: + auto calculateQuadrant = [&](Vector2i quadrantOffset) + { + Aabb2i offsetUpdateArea = updateArea + centerOffset + quadrantOffset; + Aabb2i updateSectionBounds = textureBounds.GetClamped(offsetUpdateArea); + if (updateSectionBounds.IsValid()) + { + textureSpaceAreas[numQuadrants] = updateSectionBounds - textureBounds.m_min; + scaledWorldSpaceAreas[numQuadrants] = updateSectionBounds - centerOffset - quadrantOffset; + ++numQuadrants; + } + }; + + calculateQuadrant({ 0, 0 }); + calculateQuadrant({ quadrantXOffset, 0 }); + calculateQuadrant({ 0, quadrantYOffset }); + calculateQuadrant({ quadrantXOffset, quadrantYOffset }); + + return numQuadrants; + } + + void TerrainFeatureProcessor::UpdateDetailTexture(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel) + { + if (!m_detailTextureImage) + { + return; + } + + struct DetailMaterialPixel + { + uint8_t m_material1{ 255 }; + uint8_t m_material2{ 255 }; + uint8_t m_blend{ 0 }; // 0 = full weight on material1, 255 = full weight on material2 + uint8_t m_padding{ 0 }; + }; + + // Because the center of the detail texture may be offset, each update area may actually need to be split into + // up to 4 separate update areas in each sector of the quadrant. + AZStd::array textureSpaceAreas; + AZStd::array scaledWorldSpaceAreas; + uint8_t updateAreaCount = CalculateUpdateRegions(updateArea, textureBounds, centerPixel, textureSpaceAreas, scaledWorldSpaceAreas); + + // Pull the data for each area updated and use it to construct an update for the detail material id texture. + for (uint8_t i = 0; i < updateAreaCount; ++i) + { + const Aabb2i& quadrantTextureArea = textureSpaceAreas[i]; + const Aabb2i& quadrantWorldArea = scaledWorldSpaceAreas[i]; + + AZStd::vector pixels; + pixels.resize((quadrantWorldArea.m_max.m_x - quadrantWorldArea.m_min.m_x) * (quadrantWorldArea.m_max.m_y - quadrantWorldArea.m_min.m_y)); + uint32_t index = 0; + + for (int yPos = quadrantWorldArea.m_min.m_y; yPos < quadrantWorldArea.m_max.m_y; ++yPos) + { + for (int xPos = quadrantWorldArea.m_min.m_x; xPos < quadrantWorldArea.m_max.m_x; ++xPos) + { + AZ::Vector2 position = AZ::Vector2(xPos * DetailTextureScale, yPos * DetailTextureScale); + AzFramework::SurfaceData::SurfaceTagWeightList surfaceWeights; + AzFramework::Terrain::TerrainDataRequestBus::Broadcast(&AzFramework::Terrain::TerrainDataRequests::GetSurfaceWeightsFromVector2, position, surfaceWeights, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, nullptr); + + // Store the top two surface weights in the texture with m_blend storing the relative weight. + bool isFirstMaterial = true; + float firstWeight = 0.0f; + for (const auto& surfaceTagWeight : surfaceWeights) + { + if (surfaceTagWeight.m_weight > 0.0f) + { + AZ::Crc32 surfaceType = surfaceTagWeight.m_surfaceType; + uint16_t materialId = GetDetailMaterialForSurfaceTypeAndPosition(surfaceType, position); + if (materialId != m_detailMaterials.NoFreeSlot && materialId < 255) + { + if (isFirstMaterial) + { + pixels.at(index).m_material1 = aznumeric_cast(materialId); + firstWeight = surfaceTagWeight.m_weight; + // m_blend only needs to be calculated is material 2 is found, otherwise the initial value of 0 is correct. + isFirstMaterial = false; + } + else + { + pixels.at(index).m_material2 = aznumeric_cast(materialId); + float totalWeight = firstWeight + surfaceTagWeight.m_weight; + float blendWeight = 1.0f - (firstWeight / totalWeight); + pixels.at(index).m_blend = aznumeric_cast(AZStd::round(blendWeight * 255.0f)); + break; + } + } + } + else + { + break; // since the list is ordered, no other materials are in the list with positive weights. + } + } + ++index; + } + } + + const int32_t left = quadrantTextureArea.m_min.m_x; + const int32_t top = quadrantTextureArea.m_min.m_y; + const int32_t width = quadrantTextureArea.m_max.m_x - quadrantTextureArea.m_min.m_x; + const int32_t height = quadrantTextureArea.m_max.m_y - quadrantTextureArea.m_min.m_y; + + AZ::RHI::ImageUpdateRequest imageUpdateRequest; + imageUpdateRequest.m_imageSubresourcePixelOffset.m_left = aznumeric_cast(left); + imageUpdateRequest.m_imageSubresourcePixelOffset.m_top = aznumeric_cast(top); + imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerRow = width * sizeof(DetailMaterialPixel); + imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerImage = width * height * sizeof(DetailMaterialPixel); + imageUpdateRequest.m_sourceSubresourceLayout.m_rowCount = height; + imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_width = width; + imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_height = height; + imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_depth = 1; + imageUpdateRequest.m_sourceData = pixels.data(); + imageUpdateRequest.m_image = m_detailTextureImage->GetRHIImage(); + + m_detailTextureImage->UpdateImageContents(imageUpdateRequest); + } + } + + uint16_t TerrainFeatureProcessor::GetDetailMaterialForSurfaceTypeAndPosition(AZ::Crc32 surfaceType, const AZ::Vector2& position) + { + for (const auto& materialRegion : m_detailMaterialRegions.GetDataVector()) + { + if (materialRegion.m_region.Contains(AZ::Vector3(position.GetX(), position.GetY(), 0.0f))) + { + for (const auto& materialSurface : materialRegion.m_materialsForSurfaces) + { + if (materialSurface.m_surfaceTag == surfaceType) + { + return materialSurface.m_detailMaterialId; + } + } + } + } + return m_detailMaterials.NoFreeSlot; } void TerrainFeatureProcessor::UpdateTerrainData() { - static const AZ::Name TerrainHeightmapName = AZ::Name(TerrainHeightmapChars); - uint32_t width = m_areaData.m_updateWidth; - uint32_t height = m_areaData.m_updateHeight; - const AZ::Aabb& worldBounds = m_areaData.m_terrainBounds; const float queryResolution = m_areaData.m_sampleSpacing; + const AZ::Aabb& worldBounds = m_areaData.m_terrainBounds; - const AZ::RHI::Size worldSize = AZ::RHI::Size(m_areaData.m_heightmapImageWidth, m_areaData.m_heightmapImageHeight, 1); + int32_t heightmapImageXStart = aznumeric_cast(AZStd::ceilf(worldBounds.GetMin().GetX() / queryResolution)); + int32_t heightmapImageXEnd = aznumeric_cast(AZStd::floorf(worldBounds.GetMax().GetX() / queryResolution)) + 1; + int32_t heightmapImageYStart = aznumeric_cast(AZStd::ceilf(worldBounds.GetMin().GetY() / queryResolution)); + int32_t heightmapImageYEnd = aznumeric_cast(AZStd::floorf(worldBounds.GetMax().GetY() / queryResolution)) + 1; + uint32_t heightmapImageWidth = heightmapImageXEnd - heightmapImageXStart; + uint32_t heightmapImageHeight = heightmapImageYEnd - heightmapImageYStart; - if (!m_areaData.m_heightmapImage || m_areaData.m_heightmapImage->GetDescriptor().m_size != worldSize) + const AZ::RHI::Size heightmapSize = AZ::RHI::Size(heightmapImageWidth, heightmapImageHeight, 1); + + if (!m_areaData.m_heightmapImage || m_areaData.m_heightmapImage->GetDescriptor().m_size != heightmapSize) { - // World size changed, so the whole world needs updating. - width = worldSize.m_width; - height = worldSize.m_height; - m_dirtyRegion = worldBounds; - const AZ::Data::Instance imagePool = AZ::RPI::ImageSystemInterface::Get()->GetSystemAttachmentPool(); AZ::RHI::ImageDescriptor imageDescriptor = AZ::RHI::ImageDescriptor::Create2D( - AZ::RHI::ImageBindFlags::ShaderRead, width, height, AZ::RHI::Format::R16_UNORM + AZ::RHI::ImageBindFlags::ShaderRead, heightmapSize.m_width, heightmapSize.m_height, AZ::RHI::Format::R16_UNORM ); + + const AZ::Name TerrainHeightmapName = AZ::Name(TerrainHeightmapChars); m_areaData.m_heightmapImage = AZ::RPI::AttachmentImage::Create(*imagePool.get(), imageDescriptor, TerrainHeightmapName, nullptr, nullptr); - AZ_Error(TerrainFPName, m_areaData.m_heightmapImage, "Failed to initialize the heightmap image!"); + AZ_Error(TerrainFPName, m_areaData.m_heightmapImage, "Failed to initialize the heightmap image."); + + // World size changed, so the whole height map needs updating. + m_dirtyRegion = worldBounds; } + + int32_t xStart = aznumeric_cast(AZStd::ceilf(m_dirtyRegion.GetMin().GetX() / queryResolution)); + int32_t xEnd = aznumeric_cast(AZStd::floorf(m_dirtyRegion.GetMax().GetX() / queryResolution)) + 1; + int32_t yStart = aznumeric_cast(AZStd::ceilf(m_dirtyRegion.GetMin().GetY() / queryResolution)); + int32_t yEnd = aznumeric_cast(AZStd::floorf(m_dirtyRegion.GetMax().GetY() / queryResolution)) + 1; + uint32_t updateWidth = xEnd - xStart; + uint32_t updateHeight = yEnd - yStart; AZStd::vector pixels; - pixels.reserve(width * height); + pixels.reserve(updateWidth * updateHeight); { // Block other threads from accessing the surface data bus while we are in GetHeightFromFloats (which may call into the SurfaceData bus). @@ -297,18 +823,17 @@ namespace Terrain auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false); typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex); - for (uint32_t y = 0; y < height; y++) + for (int32_t y = yStart; y < yEnd; y++) { - for (uint32_t x = 0; x < width; x++) + for (int32_t x = xStart; x < xEnd; x++) { bool terrainExists = true; float terrainHeight = 0.0f; + float xPos = x * queryResolution; + float yPos = y * queryResolution; AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( terrainHeight, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, - (x * queryResolution) + m_dirtyRegion.GetMin().GetX(), - (y * queryResolution) + m_dirtyRegion.GetMin().GetY(), - AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, - &terrainExists); + xPos, yPos, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); const float clampedHeight = AZ::GetClamp((terrainHeight - worldBounds.GetMin().GetZ()) / worldBounds.GetExtents().GetZ(), 0.0f, 1.0f); const float expandedHeight = AZStd::roundf(clampedHeight * AZStd::numeric_limits::max()); @@ -321,16 +846,18 @@ namespace Terrain if (m_areaData.m_heightmapImage) { - const float left = (m_dirtyRegion.GetMin().GetX() - worldBounds.GetMin().GetX()) / queryResolution; - const float top = (m_dirtyRegion.GetMin().GetY() - worldBounds.GetMin().GetY()) / queryResolution; + constexpr uint32_t BytesPerPixel = sizeof(uint16_t); + const float left = xStart - (worldBounds.GetMin().GetX() / queryResolution); + const float top = yStart - (worldBounds.GetMin().GetY() / queryResolution); + AZ::RHI::ImageUpdateRequest imageUpdateRequest; imageUpdateRequest.m_imageSubresourcePixelOffset.m_left = aznumeric_cast(left); imageUpdateRequest.m_imageSubresourcePixelOffset.m_top = aznumeric_cast(top); - imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerRow = width * sizeof(uint16_t); - imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerImage = width * height * sizeof(uint16_t); - imageUpdateRequest.m_sourceSubresourceLayout.m_rowCount = height; - imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_width = width; - imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_height = height; + imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerRow = updateWidth * BytesPerPixel; + imageUpdateRequest.m_sourceSubresourceLayout.m_bytesPerImage = updateWidth * updateHeight * BytesPerPixel; + imageUpdateRequest.m_sourceSubresourceLayout.m_rowCount = updateHeight; + imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_width = updateWidth; + imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_height = updateHeight; imageUpdateRequest.m_sourceSubresourceLayout.m_size.m_depth = 1; imageUpdateRequest.m_sourceData = pixels.data(); imageUpdateRequest.m_image = m_areaData.m_heightmapImage->GetRHIImage(); @@ -366,6 +893,19 @@ namespace Terrain m_heightmapPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::HeightmapImage)); AZ_Error(TerrainFPName, m_heightmapPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::HeightmapImage); + m_detailMaterialIdPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailMaterialIdImage)); + AZ_Error(TerrainFPName, m_detailMaterialIdPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailMaterialIdImage); + + m_detailCenterPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailCenter)); + AZ_Error(TerrainFPName, m_detailCenterPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailCenter); + + m_detailAabbPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailAabb)); + AZ_Error(TerrainFPName, m_detailAabbPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailAabb); + + m_detailHalfPixelUvPropertyIndex = m_materialInstance->GetMaterialPropertiesLayout()->FindPropertyIndex(AZ::Name(MaterialInputs::DetailHalfPixelUv)); + AZ_Error(TerrainFPName, m_detailHalfPixelUvPropertyIndex.IsValid(), "Failed to find material input constant %s.", MaterialInputs::DetailHalfPixelUv); + + // Find any macro materials that have already been created. TerrainMacroMaterialRequestBus::EnumerateHandlers( [&](TerrainMacroMaterialRequests* handler) { @@ -376,6 +916,30 @@ namespace Terrain } ); TerrainMacroMaterialNotificationBus::Handler::BusConnect(); + + // Find any detail material areas that have already been created. + TerrainAreaMaterialRequestBus::EnumerateHandlers( + [&](TerrainAreaMaterialRequests* handler) + { + const AZ::Aabb& bounds = handler->GetTerrainSurfaceMaterialRegion(); + const AZStd::vector materialMappings = handler->GetSurfaceMaterialMappings(); + AZ::EntityId entityId = *(Terrain::TerrainAreaMaterialRequestBus::GetCurrentBusId()); + + DetailMaterialListRegion& materialRegion = FindOrCreateByEntityId(entityId, m_detailMaterialRegions); + materialRegion.m_region = bounds; + + for (const auto& materialMapping : materialMappings) + { + if (materialMapping.m_materialInstance) + { + OnTerrainSurfaceMaterialMappingCreated(entityId, materialMapping.m_surfaceTag, materialMapping.m_materialInstance); + } + } + return true; + } + ); + TerrainAreaMaterialNotificationBus::Handler::BusConnect(); + } void TerrainFeatureProcessor::UpdateMacroMaterialData(MacroMaterialData& macroMaterialData, const MacroMaterialData& newMaterialData) @@ -474,14 +1038,73 @@ namespace Terrain } } } + else if (m_forceRebuildDrawPackets) + { + for (auto& sectorData : m_sectorData) + { + for (auto& drawPacket : sectorData.m_drawPackets) + { + drawPacket.Update(*GetParentScene(), true); + } + } + } + m_forceRebuildDrawPackets = false; if (m_areaData.m_heightmapUpdated) { UpdateTerrainData(); - - const AZ::Data::Instance heightmapImage = m_areaData.m_heightmapImage; + + const AZ::Data::Instance heightmapImage = m_areaData.m_heightmapImage; // cast StreamingImage to Image m_materialInstance->SetPropertyValue(m_heightmapPropertyIndex, heightmapImage); - m_materialInstance->Compile(); + } + + AZ::Vector3 cameraPosition = AZ::Vector3::CreateZero(); + for (auto& view : process.m_views) + { + if ((view->GetUsageFlags() & AZ::RPI::View::UsageFlags::UsageCamera) > 0) + { + cameraPosition = view->GetCameraTransform().GetTranslation(); + break; + } + } + + if (m_dirtyDetailRegion.IsValid() || !cameraPosition.IsClose(m_previousCameraPosition)) + { + int32_t newDetailTexturePosX = aznumeric_cast(AZStd::roundf(cameraPosition.GetX() / DetailTextureScale)); + int32_t newDetailTexturePosY = aznumeric_cast(AZStd::roundf(cameraPosition.GetY() / DetailTextureScale)); + + Aabb2i newBounds; + newBounds.m_min.m_x = newDetailTexturePosX - DetailTextureSizeHalf; + newBounds.m_min.m_y = newDetailTexturePosY - DetailTextureSizeHalf; + newBounds.m_max.m_x = newDetailTexturePosX + DetailTextureSizeHalf; + newBounds.m_max.m_y = newDetailTexturePosY + DetailTextureSizeHalf; + + // Use modulo to find the center point in texture space. Care must be taken so negative values are + // handled appropriately (ie, we want -1 % 1024 to equal 1023, not -1) + Vector2i newCenter; + newCenter.m_x = (DetailTextureSize + (newDetailTexturePosX % DetailTextureSize)) % DetailTextureSize; + newCenter.m_y = (DetailTextureSize + (newDetailTexturePosY % DetailTextureSize)) % DetailTextureSize; + + CheckUpdateDetailTexture(newBounds, newCenter); + + m_detailTextureBounds = newBounds; + m_dirtyDetailRegion = AZ::Aabb::CreateNull(); + + m_previousCameraPosition = cameraPosition; + const AZ::Data::Instance detailTextureImage = m_detailTextureImage; // cast StreamingImage to Image + m_materialInstance->SetPropertyValue(m_detailMaterialIdPropertyIndex, detailTextureImage); + + AZ::Vector4 detailAabb = AZ::Vector4( + m_detailTextureBounds.m_min.m_x * DetailTextureScale, + m_detailTextureBounds.m_min.m_y * DetailTextureScale, + m_detailTextureBounds.m_max.m_x * DetailTextureScale, + m_detailTextureBounds.m_max.m_y * DetailTextureScale + ); + m_materialInstance->SetPropertyValue(m_detailAabbPropertyIndex, detailAabb); + m_materialInstance->SetPropertyValue(m_detailHalfPixelUvPropertyIndex, 0.5f / DetailTextureSize); + + AZ::Vector2 detailUvOffset = AZ::Vector2(float(newCenter.m_x) / DetailTextureSize, float(newCenter.m_y) / DetailTextureSize); + m_materialInstance->SetPropertyValue(m_detailCenterPropertyIndex, detailUvOffset); } if (m_areaData.m_heightmapUpdated || m_areaData.m_macroMaterialsUpdated) @@ -492,6 +1115,12 @@ namespace Terrain m_areaData.m_heightmapUpdated = false; m_areaData.m_macroMaterialsUpdated = false; + AZStd::array uvStep = + { + 1.0f / aznumeric_cast(m_areaData.m_terrainBounds.GetXExtent() / m_areaData.m_sampleSpacing), + 1.0f / aznumeric_cast(m_areaData.m_terrainBounds.GetYExtent() / m_areaData.m_sampleSpacing), + }; + for (SectorData& sectorData : m_sectorData) { ShaderTerrainData terrainDataForSrg; @@ -509,11 +1138,7 @@ namespace Terrain ((yPatch + GridMeters) - terrainBounds.GetMin().GetY()) / terrainBounds.GetYExtent() }; - terrainDataForSrg.m_uvStep = - { - 1.0f / m_areaData.m_heightmapImageWidth, - 1.0f / m_areaData.m_heightmapImageHeight, - }; + terrainDataForSrg.m_uvStep = uvStep; AZ::Transform transform = m_areaData.m_transform; transform.SetTranslation(xPatch, yPatch, m_areaData.m_transform.GetTranslation().GetZ()); @@ -603,6 +1228,11 @@ namespace Terrain } } } + + if (m_materialInstance) + { + m_materialInstance->Compile(); + } } void TerrainFeatureProcessor::InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata) @@ -746,9 +1376,10 @@ namespace Terrain // larger but this will limit how much is rendered. } - MacroMaterialData* TerrainFeatureProcessor::FindMacroMaterial(AZ::EntityId entityId) + template + T* TerrainFeatureProcessor::FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) { - for (MacroMaterialData& data : m_macroMaterials.GetDataVector()) + for (T& data : container.GetDataVector()) { if (data.m_entityId == entityId) { @@ -757,34 +1388,36 @@ namespace Terrain } return nullptr; } - - MacroMaterialData& TerrainFeatureProcessor::FindOrCreateMacroMaterial(AZ::EntityId entityId) + + template + T& TerrainFeatureProcessor::FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) { - MacroMaterialData* dataPtr = FindMacroMaterial(entityId); + T* dataPtr = FindByEntityId(entityId, container); if (dataPtr != nullptr) { return *dataPtr; } - const uint16_t slotId = m_macroMaterials.GetFreeSlotIndex(); - AZ_Assert(slotId != m_macroMaterials.NoFreeSlot, "Ran out of indices for macro materials"); + const uint16_t slotId = container.GetFreeSlotIndex(); + AZ_Assert(slotId != AZ::Render::IndexedDataVector::NoFreeSlot, "Ran out of indices"); - MacroMaterialData& data = m_macroMaterials.GetData(slotId); + T& data = container.GetData(slotId); data.m_entityId = entityId; return data; } - - void TerrainFeatureProcessor::RemoveMacroMaterial(AZ::EntityId entityId) + + template + void TerrainFeatureProcessor::RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container) { - for (MacroMaterialData& data : m_macroMaterials.GetDataVector()) + for (T& data : container.GetDataVector()) { if (data.m_entityId == entityId) { - m_macroMaterials.RemoveData(&data); + container.RemoveData(&data); return; } } - AZ_Assert(false, "Entity Id not found in m_macroMaterials.") + AZ_Assert(false, "Entity Id not found in container.") } template @@ -798,4 +1431,60 @@ namespace Terrain } } } + + auto TerrainFeatureProcessor::Vector2i::operator+(const Vector2i& rhs) const -> Vector2i + { + Vector2i offsetPoint = *this; + offsetPoint += rhs; + return offsetPoint; + } + + auto TerrainFeatureProcessor::Vector2i::operator+=(const Vector2i& rhs) -> Vector2i& + { + m_x += rhs.m_x; + m_y += rhs.m_y; + return *this; + } + + auto TerrainFeatureProcessor::Vector2i::operator-(const Vector2i& rhs) const -> Vector2i + { + return *this + -rhs; + } + + auto TerrainFeatureProcessor::Vector2i::operator-=(const Vector2i& rhs) -> Vector2i& + { + return *this += -rhs; + } + + auto TerrainFeatureProcessor::Vector2i::operator-() const -> Vector2i + { + return {-m_x, -m_y}; + } + + auto TerrainFeatureProcessor::Aabb2i::operator+(const Vector2i& rhs) const -> Aabb2i + { + return { m_min + rhs, m_max + rhs }; + } + + auto TerrainFeatureProcessor::Aabb2i::operator-(const Vector2i& rhs) const -> Aabb2i + { + return *this + -rhs; + } + + auto TerrainFeatureProcessor::Aabb2i::GetClamped(Aabb2i rhs) const -> Aabb2i + { + Aabb2i ret; + ret.m_min.m_x = AZ::GetMax(m_min.m_x, rhs.m_min.m_x); + ret.m_min.m_y = AZ::GetMax(m_min.m_y, rhs.m_min.m_y); + ret.m_max.m_x = AZ::GetMin(m_max.m_x, rhs.m_max.m_x); + ret.m_max.m_y = AZ::GetMin(m_max.m_y, rhs.m_max.m_y); + return ret; + } + + bool TerrainFeatureProcessor::Aabb2i::IsValid() const + { + // Intentionally strict, equal min/max not valid. + return m_min.m_x < m_max.m_x && m_min.m_y < m_max.m_y; + } + } diff --git a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h index f82fd8ecb0..6c357db996 100644 --- a/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h +++ b/Gems/Terrain/Code/Source/TerrainRenderer/TerrainFeatureProcessor.h @@ -12,11 +12,13 @@ #include #include +#include #include #include #include #include +#include #include namespace AZ::RPI @@ -37,6 +39,7 @@ namespace Terrain , private AZ::RPI::MaterialReloadNotificationBus::Handler , private AzFramework::Terrain::TerrainDataNotificationBus::Handler , private TerrainMacroMaterialNotificationBus::Handler + , private TerrainAreaMaterialNotificationBus::Handler { public: AZ_RTTI(TerrainFeatureProcessor, "{D7DAC1F9-4A9F-4D3C-80AE-99579BF8AB1C}", AZ::RPI::FeatureProcessor); @@ -112,6 +115,109 @@ namespace Terrain AZStd::fixed_vector m_macroMaterials; }; + enum DetailTextureFlags : uint32_t + { + UseTextureBaseColor = 0b0000'0000'0000'0000'0000'0000'0000'0001, + UseTextureNormal = 0b0000'0000'0000'0000'0000'0000'0000'0010, + UseTextureMetallic = 0b0000'0000'0000'0000'0000'0000'0000'0100, + UseTextureRoughness = 0b0000'0000'0000'0000'0000'0000'0000'1000, + UseTextureOcclusion = 0b0000'0000'0000'0000'0000'0000'0001'0000, + UseTextureHeight = 0b0000'0000'0000'0000'0000'0000'0010'0000, + UseTextureSpecularF0 = 0b0000'0000'0000'0000'0000'0000'0100'0000, + + FlipNormalX = 0b0000'0000'0000'0000'0000'0000'1000'0000, + FlipNormalY = 0b0000'0000'0000'0000'0000'0001'0000'0000, + + BlendModeMask = 0b0000'0000'0000'0000'0000'0110'0000'0000, + BlendModeLerp = 0b0000'0000'0000'0000'0000'0000'0000'0000, + BlendModeLinearLight = 0b0000'0000'0000'0000'0000'0010'0000'0000, + BlendModeMultiply = 0b0000'0000'0000'0000'0000'0100'0000'0000, + BlendModeOverlay = 0b0000'0000'0000'0000'0000'0110'0000'0000, + }; + + struct DetailMaterialShaderProperties + { + // Uv + AZStd::array m_uvTransform + { + 1.0, 0.0, 0.0, 0.0, + 0.0, 1.0, 0.0, 0.0, + 0.0, 0.0, 1.0, 0.0, + }; + + // Factor / Scale / Bias for input textures + float m_baseColorFactor{ 1.0f }; + float m_normalFactor{ 1.0f }; + float m_metalFactor{ 1.0f }; + float m_roughnessScale{ 1.0f }; + + float m_roughnessBias{ 0.0f }; + float m_specularF0Factor{ 1.0f }; + float m_occlusionFactor{ 1.0f }; + float m_heightFactor{ 1.0f }; + + float m_heightOffset{ 0.0f }; + float m_heightBlendFactor{ 0.5f }; + + // Flags + DetailTextureFlags m_flags{ 0 }; + + float m_padding; // 16 byte aligned + }; + + struct DetailMaterialData + { + AZ::Data::AssetId m_assetId; + AZ::RPI::Material::ChangeId m_materialChangeId{AZ::RPI::Material::DEFAULT_CHANGE_ID}; + + AZ::Data::Instance m_colorImage; + AZ::Data::Instance m_normalImage; + AZ::Data::Instance m_roughnessImage; + AZ::Data::Instance m_metalnessImage; + AZ::Data::Instance m_specularF0Image; + AZ::Data::Instance m_occlusionImage; + AZ::Data::Instance m_heightImage; + + DetailMaterialShaderProperties m_properties; // maps directly to shader + }; + + struct DetailMaterialSurface + { + AZ::Crc32 m_surfaceTag; + uint16_t m_detailMaterialId; + }; + + struct DetailMaterialListRegion + { + AZ::EntityId m_entityId; + AZ::Aabb m_region{AZ::Aabb::CreateNull()}; + AZStd::vector m_materialsForSurfaces; + }; + + struct Vector2i + { + int32_t m_x{ 0 }; + int32_t m_y{ 0 }; + + Vector2i operator+(const Vector2i& rhs) const; + Vector2i& operator+=(const Vector2i& rhs); + Vector2i operator-(const Vector2i& rhs) const; + Vector2i& operator-=(const Vector2i& rhs); + Vector2i operator-() const; + }; + + struct Aabb2i + { + Vector2i m_min; + Vector2i m_max; + + Aabb2i operator+(const Vector2i& offset) const; + Aabb2i operator-(const Vector2i& offset) const; + + Aabb2i GetClamped(Aabb2i rhs) const; + bool IsValid() const; + }; + // AZ::RPI::MaterialReloadNotificationBus::Handler overrides... void OnMaterialReinitialized(const MaterialInstance& material) override; @@ -124,6 +230,12 @@ namespace Terrain void OnTerrainMacroMaterialChanged(AZ::EntityId entityId, const MacroMaterialData& material) override; void OnTerrainMacroMaterialRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override; void OnTerrainMacroMaterialDestroyed(AZ::EntityId entityId) override; + + // TerrainAreaMaterialNotificationBus overrides... + void OnTerrainSurfaceMaterialMappingCreated(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) override; + void OnTerrainSurfaceMaterialMappingDestroyed(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag) override; + void OnTerrainSurfaceMaterialMappingChanged(AZ::EntityId entityId, SurfaceData::SurfaceTag surfaceTag, MaterialInstance material) override; + void OnTerrainSurfaceMaterialMappingRegionChanged(AZ::EntityId entityId, const AZ::Aabb& oldRegion, const AZ::Aabb& newRegion) override; void Initialize(); void InitializeTerrainPatch(uint16_t gridSize, float gridSpacing, PatchData& patchdata); @@ -132,12 +244,26 @@ namespace Terrain void UpdateTerrainData(); void PrepareMaterialData(); void UpdateMacroMaterialData(MacroMaterialData& macroMaterialData, const MacroMaterialData& newMaterialData); + + void TerrainHeightOrSettingsUpdated(const AZ::Aabb& dirtyRegion); + void TerrainSurfaceDataUpdated(const AZ::Aabb& dirtyRegion); + + uint16_t CreateOrUpdateDetailMaterial(MaterialInstance material); + void UpdateDetailMaterialData(DetailMaterialData& materialData, MaterialInstance material); + void CheckUpdateDetailTexture(const Aabb2i& newBounds, const Vector2i& newCenter); + void UpdateDetailTexture(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel); + uint16_t GetDetailMaterialForSurfaceTypeAndPosition(AZ::Crc32 surfaceType, const AZ::Vector2& position); + uint8_t CalculateUpdateRegions(const Aabb2i& updateArea, const Aabb2i& textureBounds, const Vector2i& centerPixel, + AZStd::array& textureSpaceAreas, AZStd::array& scaledWorldSpaceAreas); void ProcessSurfaces(const FeatureProcessor::RenderPacket& process); - - MacroMaterialData* FindMacroMaterial(AZ::EntityId entityId); - MacroMaterialData& FindOrCreateMacroMaterial(AZ::EntityId entityId); - void RemoveMacroMaterial(AZ::EntityId entityId); + + template + T* FindByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); + template + T& FindOrCreateByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); + template + void RemoveByEntityId(AZ::EntityId entityId, AZ::Render::IndexedDataVector& container); template void ForOverlappingSectors(const AZ::Aabb& bounds, Callback callback); @@ -147,8 +273,11 @@ namespace Terrain // System-level parameters static constexpr float GridSpacing{ 1.0f }; - static constexpr uint32_t GridSize{ 64 }; // number of terrain quads (vertices are m_gridSize + 1) + static constexpr int32_t GridSize{ 64 }; // number of terrain quads (vertices are m_gridSize + 1) static constexpr float GridMeters{ GridSpacing * GridSize }; + static constexpr int32_t DetailTextureSize{ 1024 }; + static constexpr int32_t DetailTextureSizeHalf{ DetailTextureSize / 2 }; + static constexpr float DetailTextureScale{ 0.5f }; AZStd::unique_ptr m_materialAssetLoader; MaterialInstance m_materialInstance; @@ -160,8 +289,13 @@ namespace Terrain AZ::RHI::ShaderInputImageIndex m_macroColorMapIndex; AZ::RHI::ShaderInputImageIndex m_macroNormalMapIndex; AZ::RPI::MaterialPropertyIndex m_heightmapPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailMaterialIdPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailCenterPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailAabbPropertyIndex; + AZ::RPI::MaterialPropertyIndex m_detailHalfPixelUvPropertyIndex; AZ::Data::Instance m_patchModel; + AZ::Vector3 m_previousCameraPosition = AZ::Vector3(AZStd::numeric_limits::max(), 0.0, 0.0); // Per-area data struct TerrainAreaData @@ -169,21 +303,26 @@ namespace Terrain AZ::Transform m_transform{ AZ::Transform::CreateIdentity() }; AZ::Aabb m_terrainBounds{ AZ::Aabb::CreateNull() }; AZ::Data::Instance m_heightmapImage; - uint32_t m_heightmapImageWidth{ 0 }; - uint32_t m_heightmapImageHeight{ 0 }; - uint32_t m_updateWidth{ 0 }; - uint32_t m_updateHeight{ 0 }; float m_sampleSpacing{ 0.0f }; bool m_heightmapUpdated{ true }; bool m_macroMaterialsUpdated{ true }; bool m_rebuildSectors{ true }; }; - + TerrainAreaData m_areaData; AZ::Aabb m_dirtyRegion{ AZ::Aabb::CreateNull() }; + AZ::Aabb m_dirtyDetailRegion{ AZ::Aabb::CreateNull() }; + + Aabb2i m_detailTextureBounds; + Vector2i m_detailTextureCenter; + AZ::Data::Instance m_detailTextureImage; + AZ::RPI::ShaderSystemInterface::GlobalShaderOptionUpdatedEvent::Handler m_handleGlobalShaderOptionUpdate; + bool m_forceRebuildDrawPackets = false; AZStd::vector m_sectorData; AZ::Render::IndexedDataVector m_macroMaterials; + AZ::Render::IndexedDataVector m_detailMaterials; + AZ::Render::IndexedDataVector m_detailMaterialRegions; }; } diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp index fa1d483a5d..8d39340b06 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.cpp @@ -76,6 +76,7 @@ void TerrainSystem::Activate() m_dirtyRegion = AZ::Aabb::CreateNull(); m_terrainHeightDirty = true; m_terrainSettingsDirty = true; + m_terrainSurfacesDirty = true; m_requestedSettings.m_systemActive = true; { @@ -115,6 +116,7 @@ void TerrainSystem::Deactivate() m_dirtyRegion = AZ::Aabb::CreateNull(); m_terrainHeightDirty = true; m_terrainSettingsDirty = true; + m_terrainSurfacesDirty = true; m_requestedSettings.m_systemActive = false; AzFramework::Terrain::TerrainDataNotificationBus::Broadcast( @@ -549,6 +551,7 @@ void TerrainSystem::RegisterArea(AZ::EntityId areaId) m_registeredAreas[areaId] = aabb; m_dirtyRegion.AddAabb(aabb); m_terrainHeightDirty = true; + m_terrainSurfacesDirty = true; } void TerrainSystem::UnregisterArea(AZ::EntityId areaId) @@ -567,14 +570,17 @@ void TerrainSystem::UnregisterArea(AZ::EntityId areaId) { m_dirtyRegion.AddAabb(aabb); m_terrainHeightDirty = true; + m_terrainSurfacesDirty = true; return true; } return false; }); } -void TerrainSystem::RefreshArea(AZ::EntityId areaId) +void TerrainSystem::RefreshArea(AZ::EntityId areaId, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask) { + using Terrain = AzFramework::Terrain::TerrainDataNotifications; + AZStd::unique_lock lock(m_areaMutex); auto areaAabb = m_registeredAreas.find(areaId); @@ -588,11 +594,18 @@ void TerrainSystem::RefreshArea(AZ::EntityId areaId) expandedAabb.AddAabb(newAabb); m_dirtyRegion.AddAabb(expandedAabb); - m_terrainHeightDirty = true; + + // Keep track of which types of data have changed so that we can send out the appropriate notifications later. + + m_terrainHeightDirty = m_terrainHeightDirty || ((changeMask & Terrain::HeightData) == Terrain::HeightData); + + m_terrainSurfacesDirty = m_terrainSurfacesDirty || ((changeMask & Terrain::SurfaceData) == Terrain::SurfaceData); } void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) { + using Terrain = AzFramework::Terrain::TerrainDataNotifications; + bool terrainSettingsChanged = false; if (m_terrainSettingsDirty) @@ -607,6 +620,7 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) m_dirtyRegion = m_currentSettings.m_worldBounds; m_dirtyRegion.AddAabb(m_requestedSettings.m_worldBounds); m_terrainHeightDirty = true; + m_terrainSurfacesDirty = true; m_currentSettings.m_worldBounds = m_requestedSettings.m_worldBounds; } @@ -614,12 +628,13 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) { m_dirtyRegion = AZ::Aabb::CreateNull(); m_terrainHeightDirty = true; + m_terrainSurfacesDirty = true; } m_currentSettings = m_requestedSettings; } - if (terrainSettingsChanged || m_terrainHeightDirty) + if (terrainSettingsChanged || m_terrainHeightDirty || m_terrainSurfacesDirty) { // Block other threads from accessing the surface data bus while we are in GetValue (which may call into the SurfaceData bus). // We lock our surface data mutex *before* checking / setting "isRequestInProgress" so that we prevent race conditions @@ -629,24 +644,27 @@ void TerrainSystem::OnTick(float /*deltaTime*/, AZ::ScriptTimePoint /*time*/) auto& surfaceDataContext = SurfaceData::SurfaceDataSystemRequestBus::GetOrCreateContext(false); typename SurfaceData::SurfaceDataSystemRequestBus::Context::DispatchLockGuard scopeLock(surfaceDataContext.m_contextMutex); - AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask = - AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::None; + Terrain::TerrainDataChangedMask changeMask = Terrain::TerrainDataChangedMask::None; if (terrainSettingsChanged) { - changeMask = static_cast( - changeMask | AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::Settings); + changeMask = static_cast(changeMask | Terrain::TerrainDataChangedMask::Settings); } if (m_terrainHeightDirty) { - changeMask = static_cast( - changeMask | AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask::HeightData); + changeMask = static_cast(changeMask | Terrain::TerrainDataChangedMask::HeightData); + } + + if (m_terrainSurfacesDirty) + { + changeMask = static_cast(changeMask | Terrain::TerrainDataChangedMask::SurfaceData); } // Make sure to set these *before* calling OnTerrainDataChanged, since it's possible that subsystems reacting to that call will // cause the data to become dirty again. AZ::Aabb dirtyRegion = m_dirtyRegion; m_terrainHeightDirty = false; + m_terrainSurfacesDirty = false; m_dirtyRegion = AZ::Aabb::CreateNull(); AzFramework::Terrain::TerrainDataNotificationBus::Broadcast( diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h index 956424f048..022cd218cc 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystem.h @@ -47,7 +47,8 @@ namespace Terrain void RegisterArea(AZ::EntityId areaId) override; void UnregisterArea(AZ::EntityId areaId) override; - void RefreshArea(AZ::EntityId areaId) override; + void RefreshArea( + AZ::EntityId areaId, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask) override; /////////////////////////////////////////// // TerrainDataRequestBus::Handler Impl @@ -164,6 +165,7 @@ namespace Terrain bool m_terrainSettingsDirty = true; bool m_terrainHeightDirty = false; + bool m_terrainSurfacesDirty = false; AZ::Aabb m_dirtyRegion; mutable AZStd::shared_mutex m_areaMutex; diff --git a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h index cda6d65a1e..013d82d94d 100644 --- a/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h +++ b/Gems/Terrain/Code/Source/TerrainSystem/TerrainSystemBus.h @@ -44,7 +44,7 @@ namespace Terrain // register an area to override terrain virtual void RegisterArea(AZ::EntityId areaId) = 0; virtual void UnregisterArea(AZ::EntityId areaId) = 0; - virtual void RefreshArea(AZ::EntityId areaId) = 0; + virtual void RefreshArea(AZ::EntityId areaId, AzFramework::Terrain::TerrainDataNotifications::TerrainDataChangedMask changeMask) = 0; }; using TerrainSystemServiceRequestBus = AZ::EBus; diff --git a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp index 0ca5e7d99a..3778ba860f 100644 --- a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp +++ b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp @@ -190,7 +190,7 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSyst CreateMockTerrainSystem(); // The TransformChanged call should refresh the area. - EXPECT_CALL(*m_terrainSystem, RefreshArea(_)).Times(1); + EXPECT_CALL(*m_terrainSystem, RefreshArea(_, _)).Times(1); AddLayerSpawnerAndShapeComponentToEntity(); @@ -211,7 +211,7 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerShapeChangedUpdatesTerrainSystem) CreateMockTerrainSystem(); // The ShapeChanged call should refresh the area. - EXPECT_CALL(*m_terrainSystem, RefreshArea(_)).Times(1); + EXPECT_CALL(*m_terrainSystem, RefreshArea(_, _)).Times(1); AddLayerSpawnerAndShapeComponentToEntity(); diff --git a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp index c314e4e968..ec500d6ada 100644 --- a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp +++ b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp @@ -93,7 +93,7 @@ TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientRefreshesTer // As the TerrainHeightGradientListComponent subscribes to the dependency monitor, RefreshArea will be called twice: // once due to OnCompositionChanged being picked up by the the dependency monitor and resending the notification, // and once when the HeightGradientListComponent gets the OnCompositionChanged directly through the DependencyNotificationBus. - EXPECT_CALL(terrainSystem, RefreshArea(_)).Times(2); + EXPECT_CALL(terrainSystem, RefreshArea(_, _)).Times(2); LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); diff --git a/Registry/CMakeLists.txt b/Registry/CMakeLists.txt index 773adac07f..df309ba657 100644 --- a/Registry/CMakeLists.txt +++ b/Registry/CMakeLists.txt @@ -12,6 +12,11 @@ endif() ly_install_directory(DIRECTORIES .) -ly_install_directory(DIRECTORIES ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$/Registry - DESTINATION ${runtime_output_directory} -) +foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + string(REPLACE "$" "${conf}" output ${runtime_output_directory}) + ly_install_directory(DIRECTORIES ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/${conf}/Registry + DESTINATION ${output} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + ) +endforeach() diff --git a/Templates/CMakeLists.txt b/Templates/CMakeLists.txt index 84a708989a..9735907a6a 100644 --- a/Templates/CMakeLists.txt +++ b/Templates/CMakeLists.txt @@ -9,8 +9,8 @@ ly_install_directory( DIRECTORIES AssetGem - CustomTool - PythonGem + CppToolGem + PythonToolGem DefaultGem DefaultProject MinimalProject diff --git a/Templates/CustomTool/Template/CMakeLists.txt b/Templates/CppToolGem/Template/CMakeLists.txt similarity index 100% rename from Templates/CustomTool/Template/CMakeLists.txt rename to Templates/CppToolGem/Template/CMakeLists.txt diff --git a/Templates/CustomTool/Template/Code/${NameLower}_editor_files.cmake b/Templates/CppToolGem/Template/Code/${NameLower}_editor_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/${NameLower}_editor_files.cmake rename to Templates/CppToolGem/Template/Code/${NameLower}_editor_files.cmake diff --git a/Templates/CustomTool/Template/Code/${NameLower}_editor_shared_files.cmake b/Templates/CppToolGem/Template/Code/${NameLower}_editor_shared_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/${NameLower}_editor_shared_files.cmake rename to Templates/CppToolGem/Template/Code/${NameLower}_editor_shared_files.cmake diff --git a/Templates/CustomTool/Template/Code/${NameLower}_editor_tests_files.cmake b/Templates/CppToolGem/Template/Code/${NameLower}_editor_tests_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/${NameLower}_editor_tests_files.cmake rename to Templates/CppToolGem/Template/Code/${NameLower}_editor_tests_files.cmake diff --git a/Templates/CustomTool/Template/Code/${NameLower}_files.cmake b/Templates/CppToolGem/Template/Code/${NameLower}_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/${NameLower}_files.cmake rename to Templates/CppToolGem/Template/Code/${NameLower}_files.cmake diff --git a/Templates/CustomTool/Template/Code/${NameLower}_shared_files.cmake b/Templates/CppToolGem/Template/Code/${NameLower}_shared_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/${NameLower}_shared_files.cmake rename to Templates/CppToolGem/Template/Code/${NameLower}_shared_files.cmake diff --git a/Templates/CustomTool/Template/Code/${NameLower}_tests_files.cmake b/Templates/CppToolGem/Template/Code/${NameLower}_tests_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/${NameLower}_tests_files.cmake rename to Templates/CppToolGem/Template/Code/${NameLower}_tests_files.cmake diff --git a/Templates/CustomTool/Template/Code/CMakeLists.txt b/Templates/CppToolGem/Template/Code/CMakeLists.txt similarity index 100% rename from Templates/CustomTool/Template/Code/CMakeLists.txt rename to Templates/CppToolGem/Template/Code/CMakeLists.txt diff --git a/Templates/CustomTool/Template/Code/Include/${Name}/${Name}Bus.h b/Templates/CppToolGem/Template/Code/Include/${Name}/${Name}Bus.h similarity index 100% rename from Templates/CustomTool/Template/Code/Include/${Name}/${Name}Bus.h rename to Templates/CppToolGem/Template/Code/Include/${Name}/${Name}Bus.h diff --git a/Templates/CustomTool/Template/Code/Platform/Android/${NameLower}_android_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Android/${NameLower}_android_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Android/${NameLower}_android_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Android/${NameLower}_android_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Android/${NameLower}_shared_android_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Android/${NameLower}_shared_android_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Android/${NameLower}_shared_android_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Android/${NameLower}_shared_android_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Android/PAL_android.cmake b/Templates/CppToolGem/Template/Code/Platform/Android/PAL_android.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Android/PAL_android.cmake rename to Templates/CppToolGem/Template/Code/Platform/Android/PAL_android.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Linux/PAL_linux.cmake b/Templates/CppToolGem/Template/Code/Platform/Linux/PAL_linux.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Linux/PAL_linux.cmake rename to Templates/CppToolGem/Template/Code/Platform/Linux/PAL_linux.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Mac/PAL_mac.cmake b/Templates/CppToolGem/Template/Code/Platform/Mac/PAL_mac.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Mac/PAL_mac.cmake rename to Templates/CppToolGem/Template/Code/Platform/Mac/PAL_mac.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake b/Templates/CppToolGem/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/Windows/PAL_windows.cmake b/Templates/CppToolGem/Template/Code/Platform/Windows/PAL_windows.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/Windows/PAL_windows.cmake rename to Templates/CppToolGem/Template/Code/Platform/Windows/PAL_windows.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/iOS/${NameLower}_ios_files.cmake b/Templates/CppToolGem/Template/Code/Platform/iOS/${NameLower}_ios_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/iOS/${NameLower}_ios_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/iOS/${NameLower}_ios_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/iOS/${NameLower}_shared_ios_files.cmake b/Templates/CppToolGem/Template/Code/Platform/iOS/${NameLower}_shared_ios_files.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/iOS/${NameLower}_shared_ios_files.cmake rename to Templates/CppToolGem/Template/Code/Platform/iOS/${NameLower}_shared_ios_files.cmake diff --git a/Templates/CustomTool/Template/Code/Platform/iOS/PAL_ios.cmake b/Templates/CppToolGem/Template/Code/Platform/iOS/PAL_ios.cmake similarity index 100% rename from Templates/CustomTool/Template/Code/Platform/iOS/PAL_ios.cmake rename to Templates/CppToolGem/Template/Code/Platform/iOS/PAL_ios.cmake diff --git a/Templates/CustomTool/Template/Code/Source/${Name}.qrc b/Templates/CppToolGem/Template/Code/Source/${Name}.qrc similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}.qrc rename to Templates/CppToolGem/Template/Code/Source/${Name}.qrc diff --git a/Templates/CustomTool/Template/Code/Source/${Name}EditorModule.cpp b/Templates/CppToolGem/Template/Code/Source/${Name}EditorModule.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}EditorModule.cpp rename to Templates/CppToolGem/Template/Code/Source/${Name}EditorModule.cpp diff --git a/Templates/CustomTool/Template/Code/Source/${Name}EditorSystemComponent.cpp b/Templates/CppToolGem/Template/Code/Source/${Name}EditorSystemComponent.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}EditorSystemComponent.cpp rename to Templates/CppToolGem/Template/Code/Source/${Name}EditorSystemComponent.cpp diff --git a/Templates/CustomTool/Template/Code/Source/${Name}EditorSystemComponent.h b/Templates/CppToolGem/Template/Code/Source/${Name}EditorSystemComponent.h similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}EditorSystemComponent.h rename to Templates/CppToolGem/Template/Code/Source/${Name}EditorSystemComponent.h diff --git a/Templates/CustomTool/Template/Code/Source/${Name}Module.cpp b/Templates/CppToolGem/Template/Code/Source/${Name}Module.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}Module.cpp rename to Templates/CppToolGem/Template/Code/Source/${Name}Module.cpp diff --git a/Templates/CustomTool/Template/Code/Source/${Name}ModuleInterface.h b/Templates/CppToolGem/Template/Code/Source/${Name}ModuleInterface.h similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}ModuleInterface.h rename to Templates/CppToolGem/Template/Code/Source/${Name}ModuleInterface.h diff --git a/Templates/CustomTool/Template/Code/Source/${Name}SystemComponent.cpp b/Templates/CppToolGem/Template/Code/Source/${Name}SystemComponent.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}SystemComponent.cpp rename to Templates/CppToolGem/Template/Code/Source/${Name}SystemComponent.cpp diff --git a/Templates/CustomTool/Template/Code/Source/${Name}SystemComponent.h b/Templates/CppToolGem/Template/Code/Source/${Name}SystemComponent.h similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}SystemComponent.h rename to Templates/CppToolGem/Template/Code/Source/${Name}SystemComponent.h diff --git a/Templates/CustomTool/Template/Code/Source/${Name}Widget.cpp b/Templates/CppToolGem/Template/Code/Source/${Name}Widget.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}Widget.cpp rename to Templates/CppToolGem/Template/Code/Source/${Name}Widget.cpp diff --git a/Templates/CustomTool/Template/Code/Source/${Name}Widget.h b/Templates/CppToolGem/Template/Code/Source/${Name}Widget.h similarity index 100% rename from Templates/CustomTool/Template/Code/Source/${Name}Widget.h rename to Templates/CppToolGem/Template/Code/Source/${Name}Widget.h diff --git a/Templates/CustomTool/Template/Code/Source/toolbar_icon.svg b/Templates/CppToolGem/Template/Code/Source/toolbar_icon.svg similarity index 100% rename from Templates/CustomTool/Template/Code/Source/toolbar_icon.svg rename to Templates/CppToolGem/Template/Code/Source/toolbar_icon.svg diff --git a/Templates/CustomTool/Template/Code/Tests/${Name}EditorTest.cpp b/Templates/CppToolGem/Template/Code/Tests/${Name}EditorTest.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Tests/${Name}EditorTest.cpp rename to Templates/CppToolGem/Template/Code/Tests/${Name}EditorTest.cpp diff --git a/Templates/CustomTool/Template/Code/Tests/${Name}Test.cpp b/Templates/CppToolGem/Template/Code/Tests/${Name}Test.cpp similarity index 100% rename from Templates/CustomTool/Template/Code/Tests/${Name}Test.cpp rename to Templates/CppToolGem/Template/Code/Tests/${Name}Test.cpp diff --git a/Templates/CustomTool/Template/Platform/Android/android_gem.cmake b/Templates/CppToolGem/Template/Platform/Android/android_gem.cmake similarity index 100% rename from Templates/CustomTool/Template/Platform/Android/android_gem.cmake rename to Templates/CppToolGem/Template/Platform/Android/android_gem.cmake diff --git a/Templates/CustomTool/Template/Platform/Android/android_gem.json b/Templates/CppToolGem/Template/Platform/Android/android_gem.json similarity index 100% rename from Templates/CustomTool/Template/Platform/Android/android_gem.json rename to Templates/CppToolGem/Template/Platform/Android/android_gem.json diff --git a/Templates/CustomTool/Template/Platform/Linux/linux_gem.cmake b/Templates/CppToolGem/Template/Platform/Linux/linux_gem.cmake similarity index 100% rename from Templates/CustomTool/Template/Platform/Linux/linux_gem.cmake rename to Templates/CppToolGem/Template/Platform/Linux/linux_gem.cmake diff --git a/Templates/CustomTool/Template/Platform/Linux/linux_gem.json b/Templates/CppToolGem/Template/Platform/Linux/linux_gem.json similarity index 100% rename from Templates/CustomTool/Template/Platform/Linux/linux_gem.json rename to Templates/CppToolGem/Template/Platform/Linux/linux_gem.json diff --git a/Templates/CustomTool/Template/Platform/Mac/mac_gem.cmake b/Templates/CppToolGem/Template/Platform/Mac/mac_gem.cmake similarity index 100% rename from Templates/CustomTool/Template/Platform/Mac/mac_gem.cmake rename to Templates/CppToolGem/Template/Platform/Mac/mac_gem.cmake diff --git a/Templates/CustomTool/Template/Platform/Mac/mac_gem.json b/Templates/CppToolGem/Template/Platform/Mac/mac_gem.json similarity index 100% rename from Templates/CustomTool/Template/Platform/Mac/mac_gem.json rename to Templates/CppToolGem/Template/Platform/Mac/mac_gem.json diff --git a/Templates/CustomTool/Template/Platform/Windows/windows_gem.cmake b/Templates/CppToolGem/Template/Platform/Windows/windows_gem.cmake similarity index 100% rename from Templates/CustomTool/Template/Platform/Windows/windows_gem.cmake rename to Templates/CppToolGem/Template/Platform/Windows/windows_gem.cmake diff --git a/Templates/CustomTool/Template/Platform/Windows/windows_gem.json b/Templates/CppToolGem/Template/Platform/Windows/windows_gem.json similarity index 100% rename from Templates/CustomTool/Template/Platform/Windows/windows_gem.json rename to Templates/CppToolGem/Template/Platform/Windows/windows_gem.json diff --git a/Templates/CustomTool/Template/Platform/iOS/ios_gem.cmake b/Templates/CppToolGem/Template/Platform/iOS/ios_gem.cmake similarity index 100% rename from Templates/CustomTool/Template/Platform/iOS/ios_gem.cmake rename to Templates/CppToolGem/Template/Platform/iOS/ios_gem.cmake diff --git a/Templates/CustomTool/Template/Platform/iOS/ios_gem.json b/Templates/CppToolGem/Template/Platform/iOS/ios_gem.json similarity index 100% rename from Templates/CustomTool/Template/Platform/iOS/ios_gem.json rename to Templates/CppToolGem/Template/Platform/iOS/ios_gem.json diff --git a/Templates/CustomTool/Template/gem.json b/Templates/CppToolGem/Template/gem.json similarity index 100% rename from Templates/CustomTool/Template/gem.json rename to Templates/CppToolGem/Template/gem.json diff --git a/Templates/CustomTool/Template/preview.png b/Templates/CppToolGem/Template/preview.png similarity index 100% rename from Templates/CustomTool/Template/preview.png rename to Templates/CppToolGem/Template/preview.png diff --git a/Templates/CustomTool/template.json b/Templates/CppToolGem/template.json similarity index 98% rename from Templates/CustomTool/template.json rename to Templates/CppToolGem/template.json index e3221db106..b516dbaec3 100644 --- a/Templates/CustomTool/template.json +++ b/Templates/CppToolGem/template.json @@ -1,12 +1,12 @@ { - "template_name": "CustomTool", - "origin": "The primary repo for CustomTool goes here: i.e. http://www.mydomain.com", - "license": "What license CustomTool uses goes here: i.e. https://opensource.org/licenses/MIT", - "display_name": "CustomTool", + "template_name": "CppToolGem", + "origin": "The primary repo for CppToolGem goes here: i.e. http://www.mydomain.com", + "license": "What license CppToolGem uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "CppToolGem", "summary": "A gem template for a custom tool in C++ that gets registered with the Editor.", "canonical_tags": [], "user_tags": [ - "CustomTool" + "CppToolGem" ], "icon_path": "preview.png", "copyFiles": [ diff --git a/Templates/PythonGem/Template/CMakeLists.txt b/Templates/PythonToolGem/Template/CMakeLists.txt similarity index 100% rename from Templates/PythonGem/Template/CMakeLists.txt rename to Templates/PythonToolGem/Template/CMakeLists.txt diff --git a/Templates/PythonGem/Template/Code/${NameLower}_editor_files.cmake b/Templates/PythonToolGem/Template/Code/${NameLower}_editor_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/${NameLower}_editor_files.cmake rename to Templates/PythonToolGem/Template/Code/${NameLower}_editor_files.cmake diff --git a/Templates/PythonGem/Template/Code/${NameLower}_editor_shared_files.cmake b/Templates/PythonToolGem/Template/Code/${NameLower}_editor_shared_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/${NameLower}_editor_shared_files.cmake rename to Templates/PythonToolGem/Template/Code/${NameLower}_editor_shared_files.cmake diff --git a/Templates/PythonGem/Template/Code/${NameLower}_editor_tests_files.cmake b/Templates/PythonToolGem/Template/Code/${NameLower}_editor_tests_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/${NameLower}_editor_tests_files.cmake rename to Templates/PythonToolGem/Template/Code/${NameLower}_editor_tests_files.cmake diff --git a/Templates/PythonGem/Template/Code/CMakeLists.txt b/Templates/PythonToolGem/Template/Code/CMakeLists.txt similarity index 100% rename from Templates/PythonGem/Template/Code/CMakeLists.txt rename to Templates/PythonToolGem/Template/Code/CMakeLists.txt diff --git a/Templates/PythonGem/Template/Code/Include/${Name}/${Name}Bus.h b/Templates/PythonToolGem/Template/Code/Include/${Name}/${Name}Bus.h similarity index 100% rename from Templates/PythonGem/Template/Code/Include/${Name}/${Name}Bus.h rename to Templates/PythonToolGem/Template/Code/Include/${Name}/${Name}Bus.h diff --git a/Templates/PythonGem/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake b/Templates/PythonToolGem/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Linux/${NameLower}_linux_files.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake b/Templates/PythonToolGem/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Linux/${NameLower}_shared_linux_files.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Linux/PAL_linux.cmake b/Templates/PythonToolGem/Template/Code/Platform/Linux/PAL_linux.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Linux/PAL_linux.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Linux/PAL_linux.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake b/Templates/PythonToolGem/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Mac/${NameLower}_mac_files.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake b/Templates/PythonToolGem/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Mac/${NameLower}_shared_mac_files.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Mac/PAL_mac.cmake b/Templates/PythonToolGem/Template/Code/Platform/Mac/PAL_mac.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Mac/PAL_mac.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Mac/PAL_mac.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake b/Templates/PythonToolGem/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Windows/${NameLower}_shared_windows_files.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake b/Templates/PythonToolGem/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Windows/${NameLower}_windows_files.cmake diff --git a/Templates/PythonGem/Template/Code/Platform/Windows/PAL_windows.cmake b/Templates/PythonToolGem/Template/Code/Platform/Windows/PAL_windows.cmake similarity index 100% rename from Templates/PythonGem/Template/Code/Platform/Windows/PAL_windows.cmake rename to Templates/PythonToolGem/Template/Code/Platform/Windows/PAL_windows.cmake diff --git a/Templates/PythonGem/Template/Code/Source/${Name}EditorModule.cpp b/Templates/PythonToolGem/Template/Code/Source/${Name}EditorModule.cpp similarity index 100% rename from Templates/PythonGem/Template/Code/Source/${Name}EditorModule.cpp rename to Templates/PythonToolGem/Template/Code/Source/${Name}EditorModule.cpp diff --git a/Templates/PythonGem/Template/Code/Source/${Name}EditorSystemComponent.cpp b/Templates/PythonToolGem/Template/Code/Source/${Name}EditorSystemComponent.cpp similarity index 100% rename from Templates/PythonGem/Template/Code/Source/${Name}EditorSystemComponent.cpp rename to Templates/PythonToolGem/Template/Code/Source/${Name}EditorSystemComponent.cpp diff --git a/Templates/PythonGem/Template/Code/Source/${Name}EditorSystemComponent.h b/Templates/PythonToolGem/Template/Code/Source/${Name}EditorSystemComponent.h similarity index 100% rename from Templates/PythonGem/Template/Code/Source/${Name}EditorSystemComponent.h rename to Templates/PythonToolGem/Template/Code/Source/${Name}EditorSystemComponent.h diff --git a/Templates/PythonGem/Template/Code/Source/${Name}ModuleInterface.h b/Templates/PythonToolGem/Template/Code/Source/${Name}ModuleInterface.h similarity index 100% rename from Templates/PythonGem/Template/Code/Source/${Name}ModuleInterface.h rename to Templates/PythonToolGem/Template/Code/Source/${Name}ModuleInterface.h diff --git a/Templates/PythonGem/Template/Code/Tests/${Name}EditorTest.cpp b/Templates/PythonToolGem/Template/Code/Tests/${Name}EditorTest.cpp similarity index 100% rename from Templates/PythonGem/Template/Code/Tests/${Name}EditorTest.cpp rename to Templates/PythonToolGem/Template/Code/Tests/${Name}EditorTest.cpp diff --git a/Templates/PythonGem/Template/Editor/Scripts/${NameLower}_dialog.py b/Templates/PythonToolGem/Template/Editor/Scripts/${NameLower}_dialog.py similarity index 100% rename from Templates/PythonGem/Template/Editor/Scripts/${NameLower}_dialog.py rename to Templates/PythonToolGem/Template/Editor/Scripts/${NameLower}_dialog.py diff --git a/Templates/PythonGem/Template/Editor/Scripts/__init__.py b/Templates/PythonToolGem/Template/Editor/Scripts/__init__.py similarity index 100% rename from Templates/PythonGem/Template/Editor/Scripts/__init__.py rename to Templates/PythonToolGem/Template/Editor/Scripts/__init__.py diff --git a/Templates/PythonGem/Template/Editor/Scripts/bootstrap.py b/Templates/PythonToolGem/Template/Editor/Scripts/bootstrap.py similarity index 100% rename from Templates/PythonGem/Template/Editor/Scripts/bootstrap.py rename to Templates/PythonToolGem/Template/Editor/Scripts/bootstrap.py diff --git a/Templates/PythonGem/Template/gem.json b/Templates/PythonToolGem/Template/gem.json similarity index 100% rename from Templates/PythonGem/Template/gem.json rename to Templates/PythonToolGem/Template/gem.json diff --git a/Templates/PythonGem/Template/preview.png b/Templates/PythonToolGem/Template/preview.png similarity index 100% rename from Templates/PythonGem/Template/preview.png rename to Templates/PythonToolGem/Template/preview.png diff --git a/Templates/PythonGem/template.json b/Templates/PythonToolGem/template.json similarity index 94% rename from Templates/PythonGem/template.json rename to Templates/PythonToolGem/template.json index 75be757abb..4d85373ead 100644 --- a/Templates/PythonGem/template.json +++ b/Templates/PythonToolGem/template.json @@ -1,14 +1,14 @@ { - "template_name": "PythonGem", + "template_name": "PythonToolGem", "restricted_name": "o3de", "restricted_platform_relative_path": "Templates", - "origin": "The primary repo for PythonGem goes here: i.e. http://www.mydomain.com", - "license": "What license PythonGem uses goes here: i.e. https://opensource.org/licenses/MIT", - "display_name": "PythonGem", - "summary": "A short description of PythonGem.", + "origin": "The primary repo for PythonToolGem goes here: i.e. http://www.mydomain.com", + "license": "What license PythonToolGem uses goes here: i.e. https://opensource.org/licenses/MIT", + "display_name": "PythonToolGem", + "summary": "A gem template for a custom tool in Python that gets registered with the Editor.", "canonical_tags": [], "user_tags": [ - "PythonGem" + "PythonToolGem" ], "icon_path": "preview.png", "copyFiles": [ diff --git a/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/editor_test.py b/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/editor_test.py index 80a562977b..1e9d4c3654 100644 --- a/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/editor_test.py +++ b/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/editor_test.py @@ -3,34 +3,49 @@ Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. SPDX-License-Identifier: Apache-2.0 OR MIT -""" +Utility for specifying an Editor test, supports seamless parallelization and/or batching of tests. This is not a set of +tools to directly invoke, but a plugin with functions intended to be called by only the Pytest framework. """ -Utility for specifying an Editor test, supports seamless parallelization and/or batching of tests. -""" - +from __future__ import annotations import pytest import inspect __test__ = False -def pytest_addoption(parser): +def pytest_addoption(parser: argparse.ArgumentParser) -> None: + """ + Options when running editor tests in batches or parallel. + :param parser: The ArgumentParser object + :return: None + """ parser.addoption("--no-editor-batch", action="store_true", help="Don't batch multiple tests in single editor") parser.addoption("--no-editor-parallel", action="store_true", help="Don't run multiple editors in parallel") parser.addoption("--editors-parallel", type=int, action="store", help="Override the number editors to run at the same time") -# Create a custom custom item collection if the class defines pytest_custom_makeitem function -# This is used for automtically generating test functions with a custom collector -def pytest_pycollect_makeitem(collector, name, obj): +def pytest_pycollect_makeitem(collector: PyCollector, name: str, obj: object) -> PyCollector: + """ + Create a custom custom item collection if the class defines pytest_custom_makeitem function. This is used for + automatically generating test functions with a custom collector. + :param collector: The Pytest collector + :param name: Name of the collector + :param obj: The custom collector, normally an EditorTestSuite.EditorTestClass object + :return: Returns the custom collector + """ if inspect.isclass(obj): for base in obj.__bases__: if hasattr(base, "pytest_custom_makeitem"): return base.pytest_custom_makeitem(collector, name, obj) -# Add custom modification of items. -# This is used for adding the runners into the item list @pytest.hookimpl(hookwrapper=True) -def pytest_collection_modifyitems(session, items, config): +def pytest_collection_modifyitems(session: Session, items: list[EditorTestBase], config: Config) -> None: + """ + Add custom modification of items. This is used for adding the runners into the item list. + :param session: The Pytest Session + :param items: The test case functions + :param config: The Pytest Config object + :return: None + """ all_classes = set() for item in items: all_classes.add(item.instance.__class__) @@ -40,4 +55,4 @@ def pytest_collection_modifyitems(session, items, config): for cls in all_classes: if hasattr(cls, "pytest_custom_modify_items"): cls.pytest_custom_modify_items(session, items, config) - + diff --git a/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/test_tools_fixtures.py b/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/test_tools_fixtures.py index f4bda9b293..af29064766 100755 --- a/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/test_tools_fixtures.py +++ b/Tools/LyTestTools/ly_test_tools/_internal/pytest_plugin/test_tools_fixtures.py @@ -55,14 +55,6 @@ def pytest_configure(config): ly_test_tools._internal.pytest_plugin.build_directory = _get_build_directory(config) ly_test_tools._internal.pytest_plugin.output_path = _get_output_path(config) - -def pytest_pycollect_makeitem(collector, name, obj): - import inspect - if inspect.isclass(obj): - for base in obj.__bases__: - if hasattr(base, "pytest_custom_makeitem"): - return base.pytest_custom_makeitem(collector, name, obj) - def _get_build_directory(config): """ Fetch and verify the cmake build directory CLI arg, without creating an error when unset diff --git a/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py b/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py index ac4ad621da..5e96f34e95 100755 --- a/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py +++ b/Tools/LyTestTools/ly_test_tools/launchers/launcher_helper.py @@ -60,7 +60,7 @@ def create_editor(workspace, launcher_platform=ly_test_tools.HOST_OS_EDITOR, arg Editor is only officially supported on the Windows Platform. :param workspace: lumberyard workspace to use - :param launcher_platform: the platform to target for a launcher (i.e. 'windows_dedicated' for DedicatedWinLauncher) + :param launcher_platform: the platform to target for a launcher (i.e. 'windows_dedicated' for DedicatedWinLauncher) :param args: List of arguments to pass to the launcher's 'args' argument during construction :return: Editor instance """ diff --git a/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py b/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py index 781977cf33..ae6ed16321 100644 --- a/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py +++ b/Tools/LyTestTools/ly_test_tools/o3de/editor_test.py @@ -3,8 +3,29 @@ Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. SPDX-License-Identifier: Apache-2.0 OR MIT -""" +This file provides editor testing functionality to easily write automated editor tests for O3DE. +For using these utilities, you can subclass your test suite from EditorTestSuite, this allows an easy way of +specifying python test scripts that the editor will run without needing to write any boilerplace code. +It supports out of the box parallelization(running multiple editor instances at once), batching(running multiple tests +in the same editor instance) and crash detection. +Usage example: + class MyTestSuite(EditorTestSuite): + + class MyFirstTest(EditorSingleTest): + from . import script_to_be_run_by_editor as test_module + + class MyTestInParallel_1(EditorParallelTest): + from . import another_script_to_be_run_by_editor as test_module + + class MyTestInParallel_2(EditorParallelTest): + from . import yet_another_script_to_be_run_by_editor as test_module + + +EditorTestSuite does introspection of the defined classes inside of it and automatically prepares the tests, +parallelizing/batching as required +""" +from __future__ import annotations import pytest from _pytest.skipping import pytest_runtest_setup as skipping_pytest_runtest_setup @@ -25,30 +46,11 @@ import re import ly_test_tools.environment.file_system as file_system import ly_test_tools.environment.waiter as waiter import ly_test_tools.environment.process_utils as process_utils +import ly_test_tools.o3de.editor_test +import ly_test_tools.o3de.editor_test_utils as editor_utils from ly_test_tools.o3de.asset_processor import AssetProcessor from ly_test_tools.launchers.exceptions import WaitTimeoutError -from . import editor_test_utils as editor_utils - -# This file provides editor testing functionality to easily write automated editor tests for O3DE. -# For using these utilities, you can subclass your test suite from EditorTestSuite, this allows an easy way of specifying -# python test scripts that the editor will run without needing to write any boilerplace code. -# It supports out of the box parallelization(running multiple editor instances at once), batching(running multiple tests in the same editor instance) and -# crash detection. -# Usage example: -# class MyTestSuite(EditorTestSuite): -# -# class MyFirstTest(EditorSingleTest): -# from . import script_to_be_run_by_editor as test_module -# -# class MyTestInParallel_1(EditorParallelTest): -# from . import another_script_to_be_run_by_editor as test_module -# -# class MyTestInParallel_2(EditorParallelTest): -# from . import yet_another_script_to_be_run_by_editor as test_module -# -# -# EditorTestSuite does introspection of the defined classes inside of it and automatically prepares the tests, parallelizing/batching as required # This file contains no tests, but with this we make sure it won't be picked up by the runner since the file ends with _test __test__ = False @@ -109,12 +111,22 @@ class EditorBatchedTest(EditorSharedTest): class Result: class Base: def get_output_str(self): + # type () -> str + """ + Checks if the output attribute exists and returns it. + :return: Either the output string or a no output message + """ if hasattr(self, "output") and self.output is not None: return self.output else: return "-- No output --" def get_editor_log_str(self): + # type () -> str + """ + Checks if the editor_log attribute exists and returns it. + :return: Either the editor_log string or a no output message + """ if hasattr(self, "editor_log") and self.editor_log is not None: return self.editor_log else: @@ -122,7 +134,14 @@ class Result: class Pass(Base): @classmethod - def create(cls, test_spec : EditorTestBase, output : str, editor_log : str): + def create(cls, test_spec: EditorTestBase, output: str, editor_log: str) -> Pass: + """ + Creates a Pass object with a given test spec, output string, and editor log string. + :test_spec: The type of EditorTestBase + :output: The test output + :editor_log: The editor log's output + :return: the Pass object + """ r = cls() r.test_spec = test_spec r.output = output @@ -141,7 +160,14 @@ class Result: class Fail(Base): @classmethod - def create(cls, test_spec : EditorTestBase, output, editor_log : str): + def create(cls, test_spec: EditorTestBase, output: str, editor_log: str) -> Fail: + """ + Creates a Fail object with a given test spec, output string, and editor log string. + :test_spec: The type of EditorTestBase + :output: The test output + :editor_log: The editor log's output + :return: the Fail object + """ r = cls() r.test_spec = test_spec r.output = output @@ -164,7 +190,17 @@ class Result: class Crash(Base): @classmethod - def create(cls, test_spec : EditorTestBase, output : str, ret_code : int, stacktrace : str, editor_log : str): + def create(cls, test_spec: EditorTestBase, output: str, ret_code: int, stacktrace: str, editor_log: str) -> Crash: + """ + Creates a Crash object with a given test spec, output string, and editor log string. This also includes the + return code and stacktrace. + :test_spec: The type of EditorTestBase + :output: The test output + :ret_code: The test's return code + :stacktrace: The test's stacktrace if available + :editor_log: The editor log's output + :return: The Crash object + """ r = cls() r.output = output r.test_spec = test_spec @@ -190,12 +226,20 @@ class Result: f"--------------\n" f"{self.get_editor_log_str()}\n" ) - crash_str = "-- No crash information found --" return output class Timeout(Base): @classmethod - def create(cls, test_spec : EditorTestBase, output : str, time_secs : float, editor_log : str): + def create(cls, test_spec: EditorTestBase, output: str, time_secs: float, editor_log: str) -> Timeout: + """ + Creates a Timeout object with a given test spec, output string, and editor log string. The timeout time + should be provided in seconds + :test_spec: The type of EditorTestBase + :output: The test output + :time_secs: The timeout duration in seconds + :editor_log: The editor log's output + :return: The Timeout object + """ r = cls() r.output = output r.test_spec = test_spec @@ -219,14 +263,22 @@ class Result: class Unknown(Base): @classmethod - def create(cls, test_spec : EditorTestBase, output : str, extra_info : str, editor_log : str): + def create(cls, test_spec: EditorTestBase, output: str, extra_info: str, editor_log: str) -> Unknown: + """ + Creates an Unknown test results object if something goes wrong. + :test_spec: The type of EditorTestBase + :output: The test output + :extra_info: Any extra information as a string + :editor_log: The editor log's output + :return: The Unknown object + """ r = cls() r.output = output r.test_spec = test_spec r.editor_log = editor_log r.extra_info = extra_info return r - + def __str__(self): output = ( f"Unknown test result, possible cause: {self.extra_info}\n" @@ -262,7 +314,19 @@ class EditorTestSuite(): _TEST_FAIL_RETCODE = 0xF # Return code for test failure @pytest.fixture(scope="class") - def editor_test_data(self, request): + def editor_test_data(self, request: Request) -> TestData: + """ + Yields a per-testsuite structure to store the data of each test result and an AssetProcessor object that will be + re-used on the whole suite + :request: The Pytest request + :yield: The TestData object + """ + yield from self._editor_test_data(request) + + def _editor_test_data(self, request: Request) -> TestData: + """ + A wrapper function for unit testing to call directly + """ class TestData(): def __init__(self): self.results = {} # Dict of str(test_spec.__name__) -> Result @@ -444,9 +508,15 @@ class EditorTestSuite(): return EditorTestSuite.EditorTestClass(name, collector) @classmethod - def pytest_custom_modify_items(cls, session, items, config): - # Add here the runners functions and filter the tests that will be run. - # The runners will be added if they have any selected tests + def pytest_custom_modify_items(cls, session: Session, items: list[EditorTestBase], config: Config) -> None: + """ + Adds the runners' functions and filters the tests that will run. The runners will be added if they have any + selected tests + :param session: The Pytest Session + :param items: The test case functions + :param config: The Pytest Config object + :return: None + """ new_items = [] for runner in cls._runners: runner.tests[:] = cls.filter_session_shared_tests(items, runner.tests) @@ -462,24 +532,50 @@ class EditorTestSuite(): items[:] = items + new_items @classmethod - def get_single_tests(cls): + def get_single_tests(cls) -> list[EditorSingleTest]: + """ + Grabs all of the EditorSingleTests subclassed tests from the EditorTestSuite class + Usage example: + class MyTestSuite(EditorTestSuite): + class MyFirstTest(EditorSingleTest): + from . import script_to_be_run_by_editor as test_module + :return: The list of single tests + """ single_tests = [c[1] for c in cls.__dict__.items() if inspect.isclass(c[1]) and issubclass(c[1], EditorSingleTest)] return single_tests @classmethod - def get_shared_tests(cls): + def get_shared_tests(cls) -> list[EditorSharedTest]: + """ + Grabs all of the EditorSharedTests from the EditorTestSuite + Usage example: + class MyTestSuite(EditorTestSuite): + class MyFirstTest(EditorSharedTest): + from . import script_to_be_run_by_editor as test_module + :return: The list of shared tests + """ shared_tests = [c[1] for c in cls.__dict__.items() if inspect.isclass(c[1]) and issubclass(c[1], EditorSharedTest)] return shared_tests @classmethod - def get_session_shared_tests(cls, session): + def get_session_shared_tests(cls, session: Session) -> list[EditorTestBase]: + """ + Filters and returns all of the shared tests in a given session. + :session: The test session + :return: The list of tests + """ shared_tests = cls.get_shared_tests() return cls.filter_session_shared_tests(session, shared_tests) @staticmethod - def filter_session_shared_tests(session_items, shared_tests): - # Retrieve the test sub-set that was collected - # this can be less than the original set if were overriden via -k argument or similars + def filter_session_shared_tests(session_items: list[EditorTestBase], shared_tests: list[EditorSharedTest]) -> list[EditorTestBase]: + """ + Retrieve the test sub-set that was collected this can be less than the original set if were overriden via -k + argument or similars + :session_items: The tests in a session to run + :shared_tests: All of the shared tests + :return: The list of filtered tests + """ def will_run(item): try: skipping_pytest_runtest_setup(item) @@ -488,13 +584,20 @@ class EditorTestSuite(): return False session_items_by_name = { item.originalname:item for item in session_items } - selected_shared_tests = [test for test in shared_tests if test.__name__ in session_items_by_name.keys() and will_run(session_items_by_name[test.__name__])] + selected_shared_tests = [test for test in shared_tests if test.__name__ in session_items_by_name.keys() and + will_run(session_items_by_name[test.__name__])] return selected_shared_tests @staticmethod - def filter_shared_tests(shared_tests, is_batchable=False, is_parallelizable=False): - # Retrieve the test sub-set that was collected - # this can be less than the original set if were overriden via -k argument or similars + def filter_shared_tests(shared_tests: list[EditorSharedTest], is_batchable: bool = False, + is_parallelizable: bool = False) -> list[EditorSharedTest]: + """ + Filters and returns all tests based off of if they are batchable and/or parallelizable + :shared_tests: All shared tests + :is_batchable: Filter to batchable tests + :is_parallelizable: Filter to parallelizable tests + :return: The list of filtered tests + """ return [ t for t in shared_tests if ( getattr(t, "is_batchable", None) is is_batchable @@ -504,9 +607,14 @@ class EditorTestSuite(): ] ### Utils ### - - # Prepares the asset processor for the test - def _prepare_asset_processor(self, workspace, editor_test_data): + def _prepare_asset_processor(self, workspace: AbstractWorkspace, editor_test_data: TestData) -> None: + """ + Prepares the asset processor for the test depending on whether or not the process is open and if the current + test owns it. + :workspace: The workspace object in case an AssetProcessor object needs to be created + :editor_test_data: The test data from calling editor_test_data() + :return: None + """ try: # Start-up an asset processor if we are not running one # If another AP process exist, don't kill it, as we don't own it @@ -524,15 +632,28 @@ class EditorTestSuite(): editor_test_data.asset_processor = None raise ex - def _setup_editor_test(self, editor, workspace, editor_test_data): + def _setup_editor_test(self, editor: Editor, workspace: AbstractWorkspace, editor_test_data: TestData) -> None: + """ + Sets up an editor test by preparing the Asset Processor, killing all other O3DE processes, and configuring + :editor: The launcher Editor object + :workspace: The test Workspace object + :editor_test_data: The TestData from calling editor_test_data() + :return: None + """ self._prepare_asset_processor(workspace, editor_test_data) editor_utils.kill_all_ly_processes(include_asset_processor=False) editor.configure_settings() - # Utility function for parsing the output information from the editor. - # It deserializes the JSON content printed in the output for every test and returns that information. @staticmethod - def _get_results_using_output(test_spec_list, output, editor_log_content): + def _get_results_using_output(test_spec_list: list[EditorTestBase], output: str, editor_log_content: str) -> dict[str, Result]: + """ + Utility function for parsing the output information from the editor. It deserializes the JSON content printed in + the output for every test and returns that information. + :test_spec_list: The list of EditorTests + :output: The Editor from Editor.get_output() + :editor_log_content: The contents of the editor log as a string + :return: A dict of the tests and their respective Result objects + """ results = {} pattern = re.compile(r"JSON_START\((.+?)\)JSON_END") out_matches = pattern.finditer(output) @@ -558,7 +679,9 @@ class EditorTestSuite(): for test_spec in test_spec_list: name = editor_utils.get_module_filename(test_spec.test_module) if name not in found_jsons.keys(): - results[test_spec.__name__] = Result.Unknown.create(test_spec, output, "Couldn't find any test run information on stdout", editor_log_content) + results[test_spec.__name__] = Result.Unknown.create(test_spec, output, + "Couldn't find any test run information on stdout", + editor_log_content) else: result = None json_result = found_jsons[name] @@ -581,9 +704,14 @@ class EditorTestSuite(): return results - # Fails the test if the test result is not a PASS, specifying the information @staticmethod - def _report_result(name : str, result : Result.Base): + def _report_result(name: str, result: Result) -> None: + """ + Fails the test if the test result is not a PASS, specifying the information + :name: Name of the test + :result: The Result object which denotes if the test passed or not + :return: None + """ if isinstance(result, Result.Pass): output_str = f"Test {name}:\n{str(result)}" print(output_str) @@ -592,10 +720,19 @@ class EditorTestSuite(): pytest.fail(error_str) ### Running tests ### - # Starts the editor with the given test and retuns an result dict with a single element specifying the result - def _exec_editor_test(self, request, workspace, editor, run_id : int, log_name : str, - test_spec : EditorTestBase, cmdline_args : List[str] = []): - + def _exec_editor_test(self, request: Request, workspace: AbstractWorkspace, editor: Editor, run_id: int, + log_name: str, test_spec: EditorTestBase, cmdline_args: list[str] = []) -> dict[str, Result]: + """ + Starts the editor with the given test and retuns an result dict with a single element specifying the result + :request: The pytest request + :workspace: The LyTestTools Workspace object + :editor: The LyTestTools Editor object + :run_id: The unique run id + :log_name: The name of the editor log to retrieve + :test_spec: The type of EditorTestBase + :cmdline_args: Any additional command line args + :return: a dictionary of Result objects + """ test_cmdline_args = self.global_extra_cmdline_args + cmdline_args test_spec_uses_null_renderer = getattr(test_spec, "use_null_renderer", None) if test_spec_uses_null_renderer or (test_spec_uses_null_renderer is None and self.use_null_renderer): @@ -629,12 +766,14 @@ class EditorTestSuite(): else: has_crashed = return_code != EditorTestSuite._TEST_FAIL_RETCODE if has_crashed: - test_result = Result.Crash.create(test_spec, output, return_code, editor_utils.retrieve_crash_output(run_id, workspace, self._TIMEOUT_CRASH_LOG), None) + test_result = Result.Crash.create(test_spec, output, return_code, editor_utils.retrieve_crash_output + (run_id, workspace, self._TIMEOUT_CRASH_LOG), None) editor_utils.cycle_crash_report(run_id, workspace) else: test_result = Result.Fail.create(test_spec, output, editor_log_content) except WaitTimeoutError: - editor.kill() + output = editor.get_output() + editor.kill() editor_log_content = editor_utils.retrieve_editor_log_content(run_id, log_name, workspace) test_result = Result.Timeout.create(test_spec, output, test_spec.timeout, editor_log_content) @@ -643,11 +782,21 @@ class EditorTestSuite(): results[test_spec.__name__] = test_result return results - # Starts an editor executable with a list of tests and returns a dict of the result of every test ran within that editor - # instance. In case of failure this function also parses the editor output to find out what specific tests failed - def _exec_editor_multitest(self, request, workspace, editor, run_id : int, log_name : str, - test_spec_list : List[EditorTestBase], cmdline_args=[]): - + def _exec_editor_multitest(self, request: Request, workspace: AbstractWorkspace, editor: Editor, run_id: int, log_name: str, + test_spec_list: list[EditorTestBase], cmdline_args: list[str] = []) -> dict[str, Result]: + """ + Starts an editor executable with a list of tests and returns a dict of the result of every test ran within that + editor instance. In case of failure this function also parses the editor output to find out what specific tests + failed. + :request: The pytest request + :workspace: The LyTestTools Workspace object + :editor: The LyTestTools Editor object + :run_id: The unique run id + :log_name: The name of the editor log to retrieve + :test_spec_list: A list of EditorTestBase tests to run in the same editor instance + :cmdline_args: Any additional command line args + :return: A dict of Result objects + """ test_cmdline_args = self.global_extra_cmdline_args + cmdline_args if self.use_null_renderer: test_cmdline_args += ["-rhi=null"] @@ -695,50 +844,66 @@ class EditorTestSuite(): if isinstance(result, Result.Unknown): if not crashed_result: # The first test with "Unknown" result (no data in output) is likely the one that crashed - crash_error = editor_utils.retrieve_crash_output(run_id, workspace, self._TIMEOUT_CRASH_LOG) + crash_error = editor_utils.retrieve_crash_output(run_id, workspace, + self._TIMEOUT_CRASH_LOG) editor_utils.cycle_crash_report(run_id, workspace) - results[test_spec_name] = Result.Crash.create(result.test_spec, output, return_code, crash_error, result.editor_log) + results[test_spec_name] = Result.Crash.create(result.test_spec, output, return_code, + crash_error, result.editor_log) crashed_result = result else: - # If there are remaning "Unknown" results, these couldn't execute because of the crash, update with info about the offender - results[test_spec_name].extra_info = f"This test has unknown result, test '{crashed_result.test_spec.__name__}' crashed before this test could be executed" - + # If there are remaning "Unknown" results, these couldn't execute because of the crash, + # update with info about the offender + results[test_spec_name].extra_info = f"This test has unknown result," \ + f"test '{crashed_result.test_spec.__name__}'" \ + f"crashed before this test could be executed" # if all the tests ran, the one that has caused the crash is the last test if not crashed_result: crash_error = editor_utils.retrieve_crash_output(run_id, workspace, self._TIMEOUT_CRASH_LOG) editor_utils.cycle_crash_report(run_id, workspace) - results[test_spec_name] = Result.Crash.create(crashed_result.test_spec, output, return_code, crash_error, crashed_result.editor_log) - - + results[test_spec_name] = Result.Crash.create(crashed_result.test_spec, output, return_code, + crash_error, crashed_result.editor_log) except WaitTimeoutError: editor.kill() - output = editor.get_output() editor_log_content = editor_utils.retrieve_editor_log_content(run_id, log_name, workspace) # The editor timed out when running the tests, get the data from the output to find out which ones ran results = self._get_results_using_output(test_spec_list, output, editor_log_content) assert len(results) == len(test_spec_list), "bug in _get_results_using_output(), the number of results don't match the tests ran" - # Similar logic here as crashes, the first test that has no result is the one that timed out timed_out_result = None for test_spec_name, result in results.items(): if isinstance(result, Result.Unknown): if not timed_out_result: - results[test_spec_name] = Result.Timeout.create(result.test_spec, result.output, self.timeout_editor_shared_test, result.editor_log) + results[test_spec_name] = Result.Timeout.create(result.test_spec, result.output, + self.timeout_editor_shared_test, + result.editor_log) timed_out_result = result else: - # If there are remaning "Unknown" results, these couldn't execute because of the timeout, update with info about the offender - results[test_spec_name].extra_info = f"This test has unknown result, test '{timed_out_result.test_spec.__name__}' timed out before this test could be executed" - + # If there are remaning "Unknown" results, these couldn't execute because of the timeout, + # update with info about the offender + results[test_spec_name].extra_info = f"This test has unknown result, test " \ + f"'{timed_out_result.test_spec.__name__}' timed out " \ + f"before this test could be executed" # if all the tests ran, the one that has caused the timeout is the last test, as it didn't close the editor if not timed_out_result: - results[test_spec_name] = Result.Timeout.create(timed_out_result.test_spec, results[test_spec_name].output, self.timeout_editor_shared_test, result.editor_log) + results[test_spec_name] = Result.Timeout.create(timed_out_result.test_spec, + results[test_spec_name].output, + self.timeout_editor_shared_test, result.editor_log) return results - # Runs a single test (one editor, one test) with the given specs - def _run_single_test(self, request, workspace, editor, editor_test_data, test_spec : EditorSingleTest): + def _run_single_test(self, request: Request, workspace: AbstractWorkspace, editor: Editor, + editor_test_data: TestData, test_spec: EditorSingleTest) -> None: + """ + Runs a single test (one editor, one test) with the given specs + :request: The Pytest Request + :workspace: The LyTestTools Workspace object + :editor: The LyTestTools Editor object + :editor_test_data: The TestData from calling editor_test_data() + :test_spec: The test class that should be a subclass of EditorSingleTest + :return: None + """ self._setup_editor_test(editor, workspace, editor_test_data) extra_cmdline_args = [] if hasattr(test_spec, "extra_cmdline_args"): @@ -749,18 +914,39 @@ class EditorTestSuite(): test_name, test_result = next(iter(results.items())) self._report_result(test_name, test_result) - # Runs a batch of tests in one single editor with the given spec list (one editor, multiple tests) - def _run_batched_tests(self, request, workspace, editor, editor_test_data, test_spec_list : List[EditorSharedTest], extra_cmdline_args=[]): + def _run_batched_tests(self, request: Request, workspace: AbstractWorkspace, editor: Editor, editor_test_data: TestData, + test_spec_list: list[EditorSharedTest], extra_cmdline_args: list[str] = []) -> None: + """ + Runs a batch of tests in one single editor with the given spec list (one editor, multiple tests) + :request: The Pytest Request + :workspace: The LyTestTools Workspace object + :editor: The LyTestTools Editor object + :editor_test_data: The TestData from calling editor_test_data() + :test_spec_list: A list of EditorSharedTest tests to run + :extra_cmdline_args: Any extra command line args in a list + :return: None + """ if not test_spec_list: return self._setup_editor_test(editor, workspace, editor_test_data) - results = self._exec_editor_multitest(request, workspace, editor, 1, "editor_test.log", test_spec_list, extra_cmdline_args) + results = self._exec_editor_multitest(request, workspace, editor, 1, "editor_test.log", test_spec_list, + extra_cmdline_args) assert results is not None editor_test_data.results.update(results) - # Runs multiple editors with one test on each editor (multiple editor, one test each) - def _run_parallel_tests(self, request, workspace, editor, editor_test_data, test_spec_list : List[EditorSharedTest], extra_cmdline_args=[]): + def _run_parallel_tests(self, request: Request, workspace: AbstractWorkspace, editor: Editor, editor_test_data: TestData, + test_spec_list: list[EditorSharedTest], extra_cmdline_args: list[str] = []) -> None: + """ + Runs multiple editors with one test on each editor (multiple editor, one test each) + :request: The Pytest Request + :workspace: The LyTestTools Workspace object + :editor: The LyTestTools Editor object + :editor_test_data: The TestData from calling editor_test_data() + :test_spec_list: A list of EditorSharedTest tests to run + :extra_cmdline_args: Any extra command line args in a list + :return: None + """ if not test_spec_list: return @@ -778,7 +964,8 @@ class EditorTestSuite(): for i in range(total_threads): def make_func(test_spec, index, my_editor): def run(request, workspace, extra_cmdline_args): - results = self._exec_editor_test(request, workspace, my_editor, index+1, f"editor_test.log", test_spec, extra_cmdline_args) + results = self._exec_editor_test(request, workspace, my_editor, index+1, f"editor_test.log", + test_spec, extra_cmdline_args) assert results is not None results_per_thread[index] = results return run @@ -796,8 +983,18 @@ class EditorTestSuite(): for result in results_per_thread: editor_test_data.results.update(result) - # Runs multiple editors with a batch of tests for each editor (multiple editor, multiple tests each) - def _run_parallel_batched_tests(self, request, workspace, editor, editor_test_data, test_spec_list : List[EditorSharedTest], extra_cmdline_args=[]): + def _run_parallel_batched_tests(self, request: Request, workspace: AbstractWorkspace, editor: Editor, editor_test_data: TestData, + test_spec_list: list[EditorSharedTest], extra_cmdline_args: list[str] = []) -> None: + """ + Runs multiple editors with a batch of tests for each editor (multiple editor, multiple tests each) + :request: The Pytest Request + :workspace: The LyTestTools Workspace object + :editor: The LyTestTools Editor object + :editor_test_data: The TestData from calling editor_test_data() + :test_spec_list: A list of EditorSharedTest tests to run + :extra_cmdline_args: Any extra command line args in a list + :return: None + """ if not test_spec_list: return @@ -813,7 +1010,9 @@ class EditorTestSuite(): def run(request, workspace, extra_cmdline_args): results = None if len(test_spec_list_for_editor) > 0: - results = self._exec_editor_multitest(request, workspace, my_editor, index+1, f"editor_test.log", test_spec_list_for_editor, extra_cmdline_args) + results = self._exec_editor_multitest(request, workspace, my_editor, index+1, + f"editor_test.log", test_spec_list_for_editor, + extra_cmdline_args) assert results is not None else: results = {} @@ -833,8 +1032,12 @@ class EditorTestSuite(): for result in results_per_thread: editor_test_data.results.update(result) - # Retrieves the number of parallel preference cmdline overrides - def _get_number_parallel_editors(self, request): + def _get_number_parallel_editors(self, request: Request) -> int: + """ + Retrieves the number of parallel preference cmdline overrides + :request: The Pytest Request + :return: The number of parallel editors to use + """ parallel_editors_value = request.config.getoption("--editors-parallel", None) if parallel_editors_value: return int(parallel_editors_value) diff --git a/Tools/LyTestTools/ly_test_tools/o3de/editor_test_utils.py b/Tools/LyTestTools/ly_test_tools/o3de/editor_test_utils.py index feff78d866..35fbe93d37 100644 --- a/Tools/LyTestTools/ly_test_tools/o3de/editor_test_utils.py +++ b/Tools/LyTestTools/ly_test_tools/o3de/editor_test_utils.py @@ -3,8 +3,10 @@ Copyright (c) Contributors to the Open 3D Engine Project. For complete copyright and license terms please see the LICENSE at the root of this distribution. SPDX-License-Identifier: Apache-2.0 OR MIT -""" +Utility functions mostly for the editor_test module. They can also be used for assisting Editor tests. +""" +from __future__ import annotations import os import time import logging @@ -14,7 +16,13 @@ import ly_test_tools.environment.waiter as waiter logger = logging.getLogger(__name__) -def kill_all_ly_processes(include_asset_processor=True): +def kill_all_ly_processes(include_asset_processor: bool = True) -> None: + """ + Kills all common O3DE processes such as the Editor, Game Launchers, and optionally Asset Processor. Defaults to + killing the Asset Processor. + :param include_asset_processor: Boolean flag whether or not to kill the AP + :return: None + """ LY_PROCESSES = [ 'Editor', 'Profiler', 'RemoteConsole', ] @@ -27,8 +35,7 @@ def kill_all_ly_processes(include_asset_processor=True): else: process_utils.kill_processes_named(LY_PROCESSES, ignore_extensions=True) -def get_testcase_module_filepath(testcase_module): - # type: (Module) -> str +def get_testcase_module_filepath(testcase_module: Module) -> str: """ return the full path of the test module using always '.py' extension :param testcase_module: The testcase python module being tested @@ -36,8 +43,7 @@ def get_testcase_module_filepath(testcase_module): """ return os.path.splitext(testcase_module.__file__)[0] + ".py" -def get_module_filename(testcase_module): - # type: (Module) -> str +def get_module_filename(testcase_module: Module): """ return The filename of the module without path Note: This is differs from module.__name__ in the essence of not having the package directory. @@ -47,7 +53,7 @@ def get_module_filename(testcase_module): """ return os.path.splitext(os.path.basename(testcase_module.__file__))[0] -def retrieve_log_path(run_id : int, workspace): +def retrieve_log_path(run_id: int, workspace: AbstractWorkspaceManager) -> str: """ return the log/ project path for this test run. :param run_id: editor id that will be used for differentiating paths @@ -56,7 +62,7 @@ def retrieve_log_path(run_id : int, workspace): """ return os.path.join(workspace.paths.project(), "user", f"log_test_{run_id}") -def retrieve_crash_output(run_id : int, workspace, timeout : float): +def retrieve_crash_output(run_id: int, workspace: AbstractWorkspaceManager, timeout: float = 10) -> str: """ returns the crash output string for the given test run. :param run_id: editor id that will be used for differentiating paths @@ -79,7 +85,7 @@ def retrieve_crash_output(run_id : int, workspace, timeout : float): crash_info += f"\n{str(ex)}" return crash_info -def cycle_crash_report(run_id : int, workspace): +def cycle_crash_report(run_id: int, workspace: AbstractWorkspaceManager) -> None: """ Attempts to rename error.log and error.dmp(crash files) into new names with the timestamp on it. :param run_id: editor id that will be used for differentiating paths @@ -99,10 +105,11 @@ def cycle_crash_report(run_id : int, workspace): except Exception as ex: logger.warning(f"Couldn't cycle file {filepath}. Error: {str(ex)}") -def retrieve_editor_log_content(run_id : int, log_name : str, workspace, timeout=10): +def retrieve_editor_log_content(run_id: int, log_name: str, workspace: AbstractWorkspaceManager, timeout: int = 10) -> str: """ Retrieves the contents of the given editor log file. :param run_id: editor id that will be used for differentiating paths + :log_name: The name of the editor log to retrieve :param workspace: Workspace fixture :timeout: Maximum time to wait for the log file to appear :return str: The contents of the log @@ -124,7 +131,7 @@ def retrieve_editor_log_content(run_id : int, log_name : str, workspace, timeout editor_info = f"-- Error reading editor.log: {str(ex)} --" return editor_info -def retrieve_last_run_test_index_from_output(test_spec_list, output : str): +def retrieve_last_run_test_index_from_output(test_spec_list: list[EditorTestBase], output: str) -> int: """ Finds out what was the last test that was run by inspecting the input. This is used for determining what was the batched test has crashed the editor diff --git a/Tools/LyTestTools/tests/unit/test_editor_test_utils.py b/Tools/LyTestTools/tests/unit/test_editor_test_utils.py new file mode 100644 index 0000000000..bf7fae7189 --- /dev/null +++ b/Tools/LyTestTools/tests/unit/test_editor_test_utils.py @@ -0,0 +1,162 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" +import pytest +import os +import unittest.mock as mock +import unittest + +import ly_test_tools.o3de.editor_test_utils as editor_test_utils + +pytestmark = pytest.mark.SUITE_smoke + +class TestEditorTestUtils(unittest.TestCase): + + @mock.patch('ly_test_tools.environment.process_utils.kill_processes_named') + def test_KillAllLyProcesses_IncludeAP_CallsCorrectly(self, mock_kill_processes_named): + process_list = ['Editor', 'Profiler', 'RemoteConsole', 'AssetProcessor', 'AssetProcessorBatch', 'AssetBuilder'] + + editor_test_utils.kill_all_ly_processes(include_asset_processor=True) + mock_kill_processes_named.assert_called_once_with(process_list, ignore_extensions=True) + + @mock.patch('ly_test_tools.environment.process_utils.kill_processes_named') + def test_KillAllLyProcesses_NotIncludeAP_CallsCorrectly(self, mock_kill_processes_named): + process_list = ['Editor', 'Profiler', 'RemoteConsole'] + ap_process_list = ['AssetProcessor', 'AssetProcessorBatch', 'AssetBuilder'] + + editor_test_utils.kill_all_ly_processes(include_asset_processor=False) + mock_kill_processes_named.assert_called_once() + assert ap_process_list not in mock_kill_processes_named.call_args[0] + + def test_GetTestcaseModuleFilepath_NoExtension_ReturnsPYExtension(self): + mock_module = mock.MagicMock() + file_path = os.path.join('path', 'under_test') + mock_module.__file__ = file_path + + assert file_path + '.py' == editor_test_utils.get_testcase_module_filepath(mock_module) + + def test_GetTestcaseModuleFilepath_PYExtension_ReturnsPYExtension(self): + mock_module = mock.MagicMock() + file_path = os.path.join('path', 'under_test.py') + mock_module.__file__ = file_path + + assert file_path == editor_test_utils.get_testcase_module_filepath(mock_module) + + def test_GetModuleFilename_PythonModule_ReturnsFilename(self): + mock_module = mock.MagicMock() + file_path = os.path.join('path', 'under_test.py') + mock_module.__file__ = file_path + + assert 'under_test' == editor_test_utils.get_module_filename(mock_module) + + def test_RetrieveLogPath_NormalProject_ReturnsLogPath(self): + mock_workspace = mock.MagicMock() + mock_workspace.paths.project.return_value = 'mock_project_path' + expected = os.path.join('mock_project_path', 'user', 'log_test_0') + + assert expected == editor_test_utils.retrieve_log_path(0, mock_workspace) + + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.environment.waiter.wait_for', mock.MagicMock()) + def test_RetrieveCrashOutput_CrashLogExists_ReturnsLogInfo(self, mock_retrieve_log_path): + mock_retrieve_log_path.return_value = 'mock_log_path' + mock_workspace = mock.MagicMock() + mock_log = 'mock crash info' + + with mock.patch('builtins.open', mock.mock_open(read_data=mock_log)) as mock_file: + assert mock_log == editor_test_utils.retrieve_crash_output(0, mock_workspace, 0) + + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.environment.waiter.wait_for', mock.MagicMock()) + def test_RetrieveCrashOutput_CrashLogNotExists_ReturnsError(self, mock_retrieve_log_path): + mock_retrieve_log_path.return_value = 'mock_log_path' + mock_workspace = mock.MagicMock() + expected = "-- No crash log available --\n[Errno 2] No such file or directory: 'mock_log_path\\\\error.log'" + + assert expected == editor_test_utils.retrieve_crash_output(0, mock_workspace, 0) + + @mock.patch('os.path.getmtime', mock.MagicMock()) + @mock.patch('os.rename') + @mock.patch('time.strftime') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('os.path.exists') + def test_CycleCrashReport_DmpExists_NamedCorrectly(self, mock_exists, mock_retrieve_log_path, mock_strftime, + mock_rename): + mock_exists.side_effect = [False, True] + mock_retrieve_log_path.return_value = 'mock_log_path' + mock_workspace = mock.MagicMock() + mock_strftime.return_value = 'mock_strftime' + + editor_test_utils.cycle_crash_report(0, mock_workspace) + mock_rename.assert_called_once_with(os.path.join('mock_log_path', 'error.dmp'), + os.path.join('mock_log_path', 'error_mock_strftime.dmp')) + + @mock.patch('os.path.getmtime', mock.MagicMock()) + @mock.patch('os.rename') + @mock.patch('time.strftime') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('os.path.exists') + def test_CycleCrashReport_LogExists_NamedCorrectly(self, mock_exists, mock_retrieve_log_path, mock_strftime, + mock_rename): + mock_exists.side_effect = [True, False] + mock_retrieve_log_path.return_value = 'mock_log_path' + mock_workspace = mock.MagicMock() + mock_strftime.return_value = 'mock_strftime' + + editor_test_utils.cycle_crash_report(0, mock_workspace) + mock_rename.assert_called_once_with(os.path.join('mock_log_path', 'error.log'), + os.path.join('mock_log_path', 'error_mock_strftime.log')) + + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.environment.waiter.wait_for', mock.MagicMock()) + def test_RetrieveEditorLogContent_CrashLogExists_ReturnsLogInfo(self, mock_retrieve_log_path): + mock_retrieve_log_path.return_value = 'mock_log_path' + mock_logname = 'mock_log.log' + mock_workspace = mock.MagicMock() + mock_log = 'mock log info' + + with mock.patch('builtins.open', mock.mock_open(read_data=mock_log)) as mock_file: + assert f'[editor.log] {mock_log}' == editor_test_utils.retrieve_editor_log_content(0, mock_logname, mock_workspace) + + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.environment.waiter.wait_for', mock.MagicMock()) + def test_RetrieveEditorLogContent_CrashLogNotExists_ReturnsError(self, mock_retrieve_log_path): + mock_retrieve_log_path.return_value = 'mock_log_path' + mock_logname = 'mock_log.log' + mock_workspace = mock.MagicMock() + expected = f"-- Error reading editor.log" + + assert expected in editor_test_utils.retrieve_editor_log_content(0, mock_logname, mock_workspace) + + def test_RetrieveLastRunTestIndexFromOutput_SecondTestFailed_Returns0(self): + mock_test = mock.MagicMock() + mock_test.__name__ = 'mock_test_name' + mock_test_list = [mock_test] + mock_editor_output = 'mock_test_name\n' \ + 'mock_test_name_1' + + assert 0 == editor_test_utils.retrieve_last_run_test_index_from_output(mock_test_list, mock_editor_output) + + def test_RetrieveLastRunTestIndexFromOutput_TenthTestFailed_Returns9(self): + mock_test_list = [] + mock_editor_output = '' + for x in range(10): + mock_test = mock.MagicMock() + mock_test.__name__ = f'mock_test_name_{x}' + mock_test_list.append(mock_test) + mock_editor_output += f'{mock_test.__name__}\n' + mock_editor_output += 'mock_test_name_x' + assert 9 == editor_test_utils.retrieve_last_run_test_index_from_output(mock_test_list, mock_editor_output) + + def test_RetrieveLastRunTestIndexFromOutput_FirstItemFailed_Returns0(self): + mock_test_list = [] + mock_editor_output = '' + for x in range(10): + mock_test = mock.MagicMock() + mock_test.__name__ = f'mock_test_name_{x}' + mock_test_list.append(mock_test) + + assert 0 == editor_test_utils.retrieve_last_run_test_index_from_output(mock_test_list, mock_editor_output) diff --git a/Tools/LyTestTools/tests/unit/test_o3de_editor_test.py b/Tools/LyTestTools/tests/unit/test_o3de_editor_test.py new file mode 100644 index 0000000000..3f6c23ea3d --- /dev/null +++ b/Tools/LyTestTools/tests/unit/test_o3de_editor_test.py @@ -0,0 +1,1099 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" +import unittest + +import pytest +import unittest.mock as mock + +import ly_test_tools +import ly_test_tools.o3de.editor_test as editor_test + +pytestmark = pytest.mark.SUITE_smoke + +class TestEditorTestBase(unittest.TestCase): + + def test_EditorSharedTest_Init_CorrectAttributes(self): + mock_editorsharedtest = editor_test.EditorSharedTest() + assert mock_editorsharedtest.is_batchable == True + assert mock_editorsharedtest.is_parallelizable == True + + def test_EditorParallelTest_Init_CorrectAttributes(self): + mock_editorsharedtest = editor_test.EditorParallelTest() + assert mock_editorsharedtest.is_batchable == False + assert mock_editorsharedtest.is_parallelizable == True + + def test_EditorBatchedTest_Init_CorrectAttributes(self): + mock_editorsharedtest = editor_test.EditorBatchedTest() + assert mock_editorsharedtest.is_batchable == True + assert mock_editorsharedtest.is_parallelizable == False + +class TestResultBase(unittest.TestCase): + + def setUp(self): + self.mock_result = editor_test.Result.Base() + + def test_GetOutputStr_HasOutput_ReturnsCorrectly(self): + self.mock_result.output = 'expected output' + assert self.mock_result.get_output_str() == 'expected output' + + def test_GetOutputStr_NoOutput_ReturnsCorrectly(self): + self.mock_result.output = None + assert self.mock_result.get_output_str() == '-- No output --' + + def test_GetEditorLogStr_HasOutput_ReturnsCorrectly(self): + self.mock_result.editor_log = 'expected log output' + assert self.mock_result.get_editor_log_str() == 'expected log output' + + def test_GetEditorLogStr_NoOutput_ReturnsCorrectly(self): + self.mock_result.editor_log = None + assert self.mock_result.get_editor_log_str() == '-- No editor log found --' + +class TestResultPass(unittest.TestCase): + + def test_Create_ValidArgs_CorrectAttributes(self): + mock_test_spec = mock.MagicMock() + mock_output = mock.MagicMock() + mock_editor_log = mock.MagicMock() + + mock_pass = editor_test.Result.Pass.create(mock_test_spec, mock_output, mock_editor_log) + assert mock_pass.test_spec == mock_test_spec + assert mock_pass.output == mock_output + assert mock_pass.editor_log == mock_editor_log + + def test_Str_ValidString_ReturnsOutput(self): + mock_test_spec = mock.MagicMock() + mock_output = 'mock_output' + mock_editor_log = mock.MagicMock() + + mock_pass = editor_test.Result.Pass.create(mock_test_spec, mock_output, mock_editor_log) + assert mock_output in str(mock_pass) + +class TestResultFail(unittest.TestCase): + + def test_Create_ValidArgs_CorrectAttributes(self): + mock_test_spec = mock.MagicMock() + mock_output = mock.MagicMock() + mock_editor_log = mock.MagicMock() + + mock_pass = editor_test.Result.Fail.create(mock_test_spec, mock_output, mock_editor_log) + assert mock_pass.test_spec == mock_test_spec + assert mock_pass.output == mock_output + assert mock_pass.editor_log == mock_editor_log + + def test_Str_ValidString_ReturnsOutput(self): + mock_test_spec = mock.MagicMock() + mock_output = 'mock_output' + mock_editor_log = 'mock_editor_log' + + mock_pass = editor_test.Result.Fail.create(mock_test_spec, mock_output, mock_editor_log) + assert mock_output in str(mock_pass) + assert mock_editor_log in str(mock_pass) + +class TestResultCrash(unittest.TestCase): + + def test_Create_ValidArgs_CorrectAttributes(self): + mock_test_spec = mock.MagicMock() + mock_output = mock.MagicMock() + mock_editor_log = mock.MagicMock() + mock_ret_code = mock.MagicMock() + mock_stacktrace = mock.MagicMock() + + mock_pass = editor_test.Result.Crash.create(mock_test_spec, mock_output, mock_ret_code, mock_stacktrace, + mock_editor_log) + assert mock_pass.test_spec == mock_test_spec + assert mock_pass.output == mock_output + assert mock_pass.editor_log == mock_editor_log + assert mock_pass.ret_code == mock_ret_code + assert mock_pass.stacktrace == mock_stacktrace + + def test_Str_ValidString_ReturnsOutput(self): + mock_test_spec = mock.MagicMock() + mock_output = 'mock_output' + mock_editor_log = 'mock_editor_log' + mock_return_code = 0 + mock_stacktrace = 'mock stacktrace' + + mock_pass = editor_test.Result.Crash.create(mock_test_spec, mock_output, mock_return_code, mock_stacktrace, + mock_editor_log) + assert mock_stacktrace in str(mock_pass) + assert mock_output in str(mock_pass) + assert mock_editor_log in str(mock_pass) + + def test_Str_MissingStackTrace_ReturnsCorrectly(self): + mock_test_spec = mock.MagicMock() + mock_output = 'mock_output' + mock_editor_log = 'mock_editor_log' + mock_return_code = 0 + mock_stacktrace = None + mock_pass = editor_test.Result.Crash.create(mock_test_spec, mock_output, mock_return_code, mock_stacktrace, + mock_editor_log) + assert mock_output in str(mock_pass) + assert mock_editor_log in str(mock_pass) + +class TestResultTimeout(unittest.TestCase): + + def test_Create_ValidArgs_CorrectAttributes(self): + mock_test_spec = mock.MagicMock() + mock_output = mock.MagicMock() + mock_editor_log = mock.MagicMock() + mock_timeout = mock.MagicMock() + + mock_pass = editor_test.Result.Timeout.create(mock_test_spec, mock_output, mock_timeout, mock_editor_log) + assert mock_pass.test_spec == mock_test_spec + assert mock_pass.output == mock_output + assert mock_pass.editor_log == mock_editor_log + assert mock_pass.time_secs == mock_timeout + + def test_Str_ValidString_ReturnsOutput(self): + mock_test_spec = mock.MagicMock() + mock_output = 'mock_output' + mock_editor_log = 'mock_editor_log' + mock_timeout = 0 + + mock_pass = editor_test.Result.Timeout.create(mock_test_spec, mock_output, mock_timeout, mock_editor_log) + assert mock_output in str(mock_pass) + assert mock_editor_log in str(mock_pass) + +class TestResultUnknown(unittest.TestCase): + + def test_Create_ValidArgs_CorrectAttributes(self): + mock_test_spec = mock.MagicMock() + mock_output = mock.MagicMock() + mock_editor_log = mock.MagicMock() + mock_extra_info = mock.MagicMock() + + mock_pass = editor_test.Result.Unknown.create(mock_test_spec, mock_output, mock_extra_info, mock_editor_log) + assert mock_pass.test_spec == mock_test_spec + assert mock_pass.output == mock_output + assert mock_pass.editor_log == mock_editor_log + assert mock_pass.extra_info == mock_extra_info + + def test_Str_ValidString_ReturnsOutput(self): + mock_test_spec = mock.MagicMock() + mock_output = 'mock_output' + mock_editor_log = 'mock_editor_log' + mock_extra_info = 'mock extra info' + + mock_pass = editor_test.Result.Unknown.create(mock_test_spec, mock_output, mock_extra_info, mock_editor_log) + assert mock_output in str(mock_pass) + assert mock_editor_log in str(mock_pass) + +class TestEditorTestSuite(unittest.TestCase): + + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + def test_EditorTestData_ValidAP_TeardownAPOnce(self, mock_kill_processes): + mock_editor_test_suite = editor_test.EditorTestSuite() + mock_test_data_generator = mock_editor_test_suite._editor_test_data(mock.MagicMock()) + mock_asset_processor = mock.MagicMock() + for test_data in mock_test_data_generator: + test_data.asset_processor = mock_asset_processor + mock_asset_processor.stop.assert_called_once_with(1) + mock_asset_processor.teardown.assert_called() + assert test_data.asset_processor is None + mock_kill_processes.assert_called_once_with(include_asset_processor=True) + + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + def test_EditorTestData_NoAP_NoTeardownAP(self, mock_kill_processes): + mock_editor_test_suite = editor_test.EditorTestSuite() + mock_test_data_generator = mock_editor_test_suite._editor_test_data(mock.MagicMock()) + for test_data in mock_test_data_generator: + test_data.asset_processor = None + mock_kill_processes.assert_called_once_with(include_asset_processor=False) + + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite.filter_session_shared_tests') + def test_PytestCustomModifyItems_FunctionsMatch_AddsRunners(self, mock_filter_tests): + class MockTestSuite(editor_test.EditorTestSuite): + pass + mock_func_1 = mock.MagicMock() + mock_test = mock.MagicMock() + runner_1 = editor_test.EditorTestSuite.Runner('mock_runner_1', mock_func_1, [mock_test]) + mock_run_pytest_func = mock.MagicMock() + runner_1.run_pytestfunc = mock_run_pytest_func + mock_result_pytestfuncs = [mock.MagicMock()] + runner_1.result_pytestfuncs = mock_result_pytestfuncs + mock_items = [] + mock_items.extend(mock_result_pytestfuncs) + + MockTestSuite._runners = [runner_1] + mock_test_1 = mock.MagicMock() + mock_test_2 = mock.MagicMock() + mock_filter_tests.return_value = [mock_test_1, mock_test_2] + + MockTestSuite.pytest_custom_modify_items(mock.MagicMock(), mock_items, mock.MagicMock()) + assert mock_items == [mock_run_pytest_func, mock_result_pytestfuncs[0]] + + def test_GetSingleTests_NoSingleTests_EmptyList(self): + class MockTestSuite(editor_test.EditorTestSuite): + pass + mock_test_suite = MockTestSuite() + tests = mock_test_suite.get_single_tests() + assert len(tests) == 0 + + def test_GetSingleTests_OneSingleTests_ReturnsOne(self): + class MockTestSuite(editor_test.EditorTestSuite): + class MockSingleTest(editor_test.EditorSingleTest): + pass + mock_test_suite = MockTestSuite() + tests = mock_test_suite.get_single_tests() + assert len(tests) == 1 + assert tests[0].__name__ == "MockSingleTest" + assert issubclass(tests[0], editor_test.EditorSingleTest) + + def test_GetSingleTests_AllTests_ReturnsOnlySingles(self): + class MockTestSuite(editor_test.EditorTestSuite): + class MockSingleTest(editor_test.EditorSingleTest): + pass + class MockAnotherSingleTest(editor_test.EditorSingleTest): + pass + class MockNotSingleTest(editor_test.EditorSharedTest): + pass + mock_test_suite = MockTestSuite() + tests = mock_test_suite.get_single_tests() + assert len(tests) == 2 + for test in tests: + assert issubclass(test, editor_test.EditorSingleTest) + + def test_GetSharedTests_NoSharedTests_EmptyList(self): + class MockTestSuite(editor_test.EditorTestSuite): + pass + mock_test_suite = MockTestSuite() + tests = mock_test_suite.get_shared_tests() + assert len(tests) == 0 + + def test_GetSharedTests_OneSharedTests_ReturnsOne(self): + class MockTestSuite(editor_test.EditorTestSuite): + class MockSharedTest(editor_test.EditorSharedTest): + pass + mock_test_suite = MockTestSuite() + tests = mock_test_suite.get_shared_tests() + assert len(tests) == 1 + assert tests[0].__name__ == 'MockSharedTest' + assert issubclass(tests[0], editor_test.EditorSharedTest) + + def test_GetSharedTests_AllTests_ReturnsOnlyShared(self): + class MockTestSuite(editor_test.EditorTestSuite): + class MockSharedTest(editor_test.EditorSharedTest): + pass + class MockAnotherSharedTest(editor_test.EditorSharedTest): + pass + class MockNotSharedTest(editor_test.EditorSingleTest): + pass + mock_test_suite = MockTestSuite() + tests = mock_test_suite.get_shared_tests() + assert len(tests) == 2 + for test in tests: + assert issubclass(test, editor_test.EditorSharedTest) + + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite.filter_session_shared_tests') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite.get_shared_tests') + def test_GetSessionSharedTests_Valid_CallsCorrectly(self, mock_get_shared_tests, mock_filter_session): + editor_test.EditorTestSuite.get_session_shared_tests(mock.MagicMock()) + assert mock_get_shared_tests.called + assert mock_filter_session.called + + @mock.patch('ly_test_tools.o3de.editor_test.skipping_pytest_runtest_setup', mock.MagicMock()) + def test_FilterSessionSharedTests_OneSharedTest_ReturnsOne(self): + def mock_test(): + pass + mock_test.originalname = 'mock_test' + mock_test.__name__ = mock_test.originalname + mock_session_items = [mock_test] + mock_shared_tests = [mock_test] + + selected_tests = editor_test.EditorTestSuite.filter_session_shared_tests(mock_session_items, mock_shared_tests) + assert selected_tests == mock_session_items + assert len(selected_tests) == 1 + + @mock.patch('ly_test_tools.o3de.editor_test.skipping_pytest_runtest_setup', mock.MagicMock()) + def test_FilterSessionSharedTests_ManyTests_ReturnsCorrectTests(self): + def mock_test(): + pass + def mock_test_2(): + pass + def mock_test_3(): + pass + mock_test.originalname = 'mock_test' + mock_test.__name__ = mock_test.originalname + mock_test_2.originalname = 'mock_test_2' + mock_test_2.__name__ = mock_test_2.originalname + mock_test_3.originalname = 'mock_test_3' + mock_test_3.__name__ = mock_test_3.originalname + mock_session_items = [mock_test, mock_test_2] + mock_shared_tests = [mock_test, mock_test_2, mock_test_3] + + selected_tests = editor_test.EditorTestSuite.filter_session_shared_tests(mock_session_items, mock_shared_tests) + assert selected_tests == mock_session_items + + @mock.patch('ly_test_tools.o3de.editor_test.skipping_pytest_runtest_setup') + def test_FilterSessionSharedTests_SkipOneTest_ReturnsCorrectTests(self, mock_skip): + def mock_test(): + pass + def mock_test_2(): + pass + def mock_test_3(): + pass + mock_skip.side_effect = [True, Exception] + mock_test.originalname = 'mock_test' + mock_test.__name__ = mock_test.originalname + mock_test_2.originalname = 'mock_test_2' + mock_test_2.__name__ = mock_test_2.originalname + mock_test_3.originalname = 'mock_test_3' + mock_test_3.__name__ = mock_test_3.originalname + mock_session_items = [mock_test, mock_test_2] + mock_shared_tests = [mock_test, mock_test_2, mock_test_3] + + selected_tests = editor_test.EditorTestSuite.filter_session_shared_tests(mock_session_items, mock_shared_tests) + assert selected_tests == [mock_test] + + @mock.patch('ly_test_tools.o3de.editor_test.skipping_pytest_runtest_setup', mock.MagicMock(side_effect=Exception)) + def test_FilterSessionSharedTests_ExceptionDuringSkipSetup_SkipsAddingTest(self): + def mock_test(): + pass + mock_test.originalname = 'mock_test' + mock_test.__name__ = mock_test.originalname + mock_session_items = [mock_test] + mock_shared_tests = [mock_test] + + selected_tests = editor_test.EditorTestSuite.filter_session_shared_tests(mock_session_items, mock_shared_tests) + assert len(selected_tests) == 0 + + def test_FilterSharedTests_TrueParams_ReturnsTrueTests(self): + mock_test = mock.MagicMock() + mock_test.is_batchable = True + mock_test.is_parallelizable = True + mock_test_2 = mock.MagicMock() + mock_test_2.is_batchable = False + mock_test_2.is_parallelizable = False + mock_shared_tests = [mock_test, mock_test_2] + + filtered_tests = editor_test.EditorTestSuite.filter_shared_tests( + mock_shared_tests, is_batchable=True, is_parallelizable=True) + assert filtered_tests == [mock_test] + + def test_FilterSharedTests_FalseParams_ReturnsFalseTests(self): + mock_test = mock.MagicMock() + mock_test.is_batchable = True + mock_test.is_parallelizable = True + mock_test_2 = mock.MagicMock() + mock_test_2.is_batchable = False + mock_test_2.is_parallelizable = False + mock_shared_tests = [mock_test, mock_test_2] + + filtered_tests = editor_test.EditorTestSuite.filter_shared_tests( + mock_shared_tests, is_batchable=False, is_parallelizable=False) + assert filtered_tests == [mock_test_2] + +class TestUtils(unittest.TestCase): + + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + def test_PrepareAssetProcessor_APExists_StartsAP(self, mock_kill_processes): + mock_test_suite = editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor_data = mock.MagicMock() + mock_ap = mock.MagicMock() + mock_editor_data.asset_processor = mock_ap + + mock_test_suite._prepare_asset_processor(mock_workspace, mock_editor_data) + assert mock_ap.start.called + assert not mock_kill_processes.called + + @mock.patch('ly_test_tools.o3de.asset_processor.AssetProcessor.start') + @mock.patch('ly_test_tools.environment.process_utils.process_exists') + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + def test_PrepareAssetProcessor_NoAP_KillAndCreateAP(self, mock_kill_processes, mock_proc_exists, mock_start): + mock_test_suite = editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor_data = mock.MagicMock() + mock_editor_data.asset_processor = None + mock_proc_exists.return_value = False + + mock_test_suite._prepare_asset_processor(mock_workspace, mock_editor_data) + mock_kill_processes.assert_called_with(include_asset_processor=True) + assert isinstance(mock_editor_data.asset_processor, ly_test_tools.o3de.asset_processor.AssetProcessor) + assert mock_start.called + + @mock.patch('ly_test_tools.o3de.asset_processor.AssetProcessor.start') + @mock.patch('ly_test_tools.environment.process_utils.process_exists') + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + def test_PrepareAssetProcessor_NoAPButProcExists_NoKill(self, mock_kill_processes, mock_proc_exists, mock_start): + mock_test_suite = editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor_data = mock.MagicMock() + mock_editor_data.asset_processor = None + mock_proc_exists.return_value = True + + mock_test_suite._prepare_asset_processor(mock_workspace, mock_editor_data) + mock_kill_processes.assert_called_with(include_asset_processor=False) + assert not mock_start.called + assert mock_editor_data.asset_processor is None + + + @mock.patch('ly_test_tools.o3de.asset_processor.AssetProcessor.start') + @mock.patch('ly_test_tools.environment.process_utils.process_exists') + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + def test_PrepareAssetProcessor_NoAPButProcExists_NoKill(self, mock_kill_processes, mock_proc_exists, mock_start): + mock_test_suite = editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor_data = mock.MagicMock() + mock_editor_data.asset_processor = None + mock_proc_exists.return_value = True + + mock_test_suite._prepare_asset_processor(mock_workspace, mock_editor_data) + mock_kill_processes.assert_called_with(include_asset_processor=False) + assert not mock_start.called + assert mock_editor_data.asset_processor is None + + @mock.patch('ly_test_tools.o3de.editor_test_utils.kill_all_ly_processes') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._prepare_asset_processor') + def test_SetupEditorTest_ValidArgs_CallsCorrectly(self, mock_prepare_ap, mock_kill_processes): + mock_test_suite = editor_test.EditorTestSuite() + mock_editor = mock.MagicMock() + mock_test_suite._setup_editor_test(mock_editor, mock.MagicMock(), mock.MagicMock()) + + assert mock_editor.configure_settings.called + assert mock_prepare_ap.called + mock_kill_processes.assert_called_once_with(include_asset_processor=False) + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Pass.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_module_filename') + def test_GetResultsUsingOutput_ValidJsonSuccess_CreatesPassResult(self, mock_get_module, mock_create): + mock_get_module.return_value = 'mock_module_name' + mock_test_suite = editor_test.EditorTestSuite() + mock_test = mock.MagicMock() + mock_test.__name__ = 'mock_test_name' + mock_test_list = [mock_test] + mock_output = 'JSON_START(' \ + '{"name": "mock_module_name", "output": "mock_std_out", "success": "mock_success_data"}' \ + ')JSON_END' + mock_pass = mock.MagicMock() + mock_create.return_value = mock_pass + + results = mock_test_suite._get_results_using_output(mock_test_list, mock_output, '') + assert mock_create.called + assert len(results) == 1 + assert results[mock_test.__name__] == mock_pass + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Fail.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_module_filename') + def test_GetResultsUsingOutput_ValidJsonFail_CreatesFailResult(self, mock_get_module, mock_create): + mock_get_module.return_value = 'mock_module_name' + mock_test_suite = editor_test.EditorTestSuite() + mock_test = mock.MagicMock() + mock_test.__name__ = 'mock_test_name' + mock_test_list = [mock_test] + mock_output = 'JSON_START(' \ + '{"name": "mock_module_name", "output": "mock_std_out", "failed": "mock_fail_data", "success": ""}' \ + ')JSON_END' + mock_fail = mock.MagicMock() + mock_create.return_value = mock_fail + + results = mock_test_suite._get_results_using_output(mock_test_list, mock_output, '') + assert mock_create.called + assert len(results) == 1 + assert results[mock_test.__name__] == mock_fail + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Unknown.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_module_filename') + def test_GetResultsUsingOutput_ModuleNotInLog_CreatesUnknownResult(self, mock_get_module, mock_create): + mock_get_module.return_value = 'different_module_name' + mock_test_suite = editor_test.EditorTestSuite() + mock_test = mock.MagicMock() + mock_test.__name__ = 'mock_test_name' + mock_test_list = [mock_test] + mock_output = 'JSON_START(' \ + '{"name": "mock_module_name", "output": "mock_std_out", "failed": "mock_fail_data"}' \ + ')JSON_END' + mock_unknown = mock.MagicMock() + mock_create.return_value = mock_unknown + + results = mock_test_suite._get_results_using_output(mock_test_list, mock_output, '') + assert mock_create.called + assert len(results) == 1 + assert results[mock_test.__name__] == mock_unknown + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Pass.create') + @mock.patch('ly_test_tools.o3de.editor_test.Result.Fail.create') + @mock.patch('ly_test_tools.o3de.editor_test.Result.Unknown.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_module_filename') + def test_GetResultsUsingOutput_MultipleTests_CreatesCorrectResults(self, mock_get_module, mock_create_unknown, + mock_create_fail, mock_create_pass): + mock_get_module.side_effect = ['mock_module_name_pass', 'mock_module_name_fail', 'different_module_name'] + mock_test_suite = editor_test.EditorTestSuite() + mock_test_pass = mock.MagicMock() + mock_test_pass.__name__ = 'mock_test_name_pass' + mock_test_fail = mock.MagicMock() + mock_test_fail.__name__ = 'mock_test_name_fail' + mock_test_unknown = mock.MagicMock() + mock_test_unknown.__name__ = 'mock_test_name_unknown' + mock_test_list = [mock_test_pass, mock_test_fail, mock_test_unknown] + mock_output = 'JSON_START(' \ + '{"name": "mock_module_name_pass", "output": "mock_std_out", "success": "mock_success_data"}' \ + ')JSON_END' \ + 'JSON_START(' \ + '{"name": "mock_module_name_fail", "output": "mock_std_out", "failed": "mock_fail_data", "success": ""}' \ + ')JSON_END' \ + 'JSON_START(' \ + '{"name": "mock_module_name_unknown", "output": "mock_std_out", "failed": "mock_fail_data", "success": ""}' \ + ')JSON_END' + mock_editor_log = 'JSON_START(' \ + '{"name": "mock_module_name_pass"}' \ + ')JSON_END' \ + 'JSON_START(' \ + '{"name": "mock_module_name_fail"}' \ + ')JSON_END' + mock_unknown = mock.MagicMock() + mock_pass = mock.MagicMock() + mock_fail = mock.MagicMock() + mock_create_unknown.return_value = mock_unknown + mock_create_pass.return_value = mock_pass + mock_create_fail.return_value = mock_fail + + results = mock_test_suite._get_results_using_output(mock_test_list, mock_output, mock_editor_log) + mock_create_pass.assert_called_with( + mock_test_pass, 'mock_std_out', 'JSON_START({"name": "mock_module_name_pass"})JSON_END') + mock_create_fail.assert_called_with( + mock_test_fail, 'mock_std_out', 'JSON_START({"name": "mock_module_name_fail"})JSON_END') + mock_create_unknown.assert_called_with( + mock_test_unknown, mock_output, "Couldn't find any test run information on stdout", mock_editor_log) + assert len(results) == 3 + assert results[mock_test_pass.__name__] == mock_pass + assert results[mock_test_fail.__name__] == mock_fail + assert results[mock_test_unknown.__name__] == mock_unknown + + @mock.patch('builtins.print') + def test_ReportResult_TestPassed_ReportsCorrectly(self, mock_print): + mock_test_name = 'mock name' + mock_pass = ly_test_tools.o3de.editor_test.Result.Pass() + ly_test_tools.o3de.editor_test.EditorTestSuite._report_result(mock_test_name, mock_pass) + mock_print.assert_called_with(f'Test {mock_test_name}:\nTest Passed\n------------\n| Output |\n------------\n' + f'-- No output --\n') + + @mock.patch('pytest.fail') + def test_ReportResult_TestFailed_FailsCorrectly(self, mock_pytest_fail): + mock_fail = ly_test_tools.o3de.editor_test.Result.Fail() + + ly_test_tools.o3de.editor_test.EditorTestSuite._report_result('mock_test_name', mock_fail) + mock_pytest_fail.assert_called_with('Test mock_test_name:\nTest FAILED\n------------\n| Output |' + '\n------------\n-- No output --\n--------------\n| Editor log |' + '\n--------------\n-- No editor log found --\n') + +class TestRunningTests(unittest.TestCase): + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Pass.create') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorTest_TestSucceeds_ReturnsPass(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_output_results, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_editor.get_returncode.return_value = 0 + mock_get_output_results.return_value = {} + mock_pass = mock.MagicMock() + mock_create.return_value = mock_pass + + results = mock_test_suite._exec_editor_test(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec, []) + assert mock_cycle_crash.called + assert mock_editor.start.called + assert mock_create.called + assert results == {mock_test_spec.__name__: mock_pass} + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Fail.create') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorTest_TestFails_ReturnsFail(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_output_results, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_editor.get_returncode.return_value = 15 + mock_get_output_results.return_value = {} + mock_fail = mock.MagicMock() + mock_create.return_value = mock_fail + + results = mock_test_suite._exec_editor_test(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec, []) + assert mock_cycle_crash.called + assert mock_editor.start.called + assert mock_create.called + assert results == {mock_test_spec.__name__: mock_fail} + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Crash.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_crash_output') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorTest_TestCrashes_ReturnsCrash(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_output_results, mock_retrieve_crash, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_editor.get_returncode.return_value = 1 + mock_get_output_results.return_value = {} + mock_crash = mock.MagicMock() + mock_create.return_value = mock_crash + + results = mock_test_suite._exec_editor_test(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec, []) + assert mock_cycle_crash.call_count == 2 + assert mock_editor.start.called + assert mock_retrieve_crash.called + assert mock_create.called + assert results == {mock_test_spec.__name__: mock_crash} + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Timeout.create') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorTest_TestTimeout_ReturnsTimeout(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_output_results, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_editor.wait.side_effect = ly_test_tools.launchers.exceptions.WaitTimeoutError() + mock_get_output_results.return_value = {} + mock_timeout = mock.MagicMock() + mock_create.return_value = mock_timeout + + results = mock_test_suite._exec_editor_test(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec, []) + assert mock_cycle_crash.called + assert mock_editor.start.called + assert mock_editor.kill.called + assert mock_create.called + assert results == {mock_test_spec.__name__: mock_timeout} + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Pass.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorMultitest_AllTestsPass_ReturnsPasses(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_editor.get_returncode.return_value = 0 + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_test_spec_2 = mock.MagicMock() + mock_test_spec_2.__name__ = 'mock_test_name_2' + mock_test_spec_list = [mock_test_spec, mock_test_spec_2] + mock_get_testcase_filepath.side_effect = ['mock_path', 'mock_path_2'] + mock_pass = mock.MagicMock() + mock_pass_2 = mock.MagicMock() + mock_create.side_effect = [mock_pass, mock_pass_2] + + results = mock_test_suite._exec_editor_multitest(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec_list, []) + assert results == {mock_test_spec.__name__: mock_pass, mock_test_spec_2.__name__: mock_pass_2} + assert mock_cycle_crash.called + assert mock_create.call_count == 2 + + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorMultitest_OneFailure_CallsCorrectFunc(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, mock_get_results): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_editor.get_returncode.return_value = 15 + mock_test_spec = mock.MagicMock() + mock_test_spec_2 = mock.MagicMock() + mock_test_spec_list = [mock_test_spec, mock_test_spec_2] + mock_get_testcase_filepath.side_effect = ['mock_path', 'mock_path_2'] + mock_get_results.return_value = {'mock_test_name': mock.MagicMock(), 'mock_test_name_2': mock.MagicMock()} + + results = mock_test_suite._exec_editor_multitest(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec_list, []) + assert mock_cycle_crash.called + assert mock_get_results.called + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Crash.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_crash_output') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorMultitest_OneCrash_ReportsOnUnknownResult(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_results, mock_retrieve_crash, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_editor.get_returncode.return_value = 1 + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_test_spec_2 = mock.MagicMock() + mock_test_spec_2.__name__ = 'mock_test_name_2' + mock_test_spec_list = [mock_test_spec, mock_test_spec_2] + mock_unknown_result = ly_test_tools.o3de.editor_test.Result.Unknown() + mock_unknown_result.test_spec = mock.MagicMock() + mock_unknown_result.editor_log = mock.MagicMock() + mock_get_testcase_filepath.side_effect = ['mock_path', 'mock_path_2'] + mock_get_results.return_value = {mock_test_spec.__name__: mock_unknown_result, + mock_test_spec_2.__name__: mock.MagicMock()} + mock_crash = mock.MagicMock() + mock_create.return_value = mock_crash + + results = mock_test_suite._exec_editor_multitest(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec_list, []) + assert mock_cycle_crash.call_count == 2 + assert mock_get_results.called + assert results[mock_test_spec.__name__] == mock_crash + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Crash.create') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_crash_output') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorMultitest_ManyUnknown_ReportsUnknownResults(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_results, mock_retrieve_crash, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_editor.get_returncode.return_value = 1 + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_test_spec_2 = mock.MagicMock() + mock_test_spec_2.__name__ = 'mock_test_name_2' + mock_test_spec_list = [mock_test_spec, mock_test_spec_2] + mock_unknown_result = ly_test_tools.o3de.editor_test.Result.Unknown() + mock_unknown_result.__name__ = 'mock_test_name' + mock_unknown_result.test_spec = mock.MagicMock() + mock_unknown_result.test_spec.__name__ = 'mock_test_spec_name' + mock_unknown_result.editor_log = mock.MagicMock() + mock_get_testcase_filepath.side_effect = ['mock_path', 'mock_path_2'] + mock_get_results.return_value = {mock_test_spec.__name__: mock_unknown_result, + mock_test_spec_2.__name__: mock_unknown_result} + mock_crash = mock.MagicMock() + mock_create.return_value = mock_crash + + results = mock_test_suite._exec_editor_multitest(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec_list, []) + assert mock_cycle_crash.call_count == 2 + assert mock_get_results.called + assert results[mock_test_spec.__name__] == mock_crash + assert results[mock_test_spec_2.__name__].extra_info + + @mock.patch('ly_test_tools.o3de.editor_test.Result.Timeout.create') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_results_using_output') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_editor_log_content') + @mock.patch('ly_test_tools.o3de.editor_test_utils.retrieve_log_path') + @mock.patch('ly_test_tools.o3de.editor_test_utils.get_testcase_module_filepath') + @mock.patch('ly_test_tools.o3de.editor_test_utils.cycle_crash_report') + def test_ExecEditorMultitest_EditorTimeout_ReportsCorrectly(self, mock_cycle_crash, mock_get_testcase_filepath, + mock_retrieve_log, mock_retrieve_editor_log, + mock_get_results, mock_create): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_workspace = mock.MagicMock() + mock_editor = mock.MagicMock() + mock_editor.wait.side_effect = ly_test_tools.launchers.exceptions.WaitTimeoutError() + mock_test_spec = mock.MagicMock() + mock_test_spec.__name__ = 'mock_test_name' + mock_test_spec_2 = mock.MagicMock() + mock_test_spec_2.__name__ = 'mock_test_name_2' + mock_test_spec_list = [mock_test_spec, mock_test_spec_2] + mock_unknown_result = ly_test_tools.o3de.editor_test.Result.Unknown() + mock_unknown_result.test_spec = mock.MagicMock() + mock_unknown_result.test_spec.__name__ = 'mock_test_spec_name' + mock_unknown_result.output = mock.MagicMock() + mock_unknown_result.editor_log = mock.MagicMock() + mock_get_testcase_filepath.side_effect = ['mock_path', 'mock_path_2'] + mock_get_results.return_value = {mock_test_spec.__name__: mock_unknown_result, + mock_test_spec_2.__name__: mock_unknown_result} + mock_timeout = mock.MagicMock() + mock_create.return_value = mock_timeout + + results = mock_test_suite._exec_editor_multitest(mock.MagicMock(), mock_workspace, mock_editor, 0, + 'mock_log_name', mock_test_spec_list, []) + assert mock_cycle_crash.called + assert mock_get_results.called + assert results[mock_test_spec_2.__name__].extra_info + assert results[mock_test_spec.__name__] == mock_timeout + + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._report_result') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._exec_editor_test') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunSingleTest_ValidTest_ReportsResults(self, mock_setup_test, mock_exec_editor_test, mock_report_result): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_test_data = mock.MagicMock() + mock_test_spec = mock.MagicMock() + mock_result = mock.MagicMock() + mock_test_name = 'mock_test_result' + mock_exec_editor_test.return_value = {mock_test_name: mock_result} + + mock_test_suite._run_single_test(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), mock_test_data, + mock_test_spec) + + assert mock_setup_test.called + assert mock_exec_editor_test.called + assert mock_test_data.results.update.called + mock_report_result.assert_called_with(mock_test_name, mock_result) + + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._exec_editor_multitest') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunBatchedTests_ValidTests_CallsCorrectly(self, mock_setup_test, mock_exec_multitest): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_test_data = mock.MagicMock() + + mock_test_suite._run_batched_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), mock_test_data, + mock.MagicMock(), []) + + assert mock_setup_test.called + assert mock_exec_multitest.called + assert mock_test_data.results.update.called + + @mock.patch('threading.Thread') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_number_parallel_editors') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunParallelTests_TwoTestsAndEditors_TwoThreads(self, mock_setup_test, mock_get_num_editors, mock_thread): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_get_num_editors.return_value = 2 + mock_test_spec_list = [mock.MagicMock(), mock.MagicMock()] + mock_test_data = mock.MagicMock() + + mock_test_suite._run_parallel_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), mock_test_data, + mock_test_spec_list, []) + + assert mock_setup_test.called + assert mock_test_data.results.update.call_count == len(mock_test_spec_list) + assert mock_thread.call_count == len(mock_test_spec_list) + + @mock.patch('threading.Thread') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_number_parallel_editors') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunParallelTests_TenTestsAndTwoEditors_TenThreads(self, mock_setup_test, mock_get_num_editors, mock_thread): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_get_num_editors.return_value = 2 + mock_test_spec_list = [] + for i in range(10): + mock_test_spec_list.append(mock.MagicMock()) + mock_test_data = mock.MagicMock() + + mock_test_suite._run_parallel_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), mock_test_data, + mock_test_spec_list, []) + + assert mock_setup_test.called + assert mock_test_data.results.update.call_count == len(mock_test_spec_list) + assert mock_thread.call_count == len(mock_test_spec_list) + + @mock.patch('threading.Thread') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_number_parallel_editors') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunParallelTests_TenTestsAndThreeEditors_TenThreads(self, mock_setup_test, mock_get_num_editors, + mock_thread): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_get_num_editors.return_value = 3 + mock_test_spec_list = [] + for i in range(10): + mock_test_spec_list.append(mock.MagicMock()) + mock_test_data = mock.MagicMock() + + mock_test_suite._run_parallel_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), mock_test_data, + mock_test_spec_list, []) + + assert mock_setup_test.called + assert mock_test_data.results.update.call_count == len(mock_test_spec_list) + assert mock_thread.call_count == len(mock_test_spec_list) + + @mock.patch('threading.Thread') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_number_parallel_editors') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunParallelBatchedTests_TwoTestsAndEditors_TwoThreads(self, mock_setup_test, mock_get_num_editors, + mock_thread): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_get_num_editors.return_value = 2 + mock_test_spec_list = [mock.MagicMock(), mock.MagicMock()] + mock_test_data = mock.MagicMock() + + mock_test_suite._run_parallel_batched_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), + mock_test_data, mock_test_spec_list, []) + + assert mock_setup_test.called + assert mock_test_data.results.update.call_count == len(mock_test_spec_list) + assert mock_thread.call_count == len(mock_test_spec_list) + + @mock.patch('threading.Thread') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_number_parallel_editors') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunParallelBatchedTests_TenTestsAndTwoEditors_2Threads(self, mock_setup_test, mock_get_num_editors, + mock_thread): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_get_num_editors.return_value = 2 + mock_test_spec_list = [] + for i in range(10): + mock_test_spec_list.append(mock.MagicMock()) + mock_test_data = mock.MagicMock() + + mock_test_suite._run_parallel_batched_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), + mock_test_data, mock_test_spec_list, []) + + assert mock_setup_test.called + assert mock_test_data.results.update.call_count == 2 + assert mock_thread.call_count == 2 + + @mock.patch('threading.Thread') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._get_number_parallel_editors') + @mock.patch('ly_test_tools.o3de.editor_test.EditorTestSuite._setup_editor_test') + def test_RunParallelBatchedTests_TenTestsAndThreeEditors_ThreeThreads(self, mock_setup_test, mock_get_num_editors, + mock_thread): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_get_num_editors.return_value = 3 + mock_test_spec_list = [] + for i in range(10): + mock_test_spec_list.append(mock.MagicMock()) + mock_test_data = mock.MagicMock() + + mock_test_suite._run_parallel_batched_tests(mock.MagicMock(), mock.MagicMock(), mock.MagicMock(), + mock_test_data, mock_test_spec_list, []) + + assert mock_setup_test.called + assert mock_test_data.results.update.call_count == 3 + assert mock_thread.call_count == 3 + + def test_GetNumberParallelEditors_ConfigExists_ReturnsConfig(self): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_request = mock.MagicMock() + mock_request.config.getoption.return_value = 1 + + num_of_editors = mock_test_suite._get_number_parallel_editors(mock_request) + assert num_of_editors == 1 + + def test_GetNumberParallelEditors_ConfigNotExists_ReturnsDefault(self): + mock_test_suite = ly_test_tools.o3de.editor_test.EditorTestSuite() + mock_request = mock.MagicMock() + mock_request.config.getoption.return_value = None + + num_of_editors = mock_test_suite._get_number_parallel_editors(mock_request) + assert num_of_editors == mock_test_suite.get_number_parallel_editors() + +@mock.patch('_pytest.python.Class.collect') +class TestEditorTestClass(unittest.TestCase): + + def setUp(self): + mock_name = mock.MagicMock() + mock_collector = mock.MagicMock() + self.mock_test_class = ly_test_tools.o3de.editor_test.EditorTestSuite.EditorTestClass(mock_name, mock_collector) + self.mock_test_class.obj = mock.MagicMock() + single_1 = mock.MagicMock() + single_1.__name__ = 'single_1_name' + single_2 = mock.MagicMock() + single_2.__name__ = 'single_2_name' + self.mock_test_class.obj.get_single_tests.return_value = [single_1, single_2] + batch_1 = mock.MagicMock() + batch_1.__name__ = 'batch_1_name' + batch_2 = mock.MagicMock() + batch_2.__name__ = 'batch_2_name' + parallel_1 = mock.MagicMock() + parallel_1.__name__ = 'parallel_1_name' + parallel_2 = mock.MagicMock() + parallel_2.__name__ = 'parallel_2_name' + both_1 = mock.MagicMock() + both_1.__name__ = 'both_1_name' + both_2 = mock.MagicMock() + both_2.__name__ = 'both_2_name' + self.mock_test_class.obj.filter_shared_tests.side_effect = [ [batch_1, batch_2], + [parallel_1, parallel_2], + [both_1, both_2] ] + + def test_Collect_NoParallelNoBatched_RunsAsSingleTests(self, mock_collect): + self.mock_test_class.config.getoption.return_value = True + self.mock_test_class.collect() + assert self.mock_test_class.obj.single_1_name.__name__ == 'single_run' + assert self.mock_test_class.obj.single_2_name.__name__ == 'single_run' + assert self.mock_test_class.obj.batch_1_name.__name__ == 'single_run' + assert self.mock_test_class.obj.batch_2_name.__name__ == 'single_run' + assert self.mock_test_class.obj.parallel_1_name.__name__ == 'single_run' + assert self.mock_test_class.obj.parallel_2_name.__name__ == 'single_run' + assert self.mock_test_class.obj.both_1_name.__name__ == 'single_run' + assert self.mock_test_class.obj.both_2_name.__name__ == 'single_run' + + def test_Collect_AllValidTests_RunsAsInteded(self, mock_collect): + self.mock_test_class.config.getoption.return_value = False + self.mock_test_class.collect() + assert self.mock_test_class.obj.single_1_name.__name__ == 'single_run' + assert self.mock_test_class.obj.single_2_name.__name__ == 'single_run' + assert self.mock_test_class.obj.batch_1_name.__name__ == 'result' + assert self.mock_test_class.obj.batch_2_name.__name__ == 'result' + assert self.mock_test_class.obj.parallel_1_name.__name__ == 'result' + assert self.mock_test_class.obj.parallel_2_name.__name__ == 'result' + assert self.mock_test_class.obj.both_1_name.__name__ == 'result' + assert self.mock_test_class.obj.both_2_name.__name__ == 'result' + + def test_Collect_AllValidTests_CallsCollect(self, mock_collect): + self.mock_test_class.collect() + assert mock_collect.called + + def test_Collect_NormalCollection_ReturnsFilteredRuns(self, mock_collect): + mock_run = mock.MagicMock() + mock_run.obj.marks = {"run_type": 'run_shared'} + mock_run_2 = mock.MagicMock() + mock_run_2.obj.marks = {"run_type": 'result'} + mock_instance = mock.MagicMock() + mock_instance.collect.return_value = [mock_run, mock_run_2] + mock_collect.return_value = [mock_instance] + + collection = self.mock_test_class.collect() + assert collection == [mock_run_2] + + def test_Collect_NormalRun_ReturnsRunners(self, mock_collect): + self.mock_test_class.collect() + runners = self.mock_test_class.obj._runners + + assert runners[0].name == 'run_batched_tests' + assert runners[1].name == 'run_parallel_tests' + assert runners[2].name == 'run_parallel_batched_tests' + + def test_Collect_NormalCollection_StoresRunners(self, mock_collect): + mock_runner = mock.MagicMock() + mock_run = mock.MagicMock() + mock_run.obj.marks = {"run_type": 'run_shared'} + mock_run.function.marks = {"runner": mock_runner} + mock_runner_2 = mock.MagicMock() + mock_runner_2.result_pytestfuncs = [] + mock_run_2 = mock.MagicMock() + mock_run_2.obj.marks = {"run_type": 'result'} + mock_run_2.function.marks = {"runner": mock_runner_2} + mock_instance = mock.MagicMock() + mock_instance.collect.return_value = [mock_run, mock_run_2] + mock_collect.return_value = [mock_instance] + + self.mock_test_class.collect() + + assert mock_runner.run_pytestfunc == mock_run + assert mock_run_2 in mock_runner_2.result_pytestfuncs diff --git a/Tools/LyTestTools/tests/unit/test_pytest_plugin_editor_test.py b/Tools/LyTestTools/tests/unit/test_pytest_plugin_editor_test.py new file mode 100644 index 0000000000..10c779e546 --- /dev/null +++ b/Tools/LyTestTools/tests/unit/test_pytest_plugin_editor_test.py @@ -0,0 +1,41 @@ +""" +Copyright (c) Contributors to the Open 3D Engine Project. +For complete copyright and license terms please see the LICENSE at the root of this distribution. + +SPDX-License-Identifier: Apache-2.0 OR MIT +""" +import pytest +import os +import unittest.mock as mock +import unittest + +import ly_test_tools._internal.pytest_plugin.editor_test as editor_test + +pytestmark = pytest.mark.SUITE_smoke + +class TestEditorTest(unittest.TestCase): + + @mock.patch('inspect.isclass', mock.MagicMock(return_value=True)) + def test_PytestPycollectMakeitem_ValidArgs_CallsCorrectly(self): + mock_collector = mock.MagicMock() + mock_name = mock.MagicMock() + mock_obj = mock.MagicMock() + mock_base = mock.MagicMock() + mock_obj.__bases__ = [mock_base] + + editor_test.pytest_pycollect_makeitem(mock_collector, mock_name, mock_obj) + mock_base.pytest_custom_makeitem.assert_called_once_with(mock_collector, mock_name, mock_obj) + + def test_PytestCollectionModifyitem_OneValidClass_CallsOnce(self): + mock_item = mock.MagicMock() + mock_class = mock.MagicMock() + mock_class.pytest_custom_modify_items = mock.MagicMock() + mock_item.instance.__class__ = mock_class + mock_session = mock.MagicMock() + mock_items = [mock_item, mock.MagicMock()] + mock_config = mock.MagicMock() + + generator = editor_test.pytest_collection_modifyitems(mock_session, mock_items, mock_config) + for x in generator: + pass + assert mock_class.pytest_custom_modify_items.call_count == 1 diff --git a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake index 6210b9cc18..af7afff5dc 100644 --- a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake +++ b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake @@ -33,7 +33,7 @@ ly_associate_package(PACKAGE_NAME mikkelsen-1.0.0.4-linux ly_associate_package(PACKAGE_NAME googletest-1.8.1-rev4-linux TARGETS googletest PACKAGE_HASH 7b7ad330f369450c316a4c4592d17fbb4c14c731c95bd8f37757203e8c2bbc1b) ly_associate_package(PACKAGE_NAME googlebenchmark-1.5.0-rev2-linux TARGETS GoogleBenchmark PACKAGE_HASH 4038878f337fc7e0274f0230f71851b385b2e0327c495fc3dd3d1c18a807928d) ly_associate_package(PACKAGE_NAME unwind-1.2.1-linux TARGETS unwind PACKAGE_HASH 3453265fb056e25432f611a61546a25f60388e315515ad39007b5925dd054a77) -ly_associate_package(PACKAGE_NAME qt-5.15.2-rev5-linux TARGETS Qt PACKAGE_HASH 76b395897b941a173002845c7219a5f8a799e44b269ffefe8091acc048130f28) +ly_associate_package(PACKAGE_NAME qt-5.15.2-rev6-linux TARGETS Qt PACKAGE_HASH a37bd9989f1e8fe57d94b98cbf9bd5c3caaea740e2f314e5162fa77300551531) ly_associate_package(PACKAGE_NAME libpng-1.6.37-rev1-linux TARGETS libpng PACKAGE_HASH 896451999f1de76375599aec4b34ae0573d8d34620d9ab29cc30b8739c265ba6) ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-linux TARGETS libsamplerate PACKAGE_HASH 41643c31bc6b7d037f895f89d8d8d6369e906b92eff42b0fe05ee6a100f06261) ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev2-linux TARGETS OpenSSL PACKAGE_HASH b779426d1e9c5ddf71160d5ae2e639c3b956e0fb5e9fcaf9ce97c4526024e3bc) diff --git a/cmake/Install.cmake b/cmake/Install.cmake index 3b74e6c654..b558590b9e 100644 --- a/cmake/Install.cmake +++ b/cmake/Install.cmake @@ -8,16 +8,38 @@ set(LY_INSTALL_ENABLED TRUE CACHE BOOL "Indicates if the install process is enabled") -if(LY_INSTALL_ENABLED) - ly_get_absolute_pal_filename(pal_dir ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Platform/${PAL_PLATFORM_NAME}) - include(${pal_dir}/Install_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) -endif() +#! ly_install: wrapper to install that handles common functionality +# +# \notes: +# - this wrapper handles the case where common installs are called multiple times from different +# build folders (when using LY_INSTALL_EXTERNAL_BUILD_DIRS) to generate install layouts that +# have multiple build permutations +# +function(ly_install) + + if(NOT LY_INSTALL_ENABLED) + return() + endif() + + cmake_parse_arguments(ly_install "" "COMPONENT" "" ${ARGN}) + if (NOT ly_install_COMPONENT OR "${ly_install_COMPONENT}" STREQUAL "${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}") + # if it is installing under the default component, we need to de-duplicate since we can have + # cases coming from different build directories (when using LY_INSTALL_EXTERNAL_BUILD_DIRS) + install(CODE "if(NOT LY_CORE_COMPONENT_ALREADY_INCLUDED)" ALL_COMPONENTS) + install(${ARGN}) + install(CODE "endif()\n" ALL_COMPONENTS) + else() + install(${ARGN}) + endif() + +endfunction() #! ly_install_directory: specifies a directory to be copied to the install layout at install time # # \arg:DIRECTORIES directories to install # \arg:DESTINATION (optional) destination to install the directory to (relative to CMAKE_PREFIX_PATH) # \arg:EXCLUDE_PATTERNS (optional) patterns to exclude +# \arg:COMPONENT (optional) component to use (defaults to CMAKE_INSTALL_DEFAULT_COMPONENT_NAME) # \arg:VERBATIM (optional) copies the directories as they are, this excludes the default exclude patterns # # \notes: @@ -34,7 +56,7 @@ function(ly_install_directory) endif() set(options VERBATIM) - set(oneValueArgs DESTINATION) + set(oneValueArgs DESTINATION COMPONENT) set(multiValueArgs DIRECTORIES EXCLUDE_PATTERNS) cmake_parse_arguments(ly_install_directory "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) @@ -42,6 +64,10 @@ function(ly_install_directory) if(NOT ly_install_directory_DIRECTORIES) message(FATAL_ERROR "You must provide at least a directory to install") endif() + + if(NOT ly_install_directory_COMPONENT) + set(ly_install_directory_COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) + endif() foreach(directory ${ly_install_directory_DIRECTORIES}) @@ -77,11 +103,12 @@ function(ly_install_directory) list(APPEND exclude_patterns PATTERN *.egg-info EXCLUDE) endif() - install(DIRECTORY ${directory} + ly_install(DIRECTORY ${directory} DESTINATION ${ly_install_directory_DESTINATION} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} # use the deafult for the time being + COMPONENT ${ly_install_directory_COMPONENT} ${exclude_patterns} ) + endforeach() endfunction() @@ -126,7 +153,7 @@ function(ly_install_files) set(install_type FILES) endif() - install(${install_type} ${files} + ly_install(${install_type} ${files} DESTINATION ${ly_install_files_DESTINATION} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} # use the default for the time being ) @@ -144,7 +171,7 @@ function(ly_install_run_code CODE) return() endif() - install(CODE ${CODE} + ly_install(CODE ${CODE} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} # use the default for the time being ) @@ -161,8 +188,13 @@ function(ly_install_run_script SCRIPT) return() endif() - install(SCRIPT ${SCRIPT} + ly_install(SCRIPT ${SCRIPT} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} # use the default for the time being ) -endfunction() \ No newline at end of file +endfunction() + +if(LY_INSTALL_ENABLED) + ly_get_absolute_pal_filename(pal_dir ${CMAKE_CURRENT_SOURCE_DIR}/cmake/Platform/${PAL_PLATFORM_NAME}) + include(${pal_dir}/Install_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) +endif() diff --git a/cmake/LYWrappers.cmake b/cmake/LYWrappers.cmake index fb3d420c26..0416d41ece 100644 --- a/cmake/LYWrappers.cmake +++ b/cmake/LYWrappers.cmake @@ -403,7 +403,7 @@ function(ly_target_link_libraries TARGET) message(FATAL_ERROR "You must provide a target") endif() - set_property(GLOBAL APPEND PROPERTY LY_DELAYED_LINK_${TARGET} ${ARGN}) + set_property(TARGET ${TARGET} APPEND PROPERTY LY_DELAYED_LINK ${ARGN}) set_property(GLOBAL APPEND PROPERTY LY_DELAYED_LINK_TARGETS ${TARGET}) # to walk them at the end endfunction() @@ -430,7 +430,7 @@ function(ly_delayed_target_link_libraries) get_property(delayed_targets GLOBAL PROPERTY LY_DELAYED_LINK_TARGETS) foreach(target ${delayed_targets}) - get_property(delayed_link GLOBAL PROPERTY LY_DELAYED_LINK_${target}) + get_property(delayed_link TARGET ${target} PROPERTY LY_DELAYED_LINK) if(delayed_link) cmake_parse_arguments(ly_delayed_target_link_libraries "" "" "${visibilities}" ${delayed_link}) @@ -458,7 +458,6 @@ function(ly_delayed_target_link_libraries) endforeach() endforeach() - set_property(GLOBAL PROPERTY LY_DELAYED_LINK_${target}) endif() diff --git a/cmake/Packaging.cmake b/cmake/Packaging.cmake index 006689549b..6a1e2d8cef 100644 --- a/cmake/Packaging.cmake +++ b/cmake/Packaging.cmake @@ -134,8 +134,9 @@ if(NOT EXISTS ${_cmake_package_dest}) endif() endif() -install(FILES ${_cmake_package_dest} +ly_install(FILES ${_cmake_package_dest} DESTINATION ./Tools/Redistributables/CMake + COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) # the version string and git tags are intended to be synchronized so it should be safe to use that instead @@ -157,8 +158,9 @@ if(${CPACK_PACKAGE_VERSION} VERSION_GREATER "0.0.0.0") list(POP_FRONT _status _status_code) if (${_status_code} EQUAL 0 AND EXISTS ${_3rd_party_license_dest}) - install(FILES ${_3rd_party_license_dest} + ly_install(FILES ${_3rd_party_license_dest} DESTINATION . + COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) else() file(REMOVE ${_3rd_party_license_dest}) @@ -202,44 +204,69 @@ endif() # IMPORTANT: required to be included AFTER setting all property overrides include(CPack REQUIRED) -function(ly_configure_cpack_component ly_configure_cpack_component_NAME) - - set(options REQUIRED) - set(oneValueArgs DISPLAY_NAME DESCRIPTION LICENSE_NAME LICENSE_FILE) - set(multiValueArgs) - - cmake_parse_arguments(ly_configure_cpack_component "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) - - # default to optional - set(component_type DISABLED) - - if(ly_configure_cpack_component_REQUIRED) - set(component_type REQUIRED) - endif() - - set(license_name ${DEFAULT_LICENSE_NAME}) - set(license_file ${DEFAULT_LICENSE_FILE}) - - if(ly_configure_cpack_component_LICENSE_NAME AND ly_configure_cpack_component_LICENSE_FILE) - set(license_name ${ly_configure_cpack_component_LICENSE_NAME}) - set(license_file ${ly_configure_cpack_component_LICENSE_FILE}) - elseif(ly_configure_cpack_component_LICENSE_NAME OR ly_configure_cpack_component_LICENSE_FILE) - message(FATAL_ERROR "Invalid argument configuration. Both LICENSE_NAME and LICENSE_FILE must be set for ly_configure_cpack_component") - endif() - - cpack_add_component( - ${ly_configure_cpack_component_NAME} ${component_type} - DISPLAY_NAME ${ly_configure_cpack_component_DISPLAY_NAME} - DESCRIPTION ${ly_configure_cpack_component_DESCRIPTION} - ) -endfunction() - # configure ALL components here -ly_configure_cpack_component( - ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} REQUIRED - DISPLAY_NAME "${PROJECT_NAME} Core" - DESCRIPTION "${PROJECT_NAME} Headers, Libraries and Tools" -) +file(APPEND "${CPACK_OUTPUT_CONFIG_FILE}" " +set(CPACK_COMPONENTS_ALL ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) +set(CPACK_COMPONENT_${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}_DISPLAY_NAME \"Common files\") +set(CPACK_COMPONENT_${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}_DESCRIPTION \"${PROJECT_NAME} Headers, scripts and common files\") +set(CPACK_COMPONENT_${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}_REQUIRED TRUE) +set(CPACK_COMPONENT_${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}_DISABLED FALSE) + +include(CPackComponents.cmake) +") + +# Generate a file (CPackComponents.config) that we will include that defines the components +# for this build permutation. This way we can get components for other permutations being passed +# through LY_INSTALL_EXTERNAL_BUILD_DIRS +unset(cpack_components_contents) + +set(required "FALSE") +set(disabled "FALSE") +if(${LY_INSTALL_PERMUTATION_COMPONENT} STREQUAL DEFAULT) + set(required "TRUE") +else() + set(disabled "TRUE") +endif() +string(APPEND cpack_components_contents " +list(APPEND CPACK_COMPONENTS_ALL ${LY_INSTALL_PERMUTATION_COMPONENT}) +set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_DISPLAY_NAME \"${LY_BUILD_PERMUTATION} common files\") +set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_DESCRIPTION \"${PROJECT_NAME} scripts and common files for ${LY_BUILD_PERMUTATION}\") +set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_DEPENDS ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) +set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_REQUIRED ${required}) +set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_DISABLED ${disabled}) +") + +foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + set(required "FALSE") + set(disabled "FALSE") + if(${conf} STREQUAL profile AND ${LY_INSTALL_PERMUTATION_COMPONENT} STREQUAL DEFAULT) + set(required "TRUE") + else() + set(disabled "TRUE") + endif() + + # Inject a check to not declare components that have not been built. We are using AzCore since that is a + # common target that will always be build, in every permutation and configuration + string(APPEND cpack_components_contents " +if(EXISTS \"${CMAKE_ARCHIVE_OUTPUT_DIRECTORY}/${conf}/${CMAKE_STATIC_LIBRARY_PREFIX}AzCore${CMAKE_STATIC_LIBRARY_SUFFIX}\") + list(APPEND CPACK_COMPONENTS_ALL ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF}) + set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF}_DISPLAY_NAME \"Binaries for ${LY_BUILD_PERMUTATION} ${conf}\") + set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF}_DESCRIPTION \"${PROJECT_NAME} libraries and applications for ${LY_BUILD_PERMUTATION} ${conf}\") + set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF}_DEPENDS ${LY_INSTALL_PERMUTATION_COMPONENT}) + set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF}_REQUIRED ${required}) + set(CPACK_COMPONENT_${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF}_DISABLED ${disabled}) +endif() +") +endforeach() +file(WRITE "${CMAKE_BINARY_DIR}/CPackComponents.cmake" ${cpack_components_contents}) + +# Inject other build directories +foreach(external_dir ${LY_INSTALL_EXTERNAL_BUILD_DIRS}) + file(APPEND "${CPACK_OUTPUT_CONFIG_FILE}" + "include(${external_dir}/CPackComponents.cmake)\n" + ) +endforeach() if(LY_INSTALLER_DOWNLOAD_URL) strip_trailing_slash(${LY_INSTALLER_DOWNLOAD_URL} LY_INSTALLER_DOWNLOAD_URL) diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index 44cdeb1994..803edb7b1d 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -8,6 +8,14 @@ include(cmake/FileUtil.cmake) +set(LY_INSTALL_EXTERNAL_BUILD_DIRS "" CACHE PATH "External build directories to be included in the install process. This allows to package non-monolithic and monolithic.") +unset(normalized_external_build_dirs) +foreach(external_dir ${LY_INSTALL_EXTERNAL_BUILD_DIRS}) + cmake_path(ABSOLUTE_PATH external_dir BASE_DIRECTORY ${LY_ROOT_FOLDER} NORMALIZE) + list(APPEND normalized_external_build_dirs ${external_dir}) +endforeach() +set(LY_INSTALL_EXTERNAL_BUILD_DIRS ${normalized_external_build_dirs}) + set(CMAKE_INSTALL_MESSAGE NEVER) # Simplify messages to reduce output noise define_property(TARGET PROPERTY LY_INSTALL_GENERATE_RUN_TARGET @@ -19,13 +27,25 @@ define_property(TARGET PROPERTY LY_INSTALL_GENERATE_RUN_TARGET ]] ) -ly_set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME Core) +# We can have elements being installed under the following components: +# - Core (required for all) (default) +# - Default +# - Default_$ +# - Monolithic +# - Monolithic_$ +# Debug/Monolithic are build permutations, so for a CMake run, it can only generate +# one of the permutations. Each build permutation can generate only one cmake_install.cmake. +# Each build permutation will generate the same elements in Core. +# CPack is able to put the two together by taking Core from one permutation and then taking +# each permutation. +ly_set(CMAKE_INSTALL_DEFAULT_COMPONENT_NAME CORE) if(LY_MONOLITHIC_GAME) set(LY_BUILD_PERMUTATION Monolithic) else() set(LY_BUILD_PERMUTATION Default) endif() +string(TOUPPER ${LY_BUILD_PERMUTATION} LY_INSTALL_PERMUTATION_COMPONENT) cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE runtime_output_directory) cmake_path(RELATIVE_PATH CMAKE_LIBRARY_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE library_output_directory) @@ -65,14 +85,24 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar continue() endif() + # For some cases (e.g. codegen) we generate headers that end up in the BUILD_DIR. Since the BUILD_DIR + # is per-permutation, we need to install such headers per permutation. For the other cases, we can install + # under the default component since they are shared across permutations/configs. + cmake_path(IS_PREFIX CMAKE_BINARY_DIR ${include_directory} NORMALIZE include_directory_child_of_build) + if(NOT include_directory_child_of_build) + set(include_directory_component ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) + else() + set(include_directory_component ${LY_INSTALL_PERMUTATION_COMPONENT}) + endif() + unset(rel_include_dir) cmake_path(RELATIVE_PATH include_directory BASE_DIRECTORY ${LY_ROOT_FOLDER} OUTPUT_VARIABLE rel_include_dir) cmake_path(APPEND rel_include_dir "..") cmake_path(NORMAL_PATH rel_include_dir OUTPUT_VARIABLE destination_dir) - - install(DIRECTORY ${include_directory} + + ly_install(DIRECTORY ${include_directory} DESTINATION ${destination_dir} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} + COMPONENT ${include_directory_component} FILES_MATCHING PATTERN *.h PATTERN *.hpp @@ -94,9 +124,9 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar cmake_path(RELATIVE_PATH target_library_output_directory BASE_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} OUTPUT_VARIABLE target_library_output_subdirectory) endif() - if(COMMAND ly_install_target_override) + if(COMMAND ly_setup_target_install_targets_override) # Mac needs special handling because of a cmake issue - ly_install_target_override(TARGET ${TARGET_NAME} + ly_setup_target_install_targets_override(TARGET ${TARGET_NAME} ARCHIVE_DIR ${archive_output_directory} LIBRARY_DIR ${library_output_directory} RUNTIME_DIR ${runtime_output_directory} @@ -104,18 +134,23 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar RUNTIME_SUBDIR ${target_runtime_output_subdirectory} ) else() - install( - TARGETS ${TARGET_NAME} - ARCHIVE - DESTINATION ${archive_output_directory} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - LIBRARY - DESTINATION ${library_output_directory}/${target_library_output_subdirectory} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - RUNTIME - DESTINATION ${runtime_output_directory}/${target_runtime_output_subdirectory} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) + foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + ly_install(TARGETS ${TARGET_NAME} + ARCHIVE + DESTINATION ${archive_output_directory} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + LIBRARY + DESTINATION ${library_output_directory}/${target_library_output_subdirectory} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + RUNTIME + DESTINATION ${runtime_output_directory}/${target_runtime_output_subdirectory} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + ) + endforeach() endif() # CMakeLists.txt related files @@ -157,16 +192,15 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar endif() # Includes need additional processing to add the install root - if(include_directories) - foreach(include ${include_directories}) - string(GENEX_STRIP ${include} include_genex_expr) - if(include_genex_expr STREQUAL include) # only for cases where there are no generation expressions - # Make the include path relative to the source dir where the target will be declared - cmake_path(RELATIVE_PATH include BASE_DIRECTORY ${absolute_target_source_dir} OUTPUT_VARIABLE target_include) - string(APPEND INCLUDE_DIRECTORIES_PLACEHOLDER "${PLACEHOLDER_INDENT}${target_include}\n") - endif() - endforeach() - endif() + foreach(include IN LISTS include_directories) + string(GENEX_STRIP ${include} include_genex_expr) + if(include_genex_expr STREQUAL include) # only for cases where there are no generation expressions + # Make the include path relative to the source dir where the target will be declared + cmake_path(RELATIVE_PATH include BASE_DIRECTORY ${absolute_target_source_dir} OUTPUT_VARIABLE target_include) + list(APPEND INCLUDE_DIRECTORIES_PLACEHOLDER "${PLACEHOLDER_INDENT}${target_include}") + endif() + endforeach() + list(JOIN INCLUDE_DIRECTORIES_PLACEHOLDER "\n" INCLUDE_DIRECTORIES_PLACEHOLDER) string(REPEAT " " 8 PLACEHOLDER_INDENT) get_target_property(RUNTIME_DEPENDENCIES_PLACEHOLDER ${TARGET_NAME} MANUALLY_ADDED_DEPENDENCIES) @@ -178,27 +212,27 @@ function(ly_setup_target OUTPUT_CONFIGURED_TARGET ALIAS_TARGET_NAME absolute_tar endif() string(REPEAT " " 12 PLACEHOLDER_INDENT) - get_target_property(inteface_build_dependencies_props ${TARGET_NAME} INTERFACE_LINK_LIBRARIES) + get_property(interface_build_dependencies_props TARGET ${TARGET_NAME} PROPERTY LY_DELAYED_LINK) unset(INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) - if(inteface_build_dependencies_props) - foreach(build_dependency ${inteface_build_dependencies_props}) + if(interface_build_dependencies_props) + cmake_parse_arguments(build_deps "" "" "PRIVATE;PUBLIC;INTERFACE" ${interface_build_dependencies_props}) + # Interface and public dependencies should always be exposed + set(build_deps_target ${build_deps_INTERFACE}) + if(build_deps_PUBLIC) + set(build_deps_target "${build_deps_target};${build_deps_PUBLIC}") + endif() + # Private dependencies should only be exposed if it is a static library, since in those cases, link + # dependencies are transfered to the downstream dependencies + if("${target_type}" STREQUAL "STATIC_LIBRARY") + set(build_deps_target "${build_deps_target};${build_deps_PRIVATE}") + endif() + foreach(build_dependency IN LISTS build_deps_target) # Skip wrapping produced when targets are not created in the same directory - if(NOT ${build_dependency} MATCHES "^::@") + if(build_dependency) list(APPEND INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER "${PLACEHOLDER_INDENT}${build_dependency}") endif() endforeach() endif() - # We also need to pass the private link libraries since we will use that to generate the runtime dependencies - get_target_property(private_build_dependencies_props ${TARGET_NAME} LINK_LIBRARIES) - if(private_build_dependencies_props) - foreach(build_dependency ${private_build_dependencies_props}) - # Skip wrapping produced when targets are not created in the same directory - if(NOT ${build_dependency} MATCHES "^::@") - list(APPEND INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER "${PLACEHOLDER_INDENT}${build_dependency}") - endif() - endforeach() - endif() - list(REMOVE_DUPLICATES INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) list(JOIN INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER "\n" INTERFACE_BUILD_DEPENDENCIES_PLACEHOLDER) string(REPEAT " " 8 PLACEHOLDER_INDENT) @@ -269,10 +303,15 @@ set_property(TARGET ${NAME_PLACEHOLDER} set(target_install_source_dir ${CMAKE_CURRENT_BINARY_DIR}/install/${relative_target_source_dir}) file(GENERATE OUTPUT "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/${NAME_PLACEHOLDER}_$.cmake" CONTENT "${target_file_contents}") - install(FILES "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/${NAME_PLACEHOLDER}_$.cmake" - DESTINATION ${relative_target_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) + + foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + ly_install(FILES "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/${NAME_PLACEHOLDER}_${conf}.cmake" + DESTINATION ${relative_target_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + ) + endforeach() # Since a CMakeLists.txt could contain multiple targets, we generate it in a folder per target ly_file_read(${LY_ROOT_FOLDER}/cmake/install/InstalledTarget.in target_cmakelists_template) @@ -312,7 +351,8 @@ function(ly_setup_subdirectory absolute_target_source_dir) @cmake_copyright_comment@ include(Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake) ]] @ONLY) - install(FILES "${target_install_source_dir}/CMakeLists.txt" + + ly_install(FILES "${target_install_source_dir}/CMakeLists.txt" DESTINATION ${relative_target_source_dir} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) @@ -322,12 +362,12 @@ include(Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cm file(CONFIGURE OUTPUT "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake" CONTENT [[ @cmake_copyright_comment@ if(LY_MONOLITHIC_GAME) - include(Platform/${PAL_PLATFORM_NAME}/Monolithic/permutation.cmake) + include(Platform/${PAL_PLATFORM_NAME}/Monolithic/permutation.cmake OPTIONAL) else() include(Platform/${PAL_PLATFORM_NAME}/Default/permutation.cmake) endif() ]]) - install(FILES "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake" + ly_install(FILES "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/platform_${PAL_PLATFORM_NAME_LOWERCASE}.cmake" DESTINATION ${relative_target_source_dir}/Platform/${PAL_PLATFORM_NAME} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) @@ -347,63 +387,45 @@ endif() "${GEM_VARIANT_TO_LOAD_PLACEHOLDER}" "${ENABLE_GEMS_PLACEHOLDER}" ) - install(FILES "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/permutation.cmake" - DESTINATION ${relative_target_source_dir}//Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} + + ly_install(FILES "${target_install_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/permutation.cmake" + DESTINATION ${relative_target_source_dir}/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT} ) endfunction() -#! ly_setup_o3de_install: orchestrates the installation of the different parts. This is the entry point from the root CMakeLists.txt -function(ly_setup_o3de_install) - - ly_setup_subdirectories() - ly_setup_cmake_install() - ly_setup_runtime_dependencies() - ly_setup_assets() - - # Misc - install(FILES - ${LY_ROOT_FOLDER}/pytest.ini - ${LY_ROOT_FOLDER}/LICENSE.txt - ${LY_ROOT_FOLDER}/README.md - DESTINATION . - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) - - if(COMMAND ly_post_install_steps) - ly_post_install_steps() - endif() - -endfunction() - #! ly_setup_cmake_install: install the "cmake" folder function(ly_setup_cmake_install) - install(DIRECTORY "${LY_ROOT_FOLDER}/cmake" + ly_install(DIRECTORY "${LY_ROOT_FOLDER}/cmake" DESTINATION . COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} PATTERN "__pycache__" EXCLUDE PATTERN "Findo3de.cmake" EXCLUDE + PATTERN "cmake/ConfigurationTypes.cmake" EXCLUDE REGEX "3rdParty/Platform\/.*\/BuiltInPackages_.*\.cmake" EXCLUDE ) + # Connect configuration types - install(FILES "${LY_ROOT_FOLDER}/cmake/install/ConfigurationTypes.cmake" + ly_install(FILES "${LY_ROOT_FOLDER}/cmake/install/ConfigurationTypes.cmake" DESTINATION cmake COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) - # Inject code that will generate each ConfigurationType_.cmake file - set(install_configuration_type_template [=[ - configure_file(@LY_ROOT_FOLDER@/cmake/install/ConfigurationType_config.cmake.in - ${CMAKE_INSTALL_PREFIX}/cmake/Platform/@PAL_PLATFORM_NAME@/@LY_BUILD_PERMUTATION@/ConfigurationTypes_${CMAKE_INSTALL_CONFIG_NAME}.cmake + + # generate each ConfigurationType_.cmake file and install it under that configuration + foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + configure_file("${LY_ROOT_FOLDER}/cmake/install/ConfigurationType_config.cmake.in" + "${CMAKE_BINARY_DIR}/cmake/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/ConfigurationTypes_${conf}.cmake" @ONLY ) - message(STATUS "Generated ${CMAKE_INSTALL_PREFIX}/cmake/Platform/@PAL_PLATFORM_NAME@/@LY_BUILD_PERMUTATION@/ConfigurationTypes_${CMAKE_INSTALL_CONFIG_NAME}.cmake") - ]=]) - string(CONFIGURE "${install_configuration_type_template}" install_configuration_type @ONLY) - install(CODE "${install_configuration_type}" - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) + ly_install(FILES "${CMAKE_BINARY_DIR}/cmake/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION}/ConfigurationTypes_${conf}.cmake" + DESTINATION cmake/Platform/${PAL_PLATFORM_NAME}/${LY_BUILD_PERMUTATION} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + ) + endforeach() # Transform the LY_EXTERNAL_SUBDIRS list into a json array set(indent " ") @@ -422,8 +444,7 @@ function(ly_setup_cmake_install) configure_file(${LY_ROOT_FOLDER}/cmake/install/engine.json.in ${CMAKE_CURRENT_BINARY_DIR}/cmake/engine.json @ONLY) - install( - FILES + ly_install(FILES "${LY_ROOT_FOLDER}/CMakeLists.txt" "${CMAKE_CURRENT_BINARY_DIR}/cmake/engine.json" DESTINATION . @@ -446,11 +467,12 @@ function(ly_setup_cmake_install) list(APPEND additional_platform_files "${plat_files}") endforeach() endforeach() - install(FILES ${additional_find_files} + + ly_install(FILES ${additional_find_files} DESTINATION cmake/3rdParty COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) - install(FILES ${additional_platform_files} + ly_install(FILES ${additional_platform_files} DESTINATION cmake/3rdParty/Platform/${PAL_PLATFORM_NAME} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) @@ -467,7 +489,7 @@ function(ly_setup_cmake_install) endforeach() configure_file(${LY_ROOT_FOLDER}/cmake/install/Findo3de.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/cmake/Findo3de.cmake @ONLY) - install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cmake/Findo3de.cmake" + ly_install(FILES "${CMAKE_CURRENT_BINARY_DIR}/cmake/Findo3de.cmake" DESTINATION cmake COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) @@ -476,10 +498,12 @@ function(ly_setup_cmake_install) # all the associations in ly_associate_package and then generate them into BuiltInPackages_.cmake. This # will consolidate all associations in one file get_property(all_package_names GLOBAL PROPERTY LY_PACKAGE_NAMES) + list(REMOVE_DUPLICATES all_package_names) set(builtinpackages "# Generated by O3DE install\n\n") foreach(package_name IN LISTS all_package_names) get_property(package_hash GLOBAL PROPERTY LY_PACKAGE_HASH_${package_name}) get_property(targets GLOBAL PROPERTY LY_PACKAGE_TARGETS_${package_name}) + list(REMOVE_DUPLICATES targets) string(APPEND builtinpackages "ly_associate_package(PACKAGE_NAME ${package_name} TARGETS ${targets} PACKAGE_HASH ${package_hash})\n") endforeach() @@ -487,7 +511,7 @@ function(ly_setup_cmake_install) file(GENERATE OUTPUT ${pal_builtin_file} CONTENT ${builtinpackages} ) - install(FILES "${pal_builtin_file}" + ly_install(FILES "${pal_builtin_file}" DESTINATION cmake/3rdParty/Platform/${PAL_PLATFORM_NAME} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) @@ -498,14 +522,17 @@ endfunction() function(ly_setup_runtime_dependencies) # Common functions used by the bellow code - if(COMMAND ly_install_code_function_override) - ly_install_code_function_override() + if(COMMAND ly_setup_runtime_dependencies_copy_function_override) + ly_setup_runtime_dependencies_copy_function_override() else() - install(CODE + ly_install(CODE "function(ly_copy source_file target_directory) - file(COPY \"\${source_file}\" DESTINATION \"\${target_directory}\" FILE_PERMISSIONS ${LY_COPY_PERMISSIONS}) + cmake_path(GET source_file FILENAME file_name) + if(NOT EXISTS \${target_directory}/\${file_name}) + file(COPY \"\${source_file}\" DESTINATION \"\${target_directory}\" FILE_PERMISSIONS ${LY_COPY_PERMISSIONS}) + endif() endfunction()" - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} + COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) endif() @@ -545,9 +572,15 @@ endfunction()" list(REMOVE_DUPLICATES runtime_commands) list(JOIN runtime_commands " " runtime_commands_str) # the spaces are just to see the right identation in the cmake_install.cmake file - install(CODE "${runtime_commands_str}" - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) + foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + ly_install(CODE +"if(\"\${CMAKE_INSTALL_CONFIG_NAME}\" MATCHES \"^(${conf})\$\") + ${runtime_commands_str} +endif()" + COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}_${UCONF} + ) + endforeach() endfunction() @@ -632,17 +665,19 @@ function(ly_setup_assets) if (NOT gem_install_dest_dir) cmake_path(SET gem_install_dest_dir .) endif() + if(IS_DIRECTORY ${gem_absolute_path}) - install(DIRECTORY "${gem_absolute_path}" + ly_install(DIRECTORY "${gem_absolute_path}" DESTINATION ${gem_install_dest_dir} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) elseif (EXISTS ${gem_absolute_path}) - install(FILES ${gem_absolute_path} + ly_install(FILES ${gem_absolute_path} DESTINATION ${gem_install_dest_dir} COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) endif() + endforeach() endforeach() @@ -702,4 +737,37 @@ function(ly_setup_subdirectory_enable_gems absolute_target_source_dir output_scr string(APPEND enable_gems_calls ${enable_gems_command}) endforeach() set(${output_script} ${enable_gems_calls} PARENT_SCOPE) +endfunction() + +#! ly_setup_o3de_install: orchestrates the installation of the different parts. This is the entry point from the root CMakeLists.txt +function(ly_setup_o3de_install) + + ly_setup_subdirectories() + ly_setup_cmake_install() + ly_setup_runtime_dependencies() + ly_setup_assets() + + # Misc + ly_install(FILES + ${LY_ROOT_FOLDER}/pytest.ini + ${LY_ROOT_FOLDER}/LICENSE.txt + ${LY_ROOT_FOLDER}/README.md + DESTINATION . + COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} + ) + + # Inject other build directories + foreach(external_dir ${LY_INSTALL_EXTERNAL_BUILD_DIRS}) + ly_install(CODE +"set(LY_CORE_COMPONENT_ALREADY_INCLUDED TRUE) +include(${external_dir}/cmake_install.cmake) +set(LY_CORE_COMPONENT_ALREADY_INCLUDED FALSE)" + ALL_COMPONENTS + ) + endforeach() + + if(COMMAND ly_post_install_steps) + ly_post_install_steps() + endif() + endfunction() \ No newline at end of file diff --git a/cmake/Platform/Linux/Install_linux.cmake b/cmake/Platform/Linux/Install_linux.cmake index dea26e5872..54d1e18837 100644 --- a/cmake/Platform/Linux/Install_linux.cmake +++ b/cmake/Platform/Linux/Install_linux.cmake @@ -6,7 +6,7 @@ # # -#! ly_install_code_function_override: Linux-specific copy function to handle RPATH fixes +#! ly_setup_runtime_dependencies_copy_function_override: Linux-specific copy function to handle RPATH fixes set(ly_copy_template [[ function(ly_copy source_file target_directory) file(COPY "${source_file}" DESTINATION "${target_directory}" FILE_PERMISSIONS @LY_COPY_PERMISSIONS@ FOLLOW_SYMLINK_CHAIN) @@ -20,9 +20,9 @@ function(ly_copy source_file target_directory) endif() endfunction()]]) -function(ly_install_code_function_override) +function(ly_setup_runtime_dependencies_copy_function_override) string(CONFIGURE "${ly_copy_template}" ly_copy_function_linux @ONLY) - install(CODE "${ly_copy_function_linux}" + ly_install(CODE "${ly_copy_function_linux}" COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} ) endfunction() diff --git a/cmake/Platform/Mac/Install_mac.cmake b/cmake/Platform/Mac/Install_mac.cmake index bdc2300131..76c381138d 100644 --- a/cmake/Platform/Mac/Install_mac.cmake +++ b/cmake/Platform/Mac/Install_mac.cmake @@ -39,10 +39,10 @@ file(GENERATE # This needs to be done here because it needs to update the install prefix # before cmake does anything else in the install process. configure_file(${LY_ROOT_FOLDER}/cmake/Platform/Mac/PreInstallSteps_mac.cmake.in ${CMAKE_BINARY_DIR}/runtime_install/PreInstallSteps_mac.cmake @ONLY) -install(SCRIPT ${CMAKE_BINARY_DIR}/runtime_install/PreInstallSteps_mac.cmake COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) +ly_install(SCRIPT ${CMAKE_BINARY_DIR}/runtime_install/PreInstallSteps_mac.cmake COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME}) -#! ly_install_target_override: Mac specific target installation -function(ly_install_target_override) +#! ly_setup_target_install_targets_override: Mac specific target installation +function(ly_setup_target_install_targets_override) set(options) set(oneValueArgs TARGET ARCHIVE_DIR LIBRARY_DIR RUNTIME_DIR LIBRARY_SUBDIR RUNTIME_SUBDIR) @@ -58,24 +58,31 @@ function(ly_install_target_override) set_property(TARGET ${ly_platform_install_target_TARGET} PROPERTY RESOURCE "") endif() - install( - TARGETS ${ly_platform_install_target_TARGET} - ARCHIVE - DESTINATION ${ly_platform_install_target_ARCHIVE_DIR} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - LIBRARY - DESTINATION ${ly_platform_install_target_LIBRARY_DIR}/${ly_platform_install_target_LIBRARY_SUBDIR} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - RUNTIME - DESTINATION ${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - BUNDLE - DESTINATION ${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - RESOURCE - DESTINATION ${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR} - COMPONENT ${CMAKE_INSTALL_DEFAULT_COMPONENT_NAME} - ) + foreach(conf IN LISTS CMAKE_CONFIGURATION_TYPES) + string(TOUPPER ${conf} UCONF) + ly_install(TARGETS ${TARGET_NAME} + ARCHIVE + DESTINATION ${ly_platform_install_target_ARCHIVE_DIR} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + LIBRARY + DESTINATION ${ly_platform_install_target_LIBRARY_DIR}/${ly_platform_install_target_LIBRARY_SUBDIR} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + RUNTIME + DESTINATION ${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + BUNDLE + DESTINATION ${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + RESOURCE + DESTINATION ${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR} + COMPONENT ${LY_INSTALL_PERMUTATION_COMPONENT}_${UCONF} + CONFIGURATIONS ${conf} + ) + endforeach() set(install_relative_binaries_path "${ly_platform_install_target_RUNTIME_DIR}/${ly_platform_install_target_RUNTIME_SUBDIR}") @@ -102,8 +109,8 @@ function(ly_install_target_override) endif() endfunction() -#! ly_install_code_function_override: Mac specific copy function to handle frameworks -function(ly_install_code_function_override) +#! ly_setup_runtime_dependencies_copy_function_override: Mac specific copy function to handle frameworks +function(ly_setup_runtime_dependencies_copy_function_override) configure_file(${LY_ROOT_FOLDER}/cmake/Platform/Mac/InstallUtils_mac.cmake.in ${CMAKE_BINARY_DIR}/runtime_install/InstallUtils_mac.cmake @ONLY) ly_install_run_script(${CMAKE_BINARY_DIR}/runtime_install/InstallUtils_mac.cmake) diff --git a/cmake/Platform/Windows/PackagingPostBuild.cmake b/cmake/Platform/Windows/PackagingPostBuild.cmake index 377a9fb221..ac457bea87 100644 --- a/cmake/Platform/Windows/PackagingPostBuild.cmake +++ b/cmake/Platform/Windows/PackagingPostBuild.cmake @@ -32,9 +32,6 @@ set(_addtional_defines -dCPACK_RESOURCE_PATH=${CPACK_SOURCE_DIR}/Platform/Windows/Packaging ) -file(REAL_PATH "${CPACK_SOURCE_DIR}/.." _root_path) -file(TO_NATIVE_PATH "${_root_path}/scripts/signer/Platform/Windows/signer.ps1" _sign_script) - if(CPACK_LICENSE_URL) list(APPEND _addtional_defines -dCPACK_LICENSE_URL=${CPACK_LICENSE_URL}) endif() @@ -58,28 +55,41 @@ set(_light_command -o "${_bootstrap_output_file}" ) -set(_signing_command - psexec.exe - -accepteula - -nobanner - -s - powershell.exe - -NoLogo - -ExecutionPolicy Bypass - -File ${_sign_script} -) +if(CPACK_UPLOAD_URL) # Skip signing if we are not uploading the package + file(REAL_PATH "${CPACK_SOURCE_DIR}/.." _root_path) + file(TO_NATIVE_PATH "${_root_path}/scripts/signer/Platform/Windows/signer.ps1" _sign_script) -message(STATUS "Signing package files in ${_cpack_wix_out_dir}") -execute_process( - COMMAND ${_signing_command} -packagePath ${_cpack_wix_out_dir} - RESULT_VARIABLE _signing_result - ERROR_VARIABLE _signing_errors - OUTPUT_VARIABLE _signing_output - ECHO_OUTPUT_VARIABLE -) + unset(_signing_command) + find_program(_psiexec_path psexec.exe) + if(_psiexec_path) + list(APPEND _signing_command + ${_psiexec_path} + -accepteula + -nobanner + -s + ) + endif() -if(NOT ${_signing_result} EQUAL 0) - message(FATAL_ERROR "An error occurred during signing package files. ${_signing_errors}") + find_program(_powershell_path powershell.exe REQUIRED) + list(APPEND _signing_command + ${_powershell_path} + -NoLogo + -ExecutionPolicy Bypass + -File ${_sign_script} + ) + + message(STATUS "Signing package files in ${_cpack_wix_out_dir}") + execute_process( + COMMAND ${_signing_command} -packagePath ${_cpack_wix_out_dir} + RESULT_VARIABLE _signing_result + ERROR_VARIABLE _signing_errors + OUTPUT_VARIABLE _signing_output + ECHO_OUTPUT_VARIABLE + ) + + if(NOT ${_signing_result} EQUAL 0) + message(FATAL_ERROR "An error occurred during signing package files. ${_signing_errors}") + endif() endif() message(STATUS "Creating Bootstrap Installer...") @@ -107,17 +117,19 @@ file(COPY ${_bootstrap_output_file} message(STATUS "Bootstrap installer generated to ${CPACK_PACKAGE_DIRECTORY}/${_bootstrap_filename}") -message(STATUS "Signing bootstrap installer in ${CPACK_PACKAGE_DIRECTORY}") -execute_process( - COMMAND ${_signing_command} -bootstrapPath ${CPACK_PACKAGE_DIRECTORY}/${_bootstrap_filename} - RESULT_VARIABLE _signing_result - ERROR_VARIABLE _signing_errors - OUTPUT_VARIABLE _signing_output - ECHO_OUTPUT_VARIABLE -) +if(CPACK_UPLOAD_URL) # Skip signing if we are not uploading the package + message(STATUS "Signing bootstrap installer in ${CPACK_PACKAGE_DIRECTORY}") + execute_process( + COMMAND ${_signing_command} -bootstrapPath ${CPACK_PACKAGE_DIRECTORY}/${_bootstrap_filename} + RESULT_VARIABLE _signing_result + ERROR_VARIABLE _signing_errors + OUTPUT_VARIABLE _signing_output + ECHO_OUTPUT_VARIABLE + ) -if(NOT ${_signing_result} EQUAL 0) - message(FATAL_ERROR "An error occurred during signing bootstrap installer. ${_signing_errors}") + if(NOT ${_signing_result} EQUAL 0) + message(FATAL_ERROR "An error occurred during signing bootstrap installer. ${_signing_errors}") + endif() endif() # use the internal default path if somehow not specified from cpack_configure_downloads diff --git a/cmake/Platform/Windows/PackagingPreBuild.cmake b/cmake/Platform/Windows/PackagingPreBuild.cmake index d3924c7a02..7f2eedf352 100644 --- a/cmake/Platform/Windows/PackagingPreBuild.cmake +++ b/cmake/Platform/Windows/PackagingPreBuild.cmake @@ -6,21 +6,41 @@ # # +if(NOT CPACK_UPLOAD_URL) # Skip signing if we are not uploading the package + return() +endif() + file(REAL_PATH "${CPACK_SOURCE_DIR}/.." _root_path) set(_cpack_wix_out_dir ${CPACK_TOPLEVEL_DIRECTORY}) file(TO_NATIVE_PATH "${_root_path}/scripts/signer/Platform/Windows/signer.ps1" _sign_script) -set(_signing_command - psexec.exe - -accepteula - -nobanner - -s - powershell.exe +unset(_signing_command) +find_program(_psiexec_path psexec.exe) +if(_psiexec_path) + list(APPEND _signing_command + ${_psiexec_path} + -accepteula + -nobanner + -s + ) +endif() + +find_program(_powershell_path powershell.exe REQUIRED) +list(APPEND _signing_command + ${_powershell_path} -NoLogo - -ExecutionPolicy Bypass + -ExecutionPolicy Bypass -File ${_sign_script} ) +# This requires to have a valid local certificate. In continuous integration, these certificates are stored +# in the machine directly. +# You can generate a test certificate to be able to run this in a PowerShell elevated promp with: +# New-SelfSignedCertificate -DnsName foo.o3de.com -Type CodeSigning -CertStoreLocation Cert:\CurrentUser\My +# Export-Certificate -Cert (Get-ChildItem Cert:\CurrentUser\My\) -Filepath "c:\selfsigned.crt" +# Import-Certificate -FilePath "c:\selfsigned.crt" -Cert Cert:\CurrentUser\TrustedPublisher +# Import-Certificate -FilePath "c:\selfsigned.crt" -Cert Cert:\CurrentUser\Root + message(STATUS "Signing executable files in ${_cpack_wix_out_dir}") execute_process( COMMAND ${_signing_command} -exePath ${_cpack_wix_out_dir} @@ -32,6 +52,6 @@ execute_process( if(NOT ${_signing_result} EQUAL 0) message(FATAL_ERROR "An error occurred during signing executable files. ${_signing_errors}") +else() + message(STATUS "Signing exes complete!") endif() - -message(STATUS "Signing exes complete!") diff --git a/cmake/Platform/Windows/Packaging_windows.cmake b/cmake/Platform/Windows/Packaging_windows.cmake index 4a03df2fd2..7b8f5a6c19 100644 --- a/cmake/Platform/Windows/Packaging_windows.cmake +++ b/cmake/Platform/Windows/Packaging_windows.cmake @@ -28,11 +28,8 @@ set(_cmake_package_name "cmake-${CPACK_DESIRED_CMAKE_VERSION}-windows-x86_64") set(CPACK_CMAKE_PACKAGE_FILE "${_cmake_package_name}.zip") set(CPACK_CMAKE_PACKAGE_HASH "15a49e2ab81c1822d75b1b1a92f7863f58e31f6d6aac1c4103eef2b071be3112") -# workaround for shortening the path cpack installs to by stripping the platform directory and forcing monolithic -# mode to strip out component folders. this unfortunately is the closest we can get to changing the install location -# as CPACK_PACKAGING_INSTALL_PREFIX/CPACK_SET_DESTDIR isn't supported for the WiX generator +# workaround for shortening the path cpack installs to by stripping the platform directory set(CPACK_TOPLEVEL_TAG "") -set(CPACK_MONOLITHIC_INSTALL ON) # CPack will generate the WiX product/upgrade GUIDs further down the chain if they weren't supplied # however, they are unique for each run. instead, let's do the auto generation here and add it to @@ -108,6 +105,8 @@ set(_raw_text_license [[ #(loc.InstallEulaAcceptance) ]]) +# The offline installer generation will be a single monolithic MSI. The WIX burn tool for the bootstrapper EXE has a size limitation. +# So we will exclude the generation of the boostrapper EXE in the offline case. if(LY_INSTALLER_DOWNLOAD_URL) set(WIX_THEME_WARNING_IMAGE ${CPACK_SOURCE_DIR}/Platform/Windows/Packaging/warning.png) diff --git a/cmake/Projects.cmake b/cmake/Projects.cmake index c09fe0fc6f..34ef3efd9b 100644 --- a/cmake/Projects.cmake +++ b/cmake/Projects.cmake @@ -150,22 +150,43 @@ foreach(project ${LY_PROJECTS}) # Get project name o3de_read_json_key(project_name ${full_directory_path}/project.json "project_name") + # The cmake tar command has a bit of a flaw + # Any paths within the archive files it creates are relative to the current working directory. + # That means with the setup of: + # cwd = "/Cache/pc" + # project product assets = "/Cache/pc/*" + # cmake dependency registry files = "/build/bin/Release/Registry/*" + # Running the tar command would result in the assets being placed in the to layout + # correctly, but the registry files + # engine.pak/ + # ../...build/bin/Release/Registry/cmake_dependencies.*.setreg -> Not correct + # project.json -> Correct + # Generate pak for project in release installs cmake_path(RELATIVE_PATH CMAKE_RUNTIME_OUTPUT_DIRECTORY BASE_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_VARIABLE install_base_runtime_output_directory) set(install_engine_pak_template [=[ if("${CMAKE_INSTALL_CONFIG_NAME}" MATCHES "^([Rr][Ee][Ll][Ee][Aa][Ss][Ee])$") set(install_output_folder "${CMAKE_INSTALL_PREFIX}/@install_base_runtime_output_directory@/@PAL_PLATFORM_NAME@/${CMAKE_INSTALL_CONFIG_NAME}/@LY_BUILD_PERMUTATION@") set(install_pak_output_folder "${install_output_folder}/Cache/@LY_ASSET_DEPLOY_ASSET_TYPE@") + set(runtime_output_directory_RELEASE @CMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE@) if(NOT DEFINED LY_ASSET_DEPLOY_ASSET_TYPE) set(LY_ASSET_DEPLOY_ASSET_TYPE @LY_ASSET_DEPLOY_ASSET_TYPE@) endif() message(STATUS "Generating ${install_pak_output_folder}/engine.pak from @full_directory_path@/Cache/${LY_ASSET_DEPLOY_ASSET_TYPE}") file(MAKE_DIRECTORY "${install_pak_output_folder}") cmake_path(SET cache_product_path "@full_directory_path@/Cache/${LY_ASSET_DEPLOY_ASSET_TYPE}") + # Copy the generated cmake_dependencies.*.setreg files for loading gems in non-monolithic to the cache + file(GLOB gem_source_paths_setreg "${runtime_output_directory_RELEASE}/Registry/*.setreg") + # The MergeSettingsToRegistry_TargetBuildDependencyRegistry function looks for lowercase "registry" + # So make sure the to copy it to a lowercase path, so that it works on non-case sensitive filesystems + file(MAKE_DIRECTORY "${cache_product_path}/registry") + file(COPY ${gem_source_paths_setreg} DESTINATION "${cache_product_path}/registry") + file(GLOB product_assets "${cache_product_path}/*") - if(product_assets) + list(APPEND pak_artifacts ${product_assets}) + if(pak_artifacts) execute_process( - COMMAND ${CMAKE_COMMAND} -E tar "cf" "${install_pak_output_folder}/engine.pak" --format=zip -- ${product_assets} + COMMAND ${CMAKE_COMMAND} -E tar "cf" "${install_pak_output_folder}/engine.pak" --format=zip -- ${pak_artifacts} WORKING_DIRECTORY "${cache_product_path}" RESULT_VARIABLE archive_creation_result ) diff --git a/cmake/install/ConfigurationType_config.cmake.in b/cmake/install/ConfigurationType_config.cmake.in index 074e034899..0a6940d7ff 100644 --- a/cmake/install/ConfigurationType_config.cmake.in +++ b/cmake/install/ConfigurationType_config.cmake.in @@ -8,4 +8,4 @@ include_guard(GLOBAL) -list(APPEND CMAKE_CONFIGURATION_TYPES @CMAKE_INSTALL_CONFIG_NAME@) +list(APPEND CMAKE_CONFIGURATION_TYPES @conf@) diff --git a/engine.json b/engine.json index 14deffecb8..05ccd0abfd 100644 --- a/engine.json +++ b/engine.json @@ -92,8 +92,9 @@ "templates": [ "Templates/AssetGem", "Templates/DefaultGem", - "Templates/CustomTool", "Templates/DefaultProject", - "Templates/MinimalProject" + "Templates/CppToolGem", + "Templates/MinimalProject", + "Templates/PythonToolGem" ] } diff --git a/scripts/build/Jenkins/Jenkinsfile b/scripts/build/Jenkins/Jenkinsfile index f5e5edcf66..127d6b80ca 100644 --- a/scripts/build/Jenkins/Jenkinsfile +++ b/scripts/build/Jenkins/Jenkinsfile @@ -16,7 +16,7 @@ EMPTY_JSON = readJSON text: '{}' ENGINE_REPOSITORY_NAME = 'o3de' // Branches with build snapshots -BUILD_SNAPSHOTS = ['development', 'stabilization/2106'] +BUILD_SNAPSHOTS = ['development', 'stabilization/2110'] // Build snapshots with empty snapshot (for use with 'SNAPSHOT' pipeline paramater) BUILD_SNAPSHOTS_WITH_EMPTY = BUILD_SNAPSHOTS + '' @@ -449,8 +449,8 @@ def UploadAPLogs(Map options, String branchName, String jobName, String workspac } def command = "${pythonPath} -u ${s3UploadScriptPath} --base_dir ${apLogsPath} " + "--file_regex \".*\" --bucket ${env.AP_LOGS_S3_BUCKET} " + - "--search_subdirectories True --key_prefix ${env.JOB_NAME}/${branchName}/${env.BUILD_NUMBER}/${jobName}" + - "--extra-args {\"ACL\": \"bucket-owner-full-control\"}" + "--search_subdirectories True --key_prefix ${env.JENKINS_JOB_NAME}/${branchName}/${env.BUILD_NUMBER}/${jobName} " + + "--extra_args {\"ACL\": \"bucket-owner-full-control\"}" palSh(command, "Uploading AP logs for job ${jobName} for branch ${branchName}", false) } } @@ -806,6 +806,7 @@ try { platform.value.build_types.each { build_job -> if (IsJobEnabled(branchName, build_job, pipelineName, platform.key)) { // User can filter jobs, jobs are tagged by pipeline def envVars = GetBuildEnvVars(platform.value.PIPELINE_ENV ?: EMPTY_JSON, build_job.value.PIPELINE_ENV ?: EMPTY_JSON, pipelineName) + envVars['JENKINS_JOB_NAME'] = env.JOB_NAME // Save original Jenkins job name to JENKINS_JOB_NAME envVars['JOB_NAME'] = "${branchName}_${platform.key}_${build_job.key}" // backwards compatibility, some scripts rely on this someBuildHappened = true diff --git a/scripts/build/Platform/Android/build_config.json b/scripts/build/Platform/Android/build_config.json index c1fbb1dd87..5e2da44a3c 100644 --- a/scripts/build/Platform/Android/build_config.json +++ b/scripts/build/Platform/Android/build_config.json @@ -35,7 +35,7 @@ "PARAMETERS": { "CONFIGURATION":"debug", "OUTPUT_DIRECTORY":"build\\android", - "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\" -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\"", "CMAKE_LY_PROJECTS":"AutomatedTesting", "CMAKE_TARGET":"all", "CMAKE_BUILD_ARGS":"-j!NUMBER_OF_PROCESSORS!" @@ -60,7 +60,7 @@ "PARAMETERS": { "CONFIGURATION":"profile", "OUTPUT_DIRECTORY":"build\\android", - "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\" -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\"", "CMAKE_LY_PROJECTS":"AutomatedTesting", "CMAKE_TARGET":"all", "CMAKE_BUILD_ARGS":"-j!NUMBER_OF_PROCESSORS!" @@ -93,7 +93,7 @@ "PARAMETERS": { "CONFIGURATION":"profile", "OUTPUT_DIRECTORY":"build\\windows_vs2019", - "CMAKE_OPTIONS":"-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS":"-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS":"AutomatedTesting", "CMAKE_TARGET":"AssetProcessorBatch", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -112,7 +112,7 @@ "PARAMETERS": { "CONFIGURATION":"release", "OUTPUT_DIRECTORY":"build\\android", - "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\" -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\"", "CMAKE_LY_PROJECTS":"AutomatedTesting", "CMAKE_TARGET":"all", "CMAKE_BUILD_ARGS":"-j!NUMBER_OF_PROCESSORS!" @@ -128,7 +128,7 @@ "PARAMETERS": { "CONFIGURATION":"release", "OUTPUT_DIRECTORY":"build\\mono_android", - "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\" -DLY_MONOLITHIC_GAME=TRUE -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS":"-G \"Ninja Multi-Config\" -DCMAKE_TOOLCHAIN_FILE=cmake\\Platform\\Android\\Toolchain_android.cmake -DANDROID_ABI=arm64-v8a -DANDROID_ARM_MODE=arm -DANDROID_ARM_NEON=FALSE -DANDROID_NATIVE_API_LEVEL=21 -DLY_NDK_DIR=\"!LY_3RDPARTY_PATH!\\android-ndk\\r21d\" -DLY_MONOLITHIC_GAME=TRUE", "CMAKE_LY_PROJECTS":"AutomatedTesting", "CMAKE_TARGET":"all", "CMAKE_BUILD_ARGS":"-j!NUMBER_OF_PROCESSORS!" diff --git a/scripts/build/Platform/Linux/build_config.json b/scripts/build/Platform/Linux/build_config.json index b76a950beb..c0307de23d 100644 --- a/scripts/build/Platform/Linux/build_config.json +++ b/scripts/build/Platform/Linux/build_config.json @@ -37,7 +37,7 @@ "PARAMETERS": { "CONFIGURATION": "debug", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all" } @@ -53,7 +53,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all" } @@ -80,7 +80,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", "CTEST_OPTIONS": "-E Gem::EMotionFX.Editor.Tests -LE (SUITE_sandbox|SUITE_awsi) -L FRAMEWORK_googletest --no-tests=error", @@ -110,7 +110,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "AssetProcessorBatch", "ASSET_PROCESSOR_BINARY": "bin/profile/AssetProcessorBatch", @@ -142,7 +142,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_periodic", "CTEST_OPTIONS": "-L (SUITE_periodic) --no-tests=error", @@ -162,7 +162,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4 -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4 -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all", "CTEST_OPTIONS": "-L (SUITE_sandbox) --no-tests=error" @@ -178,7 +178,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_benchmark", "CTEST_OPTIONS": "-L (SUITE_benchmark) --no-tests=error", @@ -195,7 +195,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build/linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all" } @@ -210,7 +210,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build/mono_linux", - "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_MONOLITHIC_GAME=TRUE -DLY_UNITY_BUILD=TRUE -DLY_PARALLEL_LINK_JOBS=4", + "CMAKE_OPTIONS": "-G 'Ninja Multi-Config' -DCMAKE_C_COMPILER=clang-6.0 -DCMAKE_CXX_COMPILER=clang++-6.0 -DLY_MONOLITHIC_GAME=TRUE -DLY_PARALLEL_LINK_JOBS=4", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "all" } diff --git a/scripts/build/Platform/Mac/build_config.json b/scripts/build/Platform/Mac/build_config.json index 7eb3cb5699..b57cc522bc 100644 --- a/scripts/build/Platform/Mac/build_config.json +++ b/scripts/build/Platform/Mac/build_config.json @@ -37,7 +37,7 @@ "PARAMETERS": { "CONFIGURATION": "debug", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD" } @@ -51,7 +51,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD" } @@ -81,7 +81,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "AssetProcessorBatch", "ASSET_PROCESSOR_BINARY": "bin/profile/AssetProcessorBatch", @@ -99,7 +99,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_periodic", "CTEST_OPTIONS": "-L \"(SUITE_periodic)\"", @@ -116,7 +116,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_benchmark", "CTEST_OPTIONS": "-L \"(SUITE_benchmark)\"", @@ -133,7 +133,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD" } @@ -148,7 +148,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build/mono_mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_MONOLITHIC_GAME=TRUE -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode -DLY_MONOLITHIC_GAME=TRUE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD" } diff --git a/scripts/build/Platform/Windows/build_config.json b/scripts/build/Platform/Windows/build_config.json index de83294639..b185a845ec 100644 --- a/scripts/build/Platform/Windows/build_config.json +++ b/scripts/build/Platform/Windows/build_config.json @@ -99,7 +99,7 @@ "PARAMETERS": { "CONFIGURATION": "debug", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -113,7 +113,7 @@ "PARAMETERS": { "CONFIGURATION": "debug", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_smoke TEST_SUITE_main", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -131,7 +131,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE -DLY_TEST_IMPACT_INSTRUMENTATION_BIN=!TEST_IMPACT_WIN_BINARY!", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_TEST_IMPACT_INSTRUMENTATION_BIN=!TEST_IMPACT_WIN_BINARY!", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -162,7 +162,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_smoke TEST_SUITE_main", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -183,7 +183,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_smoke TEST_SUITE_main", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -203,7 +203,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "AssetProcessorBatch", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -234,7 +234,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_awsi", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -253,7 +253,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_periodic", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -275,7 +275,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_sandbox", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -294,7 +294,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "TEST_SUITE_benchmark", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo", @@ -314,7 +314,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -330,7 +330,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build\\mono_windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_MONOLITHIC_GAME=TRUE -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_MONOLITHIC_GAME=TRUE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -345,7 +345,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE -DLY_DISABLE_TEST_MODULES=TRUE", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_DISABLE_TEST_MODULES=TRUE", "CMAKE_LY_PROJECTS": "", "CMAKE_TARGET": "INSTALL", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -363,7 +363,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE -DLY_DISABLE_TEST_MODULES=TRUE -DLY_VERSION_ENGINE_NAME=o3de-sdk -DLY_INSTALLER_WIX_ROOT=\"!WIX! \"", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_DISABLE_TEST_MODULES=TRUE -DLY_VERSION_ENGINE_NAME=o3de-sdk -DLY_INSTALLER_WIX_ROOT=\"!WIX! \"", "EXTRA_CMAKE_OPTIONS": "-DLY_INSTALLER_AUTO_GEN_TAG=ON -DLY_INSTALLER_DOWNLOAD_URL=!INSTALLER_DOWNLOAD_URL! -DLY_INSTALLER_LICENSE_URL=!INSTALLER_DOWNLOAD_URL!/license", "CPACK_BUCKET": "!INSTALLER_BUCKET!", "CMAKE_LY_PROJECTS": "", @@ -382,7 +382,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE -DCMAKE_MODULE_PATH=!WORKSPACE!/o3de/cmake", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_MODULE_PATH=!WORKSPACE!/o3de/cmake", "CMAKE_LY_PROJECTS": "", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" @@ -397,7 +397,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build\\windows_vs2019", - "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE -DCMAKE_MODULE_PATH=!WORKSPACE!/o3de/install/cmake", + "CMAKE_OPTIONS": "-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DCMAKE_MODULE_PATH=!WORKSPACE!/o3de/install/cmake", "CMAKE_LY_PROJECTS": "", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" diff --git a/scripts/build/Platform/Windows/package_build_config.json b/scripts/build/Platform/Windows/package_build_config.json index 89431ad96e..5eb96f1e38 100644 --- a/scripts/build/Platform/Windows/package_build_config.json +++ b/scripts/build/Platform/Windows/package_build_config.json @@ -4,7 +4,7 @@ "PARAMETERS": { "CONFIGURATION":"profile", "OUTPUT_DIRECTORY":"windows_vs2019", - "CMAKE_OPTIONS":"-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0 -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS":"-G \"Visual Studio 16 2019\" -DCMAKE_SYSTEM_VERSION=10.0", "CMAKE_LY_PROJECTS":"AtomTest;AtomSampleViewer", "CMAKE_TARGET":"ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "/m /nologo" diff --git a/scripts/build/Platform/iOS/build_config.json b/scripts/build/Platform/iOS/build_config.json index 01f545ffb6..895f74daae 100644 --- a/scripts/build/Platform/iOS/build_config.json +++ b/scripts/build/Platform/iOS/build_config.json @@ -27,7 +27,7 @@ "PARAMETERS": { "CONFIGURATION": "debug", "OUTPUT_DIRECTORY": "build/ios", - "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=TRUE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=FALSE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=FALSE -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=TRUE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=FALSE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=FALSE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "-destination generic/platform=iOS" @@ -44,7 +44,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/ios", - "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=TRUE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=FALSE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=FALSE -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=TRUE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=FALSE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=FALSE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "-destination generic/platform=iOS" @@ -76,7 +76,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/mac", - "CMAKE_OPTIONS": "-G Xcode -DLY_UNITY_BUILD=TRUE -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", + "CMAKE_OPTIONS": "-G Xcode -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "AssetProcessorBatch", "ASSET_PROCESSOR_BINARY": "bin/profile/AssetProcessorBatch", @@ -94,7 +94,7 @@ "PARAMETERS": { "CONFIGURATION": "release", "OUTPUT_DIRECTORY": "build/ios", - "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=TRUE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=FALSE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=FALSE -DLY_UNITY_BUILD=TRUE", + "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=TRUE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=FALSE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=FALSE", "CMAKE_LY_PROJECTS": "AutomatedTesting", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "-destination generic/platform=iOS" @@ -112,7 +112,7 @@ "PARAMETERS": { "CONFIGURATION": "profile", "OUTPUT_DIRECTORY": "build/ios_test", - "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=FALSE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=TRUE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=TRUE -DLY_UNITY_BUILD=TRUE -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", + "CMAKE_OPTIONS": "-G Xcode -DCMAKE_TOOLCHAIN_FILE=cmake/Platform/iOS/Toolchain_ios.cmake -DLY_MONOLITHIC_GAME=FALSE -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_REQUIRED=TRUE -DLY_IOS_CODE_SIGNING_IDENTITY=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS=\"\" -DCMAKE_XCODE_ATTRIBUTE_CODE_SIGNING_ALLOWED=TRUE -DO3DE_HOME_PATH=\"${WORKSPACE}/home\" -DO3DE_REGISTER_ENGINE_PATH=\"${WORKSPACE}/o3de\" -DO3DE_REGISTER_THIS_ENGINE=TRUE", "CMAKE_TARGET": "ALL_BUILD", "CMAKE_NATIVE_BUILD_ARGS": "", "TARGET_DEVICE_NAME": "Lumberyard", diff --git a/scripts/commit_validation/commit_validation/commit_validation.py b/scripts/commit_validation/commit_validation/commit_validation.py index b9d992fc9c..513244a735 100755 --- a/scripts/commit_validation/commit_validation/commit_validation.py +++ b/scripts/commit_validation/commit_validation/commit_validation.py @@ -173,16 +173,12 @@ EXCLUDED_VALIDATION_PATTERNS = [ '*/3rdParty/*', '*/__pycache__/*', '*/External/*', - 'build', - 'Cache', - '*/Code/Framework/AzCore/azgnmx/azgnmx/*', - 'Code/Tools/CryFXC', - 'Code/Tools/HLSLCrossCompiler', - 'Code/Tools/HLSLCrossCompilerMETAL', - 'Docs', + 'build', # build artifacts + '*/Cache/*', # Asset processing artifacts + 'install', # install layout artifacts 'python/runtime', + 'restricted/*/Code/Framework/AzCore/azgnmx/azgnmx/*', 'restricted/*/Tools/*RemoteControl', - 'Tools/3dsmax', '*/user/Cache/*', '*/user/log/*', ] diff --git a/scripts/commit_validation/commit_validation/pal_allowedlist.txt b/scripts/commit_validation/commit_validation/pal_allowedlist.txt index 4c90bcb6b4..6fd5aab62e 100644 --- a/scripts/commit_validation/commit_validation/pal_allowedlist.txt +++ b/scripts/commit_validation/commit_validation/pal_allowedlist.txt @@ -23,7 +23,6 @@ */Code/Framework/AzCore/AzCore/std/parallel/binary_semaphore.h */Code/Framework/AzCore/AzCore/std/parallel/semaphore.h */Code/Framework/AzCore/Platform/Android/AzCore/AzCore_Traits_Android.h -*/Code/Framework/AzCore/Platform/AppleTV/AzCore/AzCore_Traits_AppleTV.h */Code/Framework/AzCore/Platform/iOS/AzCore/AzCore_Traits_iOS.h */Code/Framework/AzCore/Platform/Jasper/AzCore/AzCore_Traits_Jasper.h */Code/Framework/AzCore/Platform/Linux/AzCore/AzCore_Traits_Linux.h @@ -49,7 +48,6 @@ */Code/Tools/* */Gems/*/3rdParty/* */Gems/*/External/* -*/Gems/CryLegacy* */Gems/EMotionFX/Code/EMotionFX/Rendering/OpenGL2/Source/GLInclude.h */Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/MainWindow.cpp */Gems/EMotionFX/Code/EMotionFX/Tools/EMotionStudio/EMStudioSDK/Source/PluginManager.cpp @@ -61,4 +59,3 @@ */Gems/SaveData/Code/Tests/SaveDataTest.cpp */Gems/WhiteBox/Code/Source/Rendering/Legacy/WhiteBoxLegacyRenderMesh.cpp */restricted/*/Code/Framework/AzCore/AzCore/AzCore_Traits_*.h -*/Tools/CryDeprecation/precompile_check_defines.h diff --git a/scripts/o3de/o3de/repo.py b/scripts/o3de/o3de/repo.py index 9c26658a30..32c2cba428 100644 --- a/scripts/o3de/o3de/repo.py +++ b/scripts/o3de/o3de/repo.py @@ -115,14 +115,14 @@ def get_gem_json_paths_from_cached_repo(repo_uri: str) -> set: file_name = pathlib.Path(cache_filename).resolve() if not file_name.is_file(): logger.error(f'Could not find cached repo json file for {repo_uri}') - return gem_list + return gem_set with file_name.open('r') as f: try: repo_data = json.load(f) except json.JSONDecodeError as e: logger.error(f'{file_name} failed to load: {str(e)}') - return gem_list + return gem_set # Get list of gems, then add all json paths to the list if they exist in the cache repo_gems = [] diff --git a/scripts/scrubbing/validator_data_LEGAL_REVIEW_REQUIRED.py b/scripts/scrubbing/validator_data_LEGAL_REVIEW_REQUIRED.py index 666e04a2b0..0c008cbe65 100755 --- a/scripts/scrubbing/validator_data_LEGAL_REVIEW_REQUIRED.py +++ b/scripts/scrubbing/validator_data_LEGAL_REVIEW_REQUIRED.py @@ -100,14 +100,10 @@ def get_prohibited_platforms_for_package(package): def get_bypassed_directories(is_all): # Temporarily exempt folders to not fail validation while people is fixing validation errors, they will be removed once the errors are fixed. temp_bypass_directories = [ - 'commit_validation', - 'LauncherTestTools', - 'AutomatedTesting', - 'Atom' + 'commit_validation' ] bypassed_directories = [ - 'python', - 'AWSPythonSDK' + 'python' ] if not is_all: bypassed_directories.extend([ @@ -116,13 +112,7 @@ def get_bypassed_directories(is_all): 'Cache', 'logs', 'AssetProcessorTemp', - 'JenkinsScripts', - 'BuildLambdaFunctions', - 'layouts', - '.idea', 'user/log', - 'DirectXShaderCompiler', - 'v-hacd', 'External' ]) bypassed_directories.extend(temp_bypass_directories)