diff --git a/AutomatedTesting/Gem/PythonTests/AWS/README.md b/AutomatedTesting/Gem/PythonTests/AWS/README.md index 1bb36f178d..0d046cbe4c 100644 --- a/AutomatedTesting/Gem/PythonTests/AWS/README.md +++ b/AutomatedTesting/Gem/PythonTests/AWS/README.md @@ -8,11 +8,14 @@ ## Deploy CDK Applications 1. Go to the AWS IAM console and create an IAM role called o3de-automation-tests which adds your own account as as a trusted entity and uses the "AdministratorAccess" permissions policy. 2. Copy {engine_root}\scripts\build\Platform\Windows\deploy_cdk_applications.cmd to your engine root folder. -3. Open a new Command Prompt window at the engine root and set the following environment variables: +3. Open a new Command Prompt window at the engine root and set the following environment variables: +``` Set O3DE_AWS_PROJECT_NAME=AWSAUTO Set O3DE_AWS_DEPLOY_REGION=us-east-1 + Set O3DE_AWS_DEPLOY_ACCOUNT={your_aws_account_id} Set ASSUME_ROLE_ARN=arn:aws:iam::{your_aws_account_id}:role/o3de-automation-tests Set COMMIT_ID=HEAD +``` 4. In the same Command Prompt window, Deploy the CDK applications for AWS gems by running deploy_cdk_applications.cmd. ## Run Automation Tests diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py index 381f266fab..23fb249761 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Main_GPU.py @@ -89,7 +89,7 @@ class TestAllComponentsIndepthTests(object): level_creation_expected_lines = [ "Viewport is set to the expected size: True", - "Basic level created" + "Exited game mode" ] unexpected_lines = [ "Trace::Assert", @@ -189,8 +189,8 @@ class TestPerformanceBenchmarkSuite(object): "Benchmark metadata captured.", "Pass timestamps captured.", "CPU frame time captured.", - "Capturing complete.", - "Captured data successfully." + "Captured data successfully.", + "Exited game mode" ] unexpected_lines = [ diff --git a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py index 9bb7f9c50e..ad45e51080 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/TestSuite_Sandbox.py @@ -14,10 +14,6 @@ import editor_python_test_tools.hydra_test_utils as hydra logger = logging.getLogger(__name__) TEST_DIRECTORY = os.path.join(os.path.dirname(__file__), "tests") - -@pytest.mark.parametrize("project", ["AutomatedTesting"]) -@pytest.mark.parametrize("launcher_platform", ['windows_editor']) -@pytest.mark.parametrize("level", ["auto_test"]) class TestAtomEditorComponentsSandbox(object): # It requires at least one test diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py index fbd9f3459a..de7c9d2326 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_AtomFeatureIntegrationBenchmark.py @@ -90,7 +90,6 @@ def run(): benchmarker.capture_cpu_frame_time(i) general.exit_game_mode() helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0) - general.log("Capturing complete.") if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py index 93e78a7c0a..9515712583 100644 --- a/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py +++ b/AutomatedTesting/Gem/PythonTests/Atom/tests/hydra_GPUTest_BasicLevelSetup.py @@ -215,7 +215,6 @@ def run(): ScreenshotHelper(general.idle_wait_frames).capture_screenshot_blocking(f"{'AtomBasicLevelSetup'}.ppm") general.exit_game_mode() helper.wait_for_condition(function=lambda: not general.is_in_game_mode(), timeout_in_seconds=2.0) - general.log("Basic level created") if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt index 2ca7f7ea2b..fd3222ba83 100644 --- a/AutomatedTesting/Gem/PythonTests/CMakeLists.txt +++ b/AutomatedTesting/Gem/PythonTests/CMakeLists.txt @@ -58,3 +58,6 @@ add_subdirectory(smoke) ## AWS ## add_subdirectory(AWS) + +## Integration tests for editor testing framework ## +add_subdirectory(editor_test_testing) 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 2d8b124125..10a6ab1ef4 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 @@ -9,6 +9,7 @@ from __future__ import annotations from collections import Counter from collections import deque from os import path +from pathlib import Path from PySide2 import QtWidgets @@ -20,6 +21,10 @@ from editor_python_test_tools.utils import Report import azlmbr.entity as entity import azlmbr.bus as bus +import azlmbr.components as components +import azlmbr.editor as editor +import azlmbr.globals +import azlmbr.math as math import azlmbr.prefab as prefab import editor_python_test_tools.pyside_utils as pyside_utils @@ -57,26 +62,46 @@ class PrefabInstance: def __hash__(self): return hash(self.container_entity.id) - """ - See if this instance is valid to be used with other prefab operations. - :return: Whether the target instance is valid or not. - """ def is_valid(self) -> bool: + """ + See if this instance is valid to be used with other prefab operations. + :return: Whether the target instance is valid or not. + """ return self.container_entity.id.IsValid() and self.prefab_file_name in Prefab.existing_prefabs - """ - Reparent this instance to target parent entity. - The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. - :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. - """ + def has_editor_prefab_component(self) -> bool: + """ + Check if the instance's container entity contains EditorPrefabComponent. + :return: Whether the container entity of target instance has EditorPrefabComponent in it or not. + """ + return editor.EditorComponentAPIBus(bus.Broadcast, "HasComponentOfType", self.container_entity.id, azlmbr.globals.property.EditorPrefabComponentTypeId) + + def is_at_position(self, expected_position): + """ + Check if the instance's container entity is at expected position given. + :return: Whether the container entity of target instance is at expected position or not. + """ + actual_position = components.TransformBus(bus.Event, "GetWorldTranslation", self.container_entity.id) + is_at_position = actual_position.IsClose(expected_position) + + if not is_at_position: + Report.info(f"Prefab Instance Container Entity '{self.container_entity.id.ToString()}'\'s expected position: {expected_position.ToString()}, actual position: {actual_position.ToString()}") + + return is_at_position + async def ui_reparent_prefab_instance(self, parent_entity_id: EntityId): + """ + Reparent this instance to target parent entity. + The function will also check pop up dialog ui in editor to see if there's prefab cyclical dependency error while reparenting prefabs. + :param parent_entity_id: The id of the entity this instance should be a child of in the transform hierarchy next. + """ container_entity_id_before_reparent = self.container_entity.id original_parent = EditorEntity(self.container_entity.get_parent_id()) - original_parent_before_reparent_children_ids = set(original_parent.get_children_ids()) + original_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} new_parent = EditorEntity(parent_entity_id) - new_parent_before_reparent_children_ids = set(new_parent.get_children_ids()) + new_parent_before_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} pyside_utils.run_soon(lambda: self.container_entity.set_parent_entity(parent_entity_id)) pyside_utils.run_soon(lambda: wait_for_propagation()) @@ -90,23 +115,28 @@ class PrefabInstance: except pyside_utils.EventLoopTimeoutException: pass - original_parent_after_reparent_children_ids = set(original_parent.get_children_ids()) + original_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in original_parent.get_children_ids()} assert len(original_parent_after_reparent_children_ids) == len(original_parent_before_reparent_children_ids) - 1, \ "The children count of the Prefab Instance's original parent should be decreased by 1." assert not container_entity_id_before_reparent in original_parent_after_reparent_children_ids, \ "This Prefab Instance is still a child entity of its original parent entity." - new_parent_after_reparent_children_ids = set(new_parent.get_children_ids()) + new_parent_after_reparent_children_ids = {child_id.ToString(): child_id for child_id in new_parent.get_children_ids()} assert len(new_parent_after_reparent_children_ids) == len(new_parent_before_reparent_children_ids) + 1, \ "The children count of the Prefab Instance's new parent should be increased by 1." - container_entity_id_after_reparent = set(new_parent_after_reparent_children_ids).difference(new_parent_before_reparent_children_ids).pop() + after_before_diff = set(new_parent_after_reparent_children_ids.keys()).difference(set(new_parent_before_reparent_children_ids.keys())) + container_entity_id_after_reparent = new_parent_after_reparent_children_ids[after_before_diff.pop()] reparented_container_entity = EditorEntity(container_entity_id_after_reparent) reparented_container_entity_parent_id = reparented_container_entity.get_parent_id() has_correct_parent = reparented_container_entity_parent_id.ToString() == parent_entity_id.ToString() assert has_correct_parent, "Prefab Instance reparented is *not* under the expected parent entity" + current_instance_prefab = Prefab.get_prefab(self.prefab_file_name) + current_instance_prefab.instances.remove(self) + self.container_entity = reparented_container_entity + current_instance_prefab.instances.add(self) # This is a helper class which contains some of the useful information about a prefab template. class Prefab: @@ -117,31 +147,32 @@ class Prefab: self.file_path: str = get_prefab_file_path(file_path) self.instances: set[PrefabInstance] = set() - """ - Check if a prefab is ready to be used to generate its instances. - :param file_path: A unique file path of the target prefab. - :return: Whether the target prefab is loaded or not. - """ @classmethod def is_prefab_loaded(cls, file_path: str) -> bool: + """ + Check if a prefab is ready to be used to generate its instances. + :param file_path: A unique file path of the target prefab. + :return: Whether the target prefab is loaded or not. + """ return file_path in Prefab.existing_prefabs - """ - Check if a prefab exists in the directory for files of prefab tests. - :param file_name: A unique file name of the target prefab. - :return: Whether the target prefab exists or not. - """ + @classmethod def prefab_exists(cls, file_path: str) -> bool: + """ + Check if a prefab exists in the directory for files of prefab tests. + :param file_name: A unique file name of the target prefab. + :return: Whether the target prefab exists or not. + """ return path.exists(get_prefab_file_path(file_path)) - """ - Return a prefab which can be used immediately. - :param file_name: A unique file name of the target prefab. - :return: The prefab with given file name. - """ @classmethod def get_prefab(cls, file_name: str) -> Prefab: + """ + Return a prefab which can be used immediately. + :param file_name: A unique file name of the target prefab. + :return: The prefab with given file name. + """ assert file_name, "Received an empty file_name" if Prefab.is_prefab_loaded(file_name): return Prefab.existing_prefabs[file_name] @@ -151,15 +182,15 @@ class Prefab: Prefab.existing_prefabs[file_name] = Prefab(file_name) return new_prefab - """ - Create a prefab in memory and return it. The very first instance of this prefab will also be created. - :param entities: The entities that should form the new prefab (along with their descendants). - :param file_name: A unique file name of new prefab. - :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. - :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. - """ @classmethod def create_prefab(cls, entities: list[EditorEntity], file_name: str, prefab_instance_name: str=None) -> tuple(Prefab, PrefabInstance): + """ + Create a prefab in memory and return it. The very first instance of this prefab will also be created. + :param entities: The entities that should form the new prefab (along with their descendants). + :param file_name: A unique file name of new prefab. + :param prefab_instance_name: A name for the very first instance generated while prefab creation. The default instance name is the same as file_name. + :return: Created Prefab object and the very first PrefabInstance object owned by the prefab. + """ assert not Prefab.is_prefab_loaded(file_name), f"Can't create Prefab '{file_name}' since the prefab already exists" new_prefab = Prefab(file_name) @@ -169,6 +200,9 @@ class Prefab: container_entity_id = create_prefab_result.GetValue() container_entity = EditorEntity(container_entity_id) + children_entity_ids = container_entity.get_children_ids() + + assert len(children_entity_ids) == len(entities), f"Entity count of created prefab instance does *not* match the count of given entities." if prefab_instance_name: container_entity.set_name(prefab_instance_name) @@ -180,12 +214,12 @@ class Prefab: Prefab.existing_prefabs[file_name] = new_prefab return new_prefab, new_prefab_instance - """ - Remove target prefab instances. - :param prefab_instances: Instances to be removed. - """ @classmethod def remove_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Remove target prefab instances. + :param prefab_instances: Instances to be removed. + """ entity_ids_to_remove = [] entity_id_queue = [prefab_instance.container_entity for prefab_instance in prefab_instances] while entity_id_queue: @@ -212,15 +246,89 @@ class Prefab: instance_deleted_prefab = Prefab.get_prefab(instance.prefab_file_name) instance_deleted_prefab.instances.remove(instance) instance = PrefabInstance() - - """ - Instantiate an instance of this prefab. - :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. - :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. - :param prefab_position: The position in world space the prefab should be instantiated in. - :return: Instantiated PrefabInstance object owned by this prefab. - """ + + @classmethod + def duplicate_prefabs(cls, prefab_instances: list[PrefabInstance]): + """ + Duplicate target prefab instances. + :param prefab_instances: Instances to be duplicated. + :return: PrefabInstance objects of given prefab instances' duplicates. + """ + assert prefab_instances, "Input list of prefab instances should *not* be empty." + + common_parent = EditorEntity(prefab_instances[0].container_entity.get_parent_id()) + common_parent_children_ids_before_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + container_entity_ids = [prefab_instance.container_entity.id for prefab_instance in prefab_instances] + + duplicate_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DuplicateEntitiesInInstance', container_entity_ids) + assert duplicate_prefab_result.IsSuccess(), f"Prefab operation 'DuplicateEntitiesInInstance' failed. Error: {duplicate_prefab_result.GetError()}" + + wait_for_propagation() + + duplicate_container_entity_ids = duplicate_prefab_result.GetValue() + common_parent_children_ids_after_duplicate = set([child_id.ToString() for child_id in common_parent.get_children_ids()]) + + assert set([container_entity_id.ToString() for container_entity_id in container_entity_ids]).issubset(common_parent_children_ids_after_duplicate), \ + "Provided prefab instances are *not* the children of their common parent anymore after duplication." + assert common_parent_children_ids_before_duplicate.issubset(common_parent_children_ids_after_duplicate), \ + "Some children of provided entities' common parent before duplication are *not* the children of the common parent anymore after duplication." + assert len(common_parent_children_ids_after_duplicate) == len(common_parent_children_ids_before_duplicate) + len(prefab_instances), \ + "The children count of the given prefab instances' common parent entity is *not* increased to the expected number." + assert EditorEntity(duplicate_container_entity_ids[0]).get_parent_id().ToString() == common_parent.id.ToString(), \ + "Provided prefab instances' parent should be the same as duplicates' parent." + + duplicate_instances = [] + for duplicate_container_entity_id in duplicate_container_entity_ids: + prefab_file_path = prefab.PrefabPublicRequestBus(bus.Broadcast, 'GetOwningInstancePrefabPath', duplicate_container_entity_id) + assert prefab_file_path, "Returned file path should *not* be empty." + + prefab_file_name = Path(prefab_file_path).stem + duplicate_instance_prefab = Prefab.get_prefab(prefab_file_name) + duplicate_instance = PrefabInstance(prefab_file_path, EditorEntity(duplicate_container_entity_id)) + duplicate_instance_prefab.instances.add(duplicate_instance) + duplicate_instances.append(duplicate_instance) + + return duplicate_instances + + @classmethod + def detach_prefab(cls, prefab_instance: PrefabInstance): + """ + Detach target prefab instance. + :param prefab_instances: Instance to be detached. + """ + parent = EditorEntity(prefab_instance.container_entity.get_parent_id()) + parent_children_ids_before_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.has_editor_prefab_component(), f"Container entity should have EditorPrefabComponent before detachment." + + detach_prefab_result = prefab.PrefabPublicRequestBus(bus.Broadcast, 'DetachPrefab', prefab_instance.container_entity.id) + assert detach_prefab_result.IsSuccess(), f"Prefab operation 'DetachPrefab' failed. Error: {detach_prefab_result.GetError()}" + + assert not prefab_instance.has_editor_prefab_component(), f"Container entity should *not* have EditorPrefabComponent after detachment." + + parent_children_ids_after_detach = set([child_id.ToString() for child_id in parent.get_children_ids()]) + + assert prefab_instance.container_entity.id.ToString() in parent_children_ids_after_detach, \ + "Target prefab instance's container entity id should still exists after the detachment and before the propagation." + + assert len(parent_children_ids_after_detach) == len(parent_children_ids_before_detach), \ + "Parent entity should still keep the same amount of children entities." + + wait_for_propagation() + + instance_owner_prefab = Prefab.get_prefab(prefab_instance.prefab_file_name) + instance_owner_prefab.instances.remove(prefab_instance) + prefab_instance = PrefabInstance() + def instantiate(self, parent_entity: EditorEntity=None, name: str=None, prefab_position: Vector3=Vector3()) -> PrefabInstance: + """ + Instantiate an instance of this prefab. + :param parent_entity: The entity the prefab should be a child of in the transform hierarchy. + :param name: A name for newly instantiated prefab instance. The default instance name is the same as this prefab's file name. + :param prefab_position: The position in world space the prefab should be instantiated in. + :return: Instantiated PrefabInstance object owned by this prefab. + """ parent_entity_id = parent_entity.id if parent_entity is not None else EntityId() instantiate_prefab_result = prefab.PrefabPublicRequestBus( @@ -240,4 +348,6 @@ class Prefab: assert not new_prefab_instance in self.instances, "This prefab instance is already existed before this instantiation." self.instances.add(new_prefab_instance) + assert new_prefab_instance.is_at_position(prefab_position), "This prefab instance is *not* at expected position." + return new_prefab_instance diff --git a/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py b/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py index a3fd5c741f..61c2816f50 100755 --- a/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py +++ b/AutomatedTesting/Gem/PythonTests/Physics/TestSuite_Utils.py @@ -9,20 +9,17 @@ import pytest import sys import ly_test_tools.environment.file_system as fs -from .FileManagement import FileManagement as fm +from .utils.FileManagement import FileManagement as fm sys.path.append(os.path.dirname(os.path.abspath(__file__)) + '/../automatedtesting_shared') -from .base import TestAutomationBase +from base import TestAutomationBase - -@pytest.mark.parametrize("platform", ["win_x64_vs2017"]) -@pytest.mark.parametrize("configuration", ["profile"]) -@pytest.mark.parametrize("spec", ["all"]) +@pytest.mark.parametrize("launcher_platform", ['windows_editor']) @pytest.mark.parametrize("project", ["AutomatedTesting"]) class TestUtils(TestAutomationBase): @fm.file_revert("UtilTest_Physmaterial_Editor_TestLibrary.physmaterial", r"AutomatedTesting\Levels\Physics\Physmaterial_Editor_Test") - def test_physmaterial_editor(self, request, workspace, editor): + def test_physmaterial_editor(self, request, workspace, launcher_platform, editor): """ Tests functionality of physmaterial editing utility :param workspace: Fixture containing platform and project detail @@ -35,11 +32,11 @@ class TestUtils(TestAutomationBase): unexpected_lines = ["Assert"] self._run_test(request, workspace, editor, physmaterial_editor_test_module, expected_lines, unexpected_lines) - def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, editor): + def test_UtilTest_Tracer_PicksErrorsAndWarnings(self, request, workspace, launcher_platform, editor): from .utils import UtilTest_Tracer_PicksErrorsAndWarnings as testcase_module self._run_test(request, workspace, editor, testcase_module, [], []) - def test_FileManagement_FindingFiles(self, workspace): + def test_FileManagement_FindingFiles(self, workspace, launcher_platform): """ Tests the functionality of "searching for files" with FileManagement._find_files() :param workspace: ly_test_tools workspace fixture @@ -110,7 +107,7 @@ class TestUtils(TestAutomationBase): find_me_too_path, found_me["FindMeToo.txt"] ) - def test_FileManagement_FileBackup(self, workspace): + def test_FileManagement_FileBackup(self, workspace, launcher_platform): """ Tests the functionality of the file back up system via the FileManagement class :param workspace: ly_test_tools workspace fixture @@ -167,7 +164,7 @@ class TestUtils(TestAutomationBase): del file_map[target_file_path] fm._save_file_map(file_map) - def test_FileManagement_FileRestoration(self, workspace): + def test_FileManagement_FileRestoration(self, workspace, launcher_platform): """ Tests the restore file system via the FileManagement class :param workspace: ly_test_tools workspace fixture @@ -261,7 +258,7 @@ class TestUtils(TestAutomationBase): ["FindMe.txt", "FindMeToo.txt"], parent_path=r"AutomatedTesting\levels\Utils\Managed_files", search_subdirs=True ) @fm.file_override("default.physxconfiguration", "UtilTest_PhysxConfig_Override.physxconfiguration") - def test_UtilTest_Managed_Files(self, request, workspace, editor): + def test_UtilTest_Managed_Files(self, request, workspace, editor, launcher_platform): from .utils import UtilTest_Managed_Files as test_module expected_lines = [] diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py index 30a0055d5a..5337f0669c 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/TestSuite_Main.py @@ -47,3 +47,11 @@ class TestAutomation(TestAutomationBase): def test_PrefabBasicWorkflow_CreateAndReparentPrefab(self, request, workspace, editor, launcher_platform): from .tests import PrefabBasicWorkflow_CreateAndReparentPrefab as test_module self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + def test_PrefabBasicWorkflow_CreateReparentAndDetachPrefab(self, request, workspace, editor, launcher_platform): + from .tests import PrefabBasicWorkflow_CreateReparentAndDetachPrefab as test_module + self._run_prefab_test(request, workspace, editor, test_module, autotest_mode=False) + + 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) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py index 9ae4614d80..bbebd70e04 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDeletePrefab.py @@ -16,16 +16,15 @@ def PrefabBasicWorkflow_CreateAndDeletePrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Asserts if prefab creation doesn't succeeds + # Creates a prefab from the new entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Asserts if prefab deletion fails + # Deletes the prefab instance Prefab.remove_prefabs([car]) if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py new file mode 100644 index 0000000000..2479ae549e --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndDuplicatePrefab.py @@ -0,0 +1,32 @@ +""" +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 PrefabBasicWorkflow_CreateAndDuplicatePrefab(): + + 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 + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the new entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Duplicates the prefab instance + Prefab.duplicate_prefabs([car]) + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateAndDuplicatePrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py index f78bbf483d..1cbc591c29 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateAndReparentPrefab.py @@ -22,24 +22,23 @@ def PrefabBasicWorkflow_CreateAndReparentPrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new car entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the car entity _, car = Prefab.create_prefab( car_prefab_entities, CAR_PREFAB_FILE_NAME) - # Creates another new Entity at the root level + # Creates another new wheel entity at the root level wheel_entity = EditorEntity.create_editor_entity() wheel_prefab_entities = [wheel_entity] - # Checks for wheel prefab creation passed or not + # Creates another prefab from the wheel entity _, wheel = Prefab.create_prefab( wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) - # Checks for prefab reparenting passed or not + # Reparents the wheel prefab instance to the container entity of the car prefab instance await wheel.ui_reparent_prefab_instance(car.container_entity.id) run_test() diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py index 568a2c15b4..cae105a9a9 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreatePrefab.py @@ -17,12 +17,11 @@ def PrefabBasicWorkflow_CreatePrefab(): prefab_test_utils.open_base_tests_level() - # Creates a new Entity at the root level - # Asserts if creation didn't succeed + # Creates a new entity at the root level car_entity = EditorEntity.create_editor_entity() car_prefab_entities = [car_entity] - # Checks for prefab creation passed or not + # Creates a prefab from the new entity Prefab.create_prefab(car_prefab_entities, CAR_PREFAB_FILE_NAME) if __name__ == "__main__": diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py new file mode 100644 index 0000000000..bdf77c4bf3 --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_CreateReparentAndDetachPrefab.py @@ -0,0 +1,51 @@ +""" +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 PrefabBasicWorkflow_CreateReparentAndDetachPrefab(): + + CAR_PREFAB_FILE_NAME = 'car_prefab' + WHEEL_PREFAB_FILE_NAME = 'wheel_prefab' + + import editor_python_test_tools.pyside_utils as pyside_utils + + @pyside_utils.wrap_async + async def run_test(): + + 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 car entity at the root level + car_entity = EditorEntity.create_editor_entity() + car_prefab_entities = [car_entity] + + # Creates a prefab from the car entity + _, car = Prefab.create_prefab( + car_prefab_entities, CAR_PREFAB_FILE_NAME) + + # Creates another new wheel entity at the root level + wheel_entity = EditorEntity.create_editor_entity() + wheel_prefab_entities = [wheel_entity] + + # Creates another prefab from the wheel entity + _, wheel = Prefab.create_prefab( + wheel_prefab_entities, WHEEL_PREFAB_FILE_NAME) + + # Reparents the wheel prefab instance to the container entity of the car prefab instance + await wheel.ui_reparent_prefab_instance(car.container_entity.id) + + # Detaches the wheel prefab instance + Prefab.detach_prefab(wheel) + + run_test() + +if __name__ == "__main__": + from editor_python_test_tools.utils import Report + Report.start_test(PrefabBasicWorkflow_CreateReparentAndDetachPrefab) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py index 1b962d2ca7..a701802cd4 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabBasicWorkflow_InstantiatePrefab.py @@ -19,9 +19,8 @@ def PrefabBasicWorkflow_InstantiatePrefab(): prefab_test_utils.open_base_tests_level() - # Checks for prefab instantiation passed or not + # Instantiates a new car prefab instance test_prefab = Prefab.get_prefab(EXISTING_TEST_PREFAB_FILE_NAME) - test_instance = test_prefab.instantiate( prefab_position=INSTANTIATED_TEST_PREFAB_POSITION) diff --git a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py index f82af23023..f865daf41a 100644 --- a/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py +++ b/AutomatedTesting/Gem/PythonTests/Prefab/tests/PrefabTestUtils.py @@ -18,20 +18,6 @@ import azlmbr.components as components import azlmbr.entity as entity import azlmbr.legacy.general as general -def check_entity_at_position(entity_id, expected_entity_position): - entity_at_expected_position_result = ( - "entity is at expected position", - "entity is *not* at expected position") - - actual_entity_position = components.TransformBus(bus.Event, "GetWorldTranslation", entity_id) - is_at_position = actual_entity_position.IsClose(expected_entity_position) - Report.result(entity_at_expected_position_result, is_at_position) - - if not is_at_position: - Report.info(f"Entity '{entity_id.ToString()}'\'s expected position: {expected_entity_position.ToString()}, actual position: {actual_entity_position.ToString()}") - - return is_at_position - def check_entity_children_count(entity_id, expected_children_count): entity_children_count_matched_result = ( "Entity with a unique name found", @@ -47,19 +33,6 @@ def check_entity_children_count(entity_id, expected_children_count): return entity_children_count_matched -def get_children_ids_by_name(entity_id, entity_name): - entity = EditorEntity(entity_id) - children_entity_ids = entity.get_children_ids() - - result = [] - for child_entity_id in children_entity_ids: - child_entity = EditorEntity(child_entity_id) - child_entity_name = child_entity.get_name() - if child_entity_name == entity_name: - result.append(child_entity_id) - - return result - def open_base_tests_level(): helper.init_idle() helper.open_level("Prefab", "Base") diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt b/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt new file mode 100644 index 0000000000..dbf26d66cd --- /dev/null +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/CMakeLists.txt @@ -0,0 +1,24 @@ +# +# 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 timeouts on jenkins, investigation is needed. Commment for now +# +#if(PAL_TRAIT_BUILD_TESTS_SUPPORTED AND PAL_TRAIT_BUILD_HOST_TOOLS) +# ly_add_pytest( +# NAME AutomatedTesting::EditorTestTesting +# TEST_SUITE main +# TEST_SERIAL +# PATH ${CMAKE_CURRENT_LIST_DIR}/TestSuite_Main.py +# RUNTIME_DEPENDENCIES +# Legacy::Editor +# AZ::AssetProcessor +# AutomatedTesting.Assets +# COMPONENT +# TestTools +# ) +#endif() diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py index 7c7e063951..15ba6690a1 100644 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py +++ b/AutomatedTesting/Gem/PythonTests/editor_test_testing/TestSuite_Main.py @@ -35,10 +35,11 @@ class TestEditorTest: @classmethod def setup_class(cls): TestEditorTest.args = sys.argv.copy() - build_dir_arg_index = TestEditorTest.args.index("--build-directory") - if build_dir_arg_index < 0: - print("Error: Must pass --build-directory argument in order to run this test") - sys.exit(-2) + build_dir_arg_index = -1 + try: + build_dir_arg_index = TestEditorTest.args.index("--build-directory") + except ValueError as ex: + raise ValueError("Must pass --build-directory argument in order to run this test") TestEditorTest.args[build_dir_arg_index+1] = os.path.abspath(TestEditorTest.args[build_dir_arg_index+1]) TestEditorTest.args.append("-s") diff --git a/Code/Framework/AzCore/AzCore/Utils/Utils.cpp b/Code/Framework/AzCore/AzCore/Utils/Utils.cpp index 2031d14d08..e6bfd78806 100644 --- a/Code/Framework/AzCore/AzCore/Utils/Utils.cpp +++ b/Code/Framework/AzCore/AzCore/Utils/Utils.cpp @@ -120,7 +120,7 @@ namespace AZ::Utils AZ::Outcome WriteFile(AZStd::string_view content, AZStd::string_view filePath) { AZ::IO::FixedMaxPath filePathFixed = filePath; // Because FileIOStream requires a null-terminated string - AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite); + AZ::IO::FileIOStream stream(filePathFixed.c_str(), AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath); bool success = false; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h new file mode 100644 index 0000000000..73523ee1ba --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/HeightfieldProviderBus.h @@ -0,0 +1,97 @@ +/* + * 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 Physics +{ + //! The QuadMeshType specifies the property of the heightfield quad. + enum class QuadMeshType : uint8_t + { + SubdivideUpperLeftToBottomRight, //!< Subdivide the quad, from upper left to bottom right |\|, into two triangles. + SubdivideBottomLeftToUpperRight, //!< Subdivide the quad, from bottom left to upper right |/|, into two triangles. + Hole //!< The quad should be treated as a hole in the heightfield. + }; + + struct HeightMaterialPoint + { + float m_height{ 0.0f }; //!< Holds the height of this point in the heightfield relative to the heightfield entity location. + QuadMeshType m_quadMeshType{ QuadMeshType::SubdivideUpperLeftToBottomRight }; //!< By default, create two triangles like this |\|, where this point is in the upper left corner. + uint8_t m_materialIndex{ 0 }; //!< The surface material index for the upper left corner of this quad. + uint16_t m_padding{ 0 }; //!< available for future use. + }; + + //! An interface to provide heightfield values. + class HeightfieldProviderRequests + : public AZ::ComponentBus + { + public: + //! Returns the distance between each height in the map. + //! @return Vector containing Column Spacing, Rows Spacing. + virtual AZ::Vector2 GetHeightfieldGridSpacing() const = 0; + + //! Returns the height field gridsize. + //! @param numColumns contains the size of the grid in the x direction. + //! @param numRows contains the size of the grid in the y direction. + virtual void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const = 0; + + //! Returns the height field min and max height bounds. + //! @param minHeightBounds contains the minimum height that the heightfield can contain. + //! @param maxHeightBounds contains the maximum height that the heightfield can contain. + virtual void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const = 0; + + //! Returns the AABB of the heightfield. + //! This is provided separately from the shape AABB because the heightfield might choose to modify the AABB bounds. + //! @return AABB of the heightfield. + virtual AZ::Aabb GetHeightfieldAabb() const = 0; + + //! Returns the world transform for the heightfield. + //! This is provided separately from the entity transform because the heightfield might want to clear out the rotation or scale. + //! @return world transform that should be used with the heightfield data. + virtual AZ::Transform GetHeightfieldTransform() const = 0; + + //! Returns the list of materials used by the height field. + //! @return returns a vector of all materials. + virtual AZStd::vector GetMaterialList() const = 0; + + //! Returns the list of heights used by the height field. + //! @return the rows*columns vector of the heights. + virtual AZStd::vector GetHeights() const = 0; + + //! Returns the list of heights and materials used by the height field. + //! @return the rows*columns vector of the heights and materials. + virtual AZStd::vector GetHeightsAndMaterials() const = 0; + }; + + using HeightfieldProviderRequestsBus = AZ::EBus; + + //! Broadcasts notifications when heightfield data changes - heightfield providers implement HeightfieldRequests bus. + class HeightfieldProviderNotifications + : public AZ::ComponentBus + { + public: + static const AZ::EBusHandlerPolicy HandlerPolicy = AZ::EBusHandlerPolicy::Multiple; + + //! Called whenever the heightfield data changes. + //! @param the AABB of the area of data that changed. + virtual void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + } + + protected: + ~HeightfieldProviderNotifications() = default; + }; + + using HeightfieldProviderNotificationBus = AZ::EBus; +} // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h b/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h new file mode 100644 index 0000000000..221c52258d --- /dev/null +++ b/Code/Framework/AzFramework/AzFramework/Physics/Mocks/MockHeightfieldProviderBus.h @@ -0,0 +1,33 @@ +/* + * 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 + +namespace UnitTest +{ + class MockHeightfieldProviderNotificationBusListener + : private Physics::HeightfieldProviderNotificationBus::Handler + { + public: + MockHeightfieldProviderNotificationBusListener(AZ::EntityId entityid) + { + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityid); + } + + ~MockHeightfieldProviderNotificationBusListener() + { + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + } + + MOCK_METHOD1(OnHeightfieldDataChanged, void(const AZ::Aabb&)); + }; +} // namespace UnitTest diff --git a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp index e90c9d4eed..9bf00d9707 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.cpp @@ -37,6 +37,7 @@ namespace Physics REFLECT_SHAPETYPE_ENUM_VALUE(Sphere); REFLECT_SHAPETYPE_ENUM_VALUE(Cylinder); REFLECT_SHAPETYPE_ENUM_VALUE(PhysicsAsset); + REFLECT_SHAPETYPE_ENUM_VALUE(Heightfield); #undef REFLECT_SHAPETYPE_ENUM_VALUE } @@ -305,4 +306,125 @@ namespace Physics m_cachedNativeMesh = nullptr; } } + + void HeightfieldShapeConfiguration::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext + ->RegisterGenericType>(); + + serializeContext->Class() + ->Version(1); + } + } + + HeightfieldShapeConfiguration::~HeightfieldShapeConfiguration() + { + SetCachedNativeHeightfield(nullptr); + } + + HeightfieldShapeConfiguration::HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration& other) + : ShapeConfiguration(other) + , m_gridResolution(other.m_gridResolution) + , m_numColumns(other.m_numColumns) + , m_numRows(other.m_numRows) + , m_samples(other.m_samples) + , m_minHeightBounds(other.m_minHeightBounds) + , m_maxHeightBounds(other.m_maxHeightBounds) + , m_cachedNativeHeightfield(nullptr) + { + } + + HeightfieldShapeConfiguration& HeightfieldShapeConfiguration::operator=(const HeightfieldShapeConfiguration& other) + { + ShapeConfiguration::operator=(other); + + m_gridResolution = other.m_gridResolution; + m_numColumns = other.m_numColumns; + m_numRows = other.m_numRows; + m_samples = other.m_samples; + m_minHeightBounds = other.m_minHeightBounds; + m_maxHeightBounds = other.m_maxHeightBounds; + + // Prevent raw pointer from being copied + m_cachedNativeHeightfield = nullptr; + + return *this; + } + + void* HeightfieldShapeConfiguration::GetCachedNativeHeightfield() const + { + return m_cachedNativeHeightfield; + } + + void HeightfieldShapeConfiguration::SetCachedNativeHeightfield(void* cachedNativeHeightfield) const + { + if (m_cachedNativeHeightfield) + { + Physics::SystemRequestBus::Broadcast(&Physics::SystemRequests::ReleaseNativeHeightfieldObject, m_cachedNativeHeightfield); + } + + m_cachedNativeHeightfield = cachedNativeHeightfield; + } + + AZ::Vector2 HeightfieldShapeConfiguration::GetGridResolution() const + { + return m_gridResolution; + } + + void HeightfieldShapeConfiguration::SetGridResolution(const AZ::Vector2& gridResolution) + { + m_gridResolution = gridResolution; + } + + int32_t HeightfieldShapeConfiguration::GetNumColumns() const + { + return m_numColumns; + } + + void HeightfieldShapeConfiguration::SetNumColumns(int32_t numColumns) + { + m_numColumns = numColumns; + } + + int32_t HeightfieldShapeConfiguration::GetNumRows() const + { + return m_numRows; + } + + void HeightfieldShapeConfiguration::SetNumRows(int32_t numRows) + { + m_numRows = numRows; + } + + const AZStd::vector& HeightfieldShapeConfiguration::GetSamples() const + { + return m_samples; + } + + void HeightfieldShapeConfiguration::SetSamples(const AZStd::vector& samples) + { + m_samples = samples; + } + + float HeightfieldShapeConfiguration::GetMinHeightBounds() const + { + return m_minHeightBounds; + } + + void HeightfieldShapeConfiguration::SetMinHeightBounds(float minBounds) + { + m_minHeightBounds = minBounds; + } + + float HeightfieldShapeConfiguration::GetMaxHeightBounds() const + { + return m_maxHeightBounds; + } + + void HeightfieldShapeConfiguration::SetMaxHeightBounds(float maxBounds) + { + m_maxHeightBounds = maxBounds; + } } diff --git a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h index b40a8b1edd..3bcf336af6 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/ShapeConfiguration.h @@ -9,10 +9,13 @@ #pragma once #include +#include #include #include #include +#include + namespace Physics { /// Used to identify shape configuration type from base class. @@ -27,6 +30,7 @@ namespace Physics Native, ///< Native shape configuration if user wishes to bypass generic shape configurations. PhysicsAsset, ///< Shapes configured in the asset. CookedMesh, ///< Stores a blob of mesh data cooked for the specific engine. + Heightfield ///< Interacts with the physics system heightfield }; class ShapeConfiguration @@ -196,4 +200,52 @@ namespace Physics mutable void* m_cachedNativeMesh = nullptr; }; + class HeightfieldShapeConfiguration + : public ShapeConfiguration + { + public: + AZ_CLASS_ALLOCATOR(HeightfieldShapeConfiguration, AZ::SystemAllocator, 0); + AZ_RTTI(HeightfieldShapeConfiguration, "{8DF47C83-D2A9-4E7C-8620-5E173E43C0B3}", ShapeConfiguration); + static void Reflect(AZ::ReflectContext* context); + HeightfieldShapeConfiguration() = default; + HeightfieldShapeConfiguration(const HeightfieldShapeConfiguration&); + HeightfieldShapeConfiguration& operator=(const HeightfieldShapeConfiguration&); + ~HeightfieldShapeConfiguration(); + + ShapeType GetShapeType() const override + { + return ShapeType::Heightfield; + } + + void* GetCachedNativeHeightfield() const; + void SetCachedNativeHeightfield(void* cachedNativeHeightfield) const; + AZ::Vector2 GetGridResolution() const; + void SetGridResolution(const AZ::Vector2& gridSpacing); + int32_t GetNumColumns() const; + void SetNumColumns(int32_t numColumns); + int32_t GetNumRows() const; + void SetNumRows(int32_t numRows); + const AZStd::vector& GetSamples() const; + void SetSamples(const AZStd::vector& samples); + float GetMinHeightBounds() const; + void SetMinHeightBounds(float minBounds); + float GetMaxHeightBounds() const; + void SetMaxHeightBounds(float maxBounds); + + private: + //! The number of meters between each heightfield sample. + AZ::Vector2 m_gridResolution{ 1.0f }; + //! The number of columns in the heightfield sample grid. + int32_t m_numColumns{ 0 }; + //! The number of rows in the heightfield sample grid. + int32_t m_numRows{ 0 }; + //! The minimum and maximum heights that can be used by this heightfield. + //! This can be used by the physics system to choose a more optimal heightfield data type internally (ex: int16, uint8) + float m_minHeightBounds{AZStd::numeric_limits::lowest()}; + float m_maxHeightBounds{AZStd::numeric_limits::max()}; + //! The grid of sample points for the heightfield. + AZStd::vector m_samples; + //! An optional storage pointer for the physics system to cache its native heightfield representation. + mutable void* m_cachedNativeHeightfield{ nullptr }; + }; } // namespace Physics diff --git a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h index 868a27ed36..33313d612b 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h +++ b/Code/Framework/AzFramework/AzFramework/Physics/SystemBus.h @@ -132,6 +132,10 @@ namespace Physics virtual AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) = 0; + /// Releases the height field object created by the physics backend. + /// @param nativeHeightfieldObject Pointer to the height field object. + virtual void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) = 0; + /// Releases the mesh object created by the physics backend. /// @param nativeMeshObject Pointer to the mesh object. virtual void ReleaseNativeMeshObject(void* nativeMeshObject) = 0; diff --git a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp index 3dcb37c541..d989847ae8 100644 --- a/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp +++ b/Code/Framework/AzFramework/AzFramework/Physics/Utils.cpp @@ -107,6 +107,7 @@ namespace Physics PhysicsAssetShapeConfiguration::Reflect(context); NativeShapeConfiguration::Reflect(context); CookedMeshShapeConfiguration::Reflect(context); + HeightfieldShapeConfiguration::Reflect(context); AzPhysics::SystemInterface::Reflect(context); AzPhysics::Scene::Reflect(context); AzPhysics::CollisionLayer::Reflect(context); diff --git a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake b/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake similarity index 83% rename from Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake rename to Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake index 5597c28d04..162cc1ea86 100644 --- a/Gems/AudioEngineWwise/Code/audioenginewwise_stub_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/Physics/physics_mock_files.cmake @@ -7,5 +7,5 @@ # set(FILES - Source/AudioEngineWwiseModule_Stub.cpp -) + Mocks/MockHeightfieldProviderBus.h +) \ No newline at end of file diff --git a/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp b/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp index b4cad8511f..cd52393df9 100644 --- a/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp +++ b/Code/Framework/AzFramework/AzFramework/Visibility/OctreeSystemComponent.cpp @@ -14,9 +14,8 @@ namespace AzFramework { AZ_CVAR(bool, bg_octreeUseQuadtree, false, nullptr, AZ::ConsoleFunctorFlags::ReadOnly, "If set to true, the visibility octrees will degenerate to a quadtree split along the X/Y plane"); AZ_CVAR(float, bg_octreeMaxWorldExtents, 16384.0f, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum supported world size by the world octreeSystemComponent"); - AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split"); - AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation"); - + AZ_CVAR(uint32_t, bg_octreeNodeMaxEntries, 64, nullptr, AZ::ConsoleFunctorFlags::Null, "Maximum number of entries to allow in any node before forcing a split"); + AZ_CVAR(uint32_t, bg_octreeNodeMinEntries, 32, nullptr, AZ::ConsoleFunctorFlags::Null, "Minimum number of entries to allow in a node resulting from a merge operation"); static uint32_t GetChildNodeCount() { @@ -25,14 +24,12 @@ namespace AzFramework return (bg_octreeUseQuadtree) ? QuadtreeNodeChildCount : OctreeNodeChildCount; } - OctreeNode::OctreeNode(const AZ::Aabb& bounds) : m_bounds(bounds) { ; } - OctreeNode::OctreeNode(OctreeNode&& rhs) : m_bounds(rhs.m_bounds) , m_parent(rhs.m_parent) @@ -46,7 +43,6 @@ namespace AzFramework } } - OctreeNode& OctreeNode::operator=(OctreeNode&& rhs) { m_bounds = rhs.m_bounds; @@ -63,7 +59,6 @@ namespace AzFramework return *this; } - void OctreeNode::Insert(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == nullptr, "Double-insertion: Insert invoked for an entry already bound to the OctreeScene"); @@ -98,7 +93,6 @@ namespace AzFramework } } - void OctreeNode::Update(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == this, "Update invoked for an entry bound to a different OctreeNode"); @@ -129,7 +123,6 @@ namespace AzFramework } } - void OctreeNode::Remove(OctreeScene& octreeScene, VisibilityEntry* entry) { AZ_Assert(entry->m_internalNode == this, "Remove invoked for an entry bound to a different OctreeNode"); @@ -152,25 +145,30 @@ namespace AzFramework } } - void OctreeNode::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(aabb, callback); + if (AZ::ShapeIntersection::Overlaps(aabb, m_bounds)) + { + EnumerateHelper(aabb, callback); + } } - void OctreeNode::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(sphere, callback); + if (AZ::ShapeIntersection::Overlaps(sphere, m_bounds)) + { + EnumerateHelper(sphere, callback); + } } - void OctreeNode::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const { - EnumerateHelper(frustum, callback); + if (AZ::ShapeIntersection::Overlaps(frustum, m_bounds)) + { + EnumerateHelper(frustum, callback); + } } - void OctreeNode::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const { // Invoke the callback for the current node @@ -190,25 +188,21 @@ namespace AzFramework } } - const AZStd::vector& OctreeNode::GetEntries() const { return m_entries; } - OctreeNode* OctreeNode::GetChildren() const { return m_children; } - bool OctreeNode::IsLeaf() const { return m_children == nullptr; } - void OctreeNode::TryMerge(OctreeScene& octreeScene) { if (IsLeaf()) @@ -236,7 +230,6 @@ namespace AzFramework } } - template void OctreeNode::EnumerateHelper(const T& boundingVolume, const IVisibilityScene::EnumerateCallback& callback) const { @@ -262,7 +255,6 @@ namespace AzFramework } } - void OctreeNode::Split(OctreeScene& octreeScene) { AZ_Assert(m_children == nullptr, "Split invoked on an octreeScene node that has already been split"); @@ -312,7 +304,6 @@ namespace AzFramework } } - void OctreeNode::Merge(OctreeScene& octreeScene) { AZ_Assert(m_children != nullptr, "Merge invoked on an octreeScene node that does not have children"); @@ -371,7 +362,6 @@ namespace AzFramework } } - void OctreeScene::RemoveEntry(VisibilityEntry& entry) { AZStd::lock_guard lock(m_sharedMutex); @@ -382,35 +372,30 @@ namespace AzFramework } } - void OctreeScene::Enumerate(const AZ::Aabb& aabb, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(aabb, callback); } - void OctreeScene::Enumerate(const AZ::Sphere& sphere, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(sphere, callback); } - void OctreeScene::Enumerate(const AZ::Frustum& frustum, const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.Enumerate(frustum, callback); } - void OctreeScene::EnumerateNoCull(const IVisibilityScene::EnumerateCallback& callback) const { AZStd::shared_lock lock(m_sharedMutex); m_root.EnumerateNoCull(callback); } - uint32_t OctreeScene::GetEntryCount() const { return m_entryCount; @@ -421,26 +406,22 @@ namespace AzFramework return m_nodeCount; } - uint32_t OctreeScene::GetFreeNodeCount() const { // Each entry represents GetChildNodeCount() nodes return aznumeric_cast(m_freeOctreeNodes.size() * GetChildNodeCount()); } - uint32_t OctreeScene::GetPageCount() const { return aznumeric_cast(m_nodeCache.size()); } - uint32_t OctreeScene::GetChildNodeCount() const { return AzFramework::GetChildNodeCount(); } - void OctreeScene::DumpStats() { AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::EntryCount = %u", GetName().GetCStr(), GetEntryCount()); @@ -450,21 +431,18 @@ namespace AzFramework AZ_TracePrintf("Console", "OctreeScene[\"%s\"]::ChildNodeCount = %u", GetName().GetCStr(), GetChildNodeCount()); } - static inline uint32_t CreateNodeIndex(uint32_t page, uint32_t offset) { AZ_Assert(page <= 0xFFFF && offset <= 0xFFFF, "Out of range values passed to CreateNodeIndex"); return (page << 16) | offset; } - static inline void ExtractPageAndOffsetFromIndex(uint32_t index, uint32_t& page, uint32_t& offset) { offset = index & 0x0000FFFF; page = index >> 16; } - uint32_t OctreeScene::AllocateChildNodes() { const uint32_t childCount = GetChildNodeCount(); @@ -508,14 +486,12 @@ namespace AzFramework return CreateNodeIndex(nextChildPage, nextChildOffset); } - void OctreeScene::ReleaseChildNodes(uint32_t nodeIndex) { m_nodeCount -= GetChildNodeCount(); m_freeOctreeNodes.push(nodeIndex); } - OctreeNode* OctreeScene::GetChildNodesAtIndex(uint32_t nodeIndex) const { uint32_t childPage; @@ -524,7 +500,6 @@ namespace AzFramework return &(*m_nodeCache[childPage])[childOffset]; } - void OctreeSystemComponent::Reflect(AZ::ReflectContext* context) { if (auto* serializeContext = azrtti_cast(context)) @@ -534,19 +509,16 @@ namespace AzFramework } } - void OctreeSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC("OctreeService")); } - void OctreeSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) { incompatible.push_back(AZ_CRC("OctreeService")); } - OctreeSystemComponent::OctreeSystemComponent() { AZ::Interface::Register(this); @@ -555,7 +527,6 @@ namespace AzFramework m_defaultScene = aznew OctreeScene(AZ::Name("DefaultVisibilityScene")); } - OctreeSystemComponent::~OctreeSystemComponent() { AZ_Assert(m_scenes.empty(), "All IVisibilityScenes must be destroyed before shutdown"); @@ -566,13 +537,11 @@ namespace AzFramework AZ::Interface::Unregister(this); } - void OctreeSystemComponent::Activate() { ; } - void OctreeSystemComponent::Deactivate() { ; @@ -591,7 +560,6 @@ namespace AzFramework return newScene; } - void OctreeSystemComponent::DestroyVisibilityScene(IVisibilityScene* visScene) { for (auto iter = m_scenes.begin(); iter != m_scenes.end(); ++iter) @@ -606,7 +574,6 @@ namespace AzFramework AZ_Assert(false, "visScene[\"%s\"] not found in the OctreeSystemComponent", visScene->GetName().GetCStr()); } - IVisibilityScene* OctreeSystemComponent::FindVisibilityScene(const AZ::Name& sceneName) { for (OctreeScene* scene : m_scenes) @@ -619,7 +586,6 @@ namespace AzFramework return nullptr; } - void OctreeSystemComponent::DumpStats([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { for (OctreeScene* scene : m_scenes) diff --git a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake index c5616d84c0..b338dbb0a8 100644 --- a/Code/Framework/AzFramework/AzFramework/azframework_files.cmake +++ b/Code/Framework/AzFramework/AzFramework/azframework_files.cmake @@ -228,6 +228,7 @@ set(FILES Physics/Configuration/SimulatedBodyConfiguration.cpp Physics/Configuration/SystemConfiguration.h Physics/Configuration/SystemConfiguration.cpp + Physics/HeightfieldProviderBus.h Physics/SimulatedBodies/RigidBody.h Physics/SimulatedBodies/RigidBody.cpp Physics/SimulatedBodies/StaticRigidBody.h @@ -251,6 +252,7 @@ set(FILES Physics/Shape.h Physics/ShapeConfiguration.h Physics/ShapeConfiguration.cpp + Physics/HeightfieldProviderBus.h Physics/SystemBus.h Physics/ColliderComponentBus.h Physics/RagdollPhysicsBus.h diff --git a/Code/Framework/AzFramework/CMakeLists.txt b/Code/Framework/AzFramework/CMakeLists.txt index b22586162b..c8eeac5c2d 100644 --- a/Code/Framework/AzFramework/CMakeLists.txt +++ b/Code/Framework/AzFramework/CMakeLists.txt @@ -42,6 +42,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) NAMESPACE AZ FILES_CMAKE Tests/framework_shared_tests_files.cmake + AzFramework/Physics/physics_mock_files.cmake INCLUDE_DIRECTORIES PUBLIC Tests @@ -53,7 +54,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzTestShared ) - + if(PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp index 158240210e..85669e093c 100644 --- a/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp +++ b/Code/Framework/AzFramework/Platform/Linux/AzFramework/Application/Application_Linux.cpp @@ -7,17 +7,35 @@ */ #include +#include #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB #include #endif +constexpr rlim_t g_minimumOpenFileHandles = 65536L; + //////////////////////////////////////////////////////////////////////////////////////////////////// namespace AzFramework { //////////////////////////////////////////////////////////////////////////////////////////////// Application::Implementation* Application::Implementation::Create() { + // The default open file limit for processes may not be enough for O3DE applications. + // We will need to increase to the recommended value if the current open file limit + // is not sufficient. + rlimit currentLimit; + int get_limit_result = getrlimit(RLIMIT_NOFILE, ¤tLimit); + AZ_Warning("Application", get_limit_result == 0, "Unable to read current ulimit open file limits"); + if ((get_limit_result == 0) && (currentLimit.rlim_cur < g_minimumOpenFileHandles || currentLimit.rlim_max < g_minimumOpenFileHandles)) + { + rlimit newLimit; + newLimit.rlim_cur = g_minimumOpenFileHandles; // Soft Limit + newLimit.rlim_max = g_minimumOpenFileHandles; // Hard Limit + [[maybe_unused]] int set_limit_result = setrlimit(RLIMIT_NOFILE, &newLimit); + AZ_Assert(set_limit_result == 0, "Unable to update open file limits"); + } + #if PAL_TRAIT_LINUX_WINDOW_MANAGER_XCB return aznew XcbApplication(); #elif PAL_TRAIT_LINUX_WINDOW_MANAGER_WAYLAND diff --git a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp index 88392c98c0..b0f316cf50 100644 --- a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp +++ b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.cpp @@ -53,7 +53,7 @@ namespace AzNetworking m_timeoutItemMap.erase(timeoutId); } - void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts) + void TimeoutQueue::UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts) { int32_t numTimeouts = 0; if (maxTimeouts < 0) @@ -103,7 +103,7 @@ namespace AzNetworking // By this point, the item is definitely timed out // Invoke the timeout function to see how to proceed - const TimeoutResult result = timeoutHandler.HandleTimeout(mapItem); + const TimeoutResult result = timeoutHandler(mapItem); if (result == TimeoutResult::Refresh) { @@ -122,4 +122,10 @@ namespace AzNetworking m_timeoutItemMap.erase(itemTimeoutId); } } + + void TimeoutQueue::UpdateTimeouts(ITimeoutHandler& timeoutHandler, int32_t maxTimeouts) + { + TimeoutHandler handler([&timeoutHandler](TimeoutQueue::TimeoutItem& item) { return timeoutHandler.HandleTimeout(item); }); + UpdateTimeouts(handler, maxTimeouts); + } } diff --git a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h index 239af5be0e..63417ea36f 100644 --- a/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h +++ b/Code/Framework/AzNetworking/AzNetworking/DataStructures/TimeoutQueue.h @@ -64,6 +64,12 @@ namespace AzNetworking //! @param timeoutId the identifier of the item to remove void RemoveItem(TimeoutId timeoutId); + //! Updates timeouts for all items, invokes the provided timeout functor if required. + //! @param timeoutHandler lambda to invoke for all timeouts + //! @param maxTimeouts the maximum number of timeouts to process before breaking iteration + using TimeoutHandler = AZStd::function; + void UpdateTimeouts(const TimeoutHandler& timeoutHandler, int32_t maxTimeouts = -1); + //! Updates timeouts for all items, invokes timeout handlers if required. //! @param timeoutHandler listener instance to call back on for timeouts //! @param maxTimeouts the maximum number of timeouts to process before breaking iteration diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp index 670607900f..f79f355ccb 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.cpp @@ -13,6 +13,7 @@ #include #include #include +#include namespace AzQtComponents { @@ -27,6 +28,13 @@ namespace AzQtComponents setAttribute(Qt::WA_ShowWithoutActivating); setAttribute(Qt::WA_DeleteOnClose); + m_borderRadius = toastConfiguration.m_borderRadius; + if (m_borderRadius > 0) + { + setWindowFlags(Qt::FramelessWindowHint | Qt::Dialog); + setAttribute(Qt::WA_TranslucentBackground); + } + m_ui->setupUi(this); QIcon toastIcon; @@ -53,6 +61,13 @@ namespace AzQtComponents m_ui->titleLabel->setText(toastConfiguration.m_title); m_ui->mainLabel->setText(toastConfiguration.m_description); + // hide the optional description if none is provided so the title is centered vertically + if (toastConfiguration.m_description.isEmpty()) + { + m_ui->mainLabel->setVisible(false); + m_ui->verticalLayout->removeWidget(m_ui->mainLabel); + } + m_lifeSpan.setInterval(aznumeric_cast(toastConfiguration.m_duration.count())); m_closeOnClick = toastConfiguration.m_closeOnClick; @@ -68,6 +83,24 @@ namespace AzQtComponents { } + void ToastNotification::paintEvent(QPaintEvent* event) + { + if (m_borderRadius > 0) + { + QPainter p(this); + p.setPen(Qt::transparent); + QColor painterColor; + painterColor.setRgbF(0, 0, 0, 255); + p.setBrush(painterColor); + p.setRenderHint(QPainter::Antialiasing); + p.drawRoundedRect(rect(), m_borderRadius, m_borderRadius); + } + else + { + QDialog::paintEvent(event); + } + } + void ToastNotification::ShowToastAtCursor() { QPoint globalCursorPos = QCursor::pos(); diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h index eaf8a0751d..7f2701a803 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.h @@ -52,6 +52,8 @@ namespace AzQtComponents void mousePressEvent(QMouseEvent* mouseEvent) override; bool eventFilter(QObject* object, QEvent* event) override; + void paintEvent(QPaintEvent* event) override; + public slots: void StartTimer(); void FadeOut(); @@ -65,6 +67,7 @@ namespace AzQtComponents bool m_closeOnClick; QTimer m_lifeSpan; + uint32_t m_borderRadius = 0; AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZStd::chrono::milliseconds m_fadeDuration; diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui index 107281bdc2..7aefddbd98 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotification.ui @@ -191,7 +191,7 @@ - 40 + 20 20 diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h index 05999017e4..5ace9d1be2 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h +++ b/Code/Framework/AzQtComponents/AzQtComponents/Components/ToastNotificationConfiguration.h @@ -37,6 +37,7 @@ namespace AzQtComponents QString m_title; QString m_description; QString m_customIconImage; + uint32_t m_borderRadius = 0; AZ_PUSH_DISABLE_DLL_EXPORT_MEMBER_WARNING AZStd::chrono::milliseconds m_duration = AZStd::chrono::milliseconds(5000); diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg new file mode 100644 index 0000000000..3fccf0e716 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_close.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg new file mode 100644 index 0000000000..55704ec230 --- /dev/null +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/Entity/prefab_edit_open.svg @@ -0,0 +1,4 @@ + + + + diff --git a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc index 0c8fedc79d..15049aeb69 100644 --- a/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc +++ b/Code/Framework/AzQtComponents/AzQtComponents/Images/resources.qrc @@ -6,6 +6,8 @@ Entity/layer.svg Entity/prefab.svg Entity/prefab_edit.svg + Entity/prefab_edit_open.svg + Entity/prefab_edit_close.svg Level/level.svg diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp index d95a704e84..e9a5bee87a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.cpp @@ -9,6 +9,8 @@ #include #include +#include +#include #include #include #include @@ -38,6 +40,13 @@ namespace AzToolsFramework AZ::Edit::SliceFlags::DontGatherReference); } } + + if (auto behaviorContext = azrtti_cast(context)) + { + behaviorContext->ConstantProperty( + "EditorPrefabComponentTypeId", BehaviorConstant(AZ::Uuid(EditorPrefabComponent::EditorPrefabComponentTypeId))) + ->Attribute(AZ::Script::Attributes::Scope, AZ::Script::Attributes::ScopeFlags::Automation); + } } void EditorPrefabComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h index 8873787bd3..aa15b63ac2 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/EditorPrefabComponent.h @@ -16,7 +16,9 @@ namespace AzToolsFramework class EditorPrefabComponent : public AzToolsFramework::Components::EditorComponentBase { public: - AZ_COMPONENT(EditorPrefabComponent, "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}", EditorComponentBase); + static constexpr const char* const EditorPrefabComponentTypeId = "{756E5F9C-3E08-4F8D-855C-A5AEEFB6FCDD}"; + + AZ_COMPONENT(EditorPrefabComponent, EditorPrefabComponentTypeId, EditorComponentBase); static void Reflect(AZ::ReflectContext* context); static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp index 09b5745a90..8bc258fef9 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.cpp @@ -206,6 +206,34 @@ namespace AzToolsFramework::Prefab return instance.has_value() && (&instance->get() == &m_focusedInstance->get()); } + bool PrefabFocusHandler::IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const + { + if (!m_focusedInstance.has_value()) + { + // PrefabFocusHandler has not been initialized yet. + return false; + } + + if (!entityId.IsValid()) + { + return false; + } + + InstanceOptionalReference instance = m_instanceEntityMapperInterface->FindOwningInstance(entityId); + + while (instance.has_value()) + { + if (&instance->get() == &m_focusedInstance->get()) + { + return true; + } + + instance = instance->get().GetParentInstance(); + } + + return false; + } + const AZ::IO::Path& PrefabFocusHandler::GetPrefabFocusPath([[maybe_unused]] AzFramework::EntityContextId entityContextId) const { return m_instanceFocusPath; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h index 6a71365d8e..cb8165e7f0 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusHandler.h @@ -53,6 +53,7 @@ namespace AzToolsFramework::Prefab PrefabFocusOperationResult FocusOnPathIndex(AzFramework::EntityContextId entityContextId, int index) override; AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const override; bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const override; + bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const override; const AZ::IO::Path& GetPrefabFocusPath(AzFramework::EntityContextId entityContextId) const override; const int GetPrefabFocusPathLength(AzFramework::EntityContextId entityContextId) const override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h index 86e476b56f..5bd4c6b0f6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabFocusPublicInterface.h @@ -37,10 +37,15 @@ namespace AzToolsFramework::Prefab //! Returns the entity id of the container entity for the instance the prefab system is focusing on. virtual AZ::EntityId GetFocusedPrefabContainerEntityId(AzFramework::EntityContextId entityContextId) const = 0; + //! Returns whether the entity belongs to the instance that is being focused on. + //! @param entityId The entityId of the queried entity. + //! @return true if the entity belongs to the focused instance, false otherwise. + virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0; + //! Returns whether the entity belongs to the instance that is being focused on, or one of its descendants. //! @param entityId The entityId of the queried entity. //! @return true if the entity belongs to the focused instance or one of its descendants, false otherwise. - virtual bool IsOwningPrefabBeingFocused(AZ::EntityId entityId) const = 0; + virtual bool IsOwningPrefabInFocusHierarchy(AZ::EntityId entityId) const = 0; //! Returns the path from the root instance to the currently focused instance. //! @return A path composed from the names of the container entities for the instance path. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp index ee34b628bb..d976c91c3e 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.cpp @@ -974,7 +974,7 @@ namespace AzToolsFramework return DeleteFromInstance(entityIds, true); } - PrefabOperationResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + DuplicatePrefabResult PrefabPublicHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) { if (entityIds.empty()) { @@ -1021,6 +1021,7 @@ namespace AzToolsFramework ScopedUndoBatch undoBatch("Duplicate Entities"); + EntityIdList duplicatedEntityAndInstanceIds; { AZ_PROFILE_SCOPE(AzToolsFramework, "DuplicateEntitiesInInstance::UndoCaptureAndDuplicateEntities"); @@ -1033,7 +1034,7 @@ namespace AzToolsFramework if (!retrieveEntitiesAndInstancesOutcome.IsSuccess()) { - return AZStd::move(retrieveEntitiesAndInstancesOutcome); + return AZ::Failure(retrieveEntitiesAndInstancesOutcome.TakeError()); } // Take a snapshot of the instance DOM before we manipulate it @@ -1044,8 +1045,6 @@ namespace AzToolsFramework PrefabDom instanceDomAfter; instanceDomAfter.CopyFrom(instanceDomBefore, instanceDomAfter.GetAllocator()); - EntityIdList duplicatedEntityAndInstanceIds; - // Duplicate any nested entities and instances as requested AZStd::unordered_map newInstanceAliasToOldInstanceMap; AZStd::unordered_map duplicateEntityAliasMap; @@ -1114,7 +1113,7 @@ namespace AzToolsFramework ToolsApplicationRequestBus::Broadcast(&ToolsApplicationRequestBus::Events::SetSelectedEntities, duplicatedEntityAndInstanceIds); } - return AZ::Success(); + return AZ::Success(AZStd::move(duplicatedEntityAndInstanceIds)); } PrefabOperationResult PrefabPublicHandler::DeleteFromInstance(const EntityIdList& entityIds, bool deleteDescendants) diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h index a9dadc3336..4961be9d77 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicHandler.h @@ -63,7 +63,7 @@ namespace AzToolsFramework PrefabOperationResult DeleteEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; - PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h index 528d4f6d1b..ede857dd2b 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicInterface.h @@ -26,6 +26,7 @@ namespace AzToolsFramework { typedef AZ::Outcome CreatePrefabResult; typedef AZ::Outcome InstantiatePrefabResult; + typedef AZ::Outcome DuplicatePrefabResult; typedef AZ::Outcome PrefabOperationResult; typedef AZ::Outcome PrefabRequestResult; typedef AZ::Outcome PrefabEntityResult; @@ -160,14 +161,15 @@ namespace AzToolsFramework /** * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. * @param entities The entities to duplicate. - * @return An outcome object; on failure, it comes with an error message detailing the cause of the error. + * @return An outcome object with a list of ids of target entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. */ - virtual PrefabOperationResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; /** * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting * the container entity into a regular entity and putting it under the parent prefab, removing the link between this - * instance and the parent, removing links between this instance and it's nested instances, adding entities directly + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly * owned by this instance under the parent instance. * Bails if the entity is not a container entity or belongs to the level prefab instance. * @param containerEntityId The container entity id of the instance to detach. diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h index 7b23fffb7f..fd4b8a5f17 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestBus.h @@ -25,6 +25,7 @@ namespace AzToolsFramework { using CreatePrefabResult = AZ::Outcome; using InstantiatePrefabResult = AZ::Outcome; + using DuplicatePrefabResult = AZ::Outcome; using PrefabOperationResult = AZ::Outcome; /** @@ -69,6 +70,29 @@ namespace AzToolsFramework * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. */ virtual PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) = 0; + + /** + * If the entity id is a container entity id, detaches the prefab instance corresponding to it. This includes converting + * the container entity into a regular entity and putting it under the parent prefab, removing the link between this + * instance and the parent, removing links between this instance and its nested instances, and adding entities directly + * owned by this instance under the parent instance. + * Bails if the entity is not a container entity or belongs to the level prefab instance. + * Return an outcome object; on failure, it comes with an error message detailing the cause of the error. + */ + virtual PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) = 0; + + /** + * Duplicates all entities in the owning instance. Bails if the entities don't all belong to the same instance. + * Return an outcome object with a list of ids of given entities' duplicates if duplication succeeded; + * on failure, it comes with an error message detailing the cause of the error. + */ + virtual DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) = 0; + + /** + * Get the file path to the prefab file for the prefab instance owning the entity provided. + * Returns the path to the prefab, or an empty path if the entity is owned by the level. + */ + virtual AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const = 0; }; using PrefabPublicRequestBus = AZ::EBus; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp index 0aaf81c4c9..3b69dcdfe4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.cpp @@ -28,6 +28,9 @@ namespace AzToolsFramework ->Event("CreatePrefabInMemory", &PrefabPublicRequests::CreatePrefabInMemory) ->Event("InstantiatePrefab", &PrefabPublicRequests::InstantiatePrefab) ->Event("DeleteEntitiesAndAllDescendantsInInstance", &PrefabPublicRequests::DeleteEntitiesAndAllDescendantsInInstance) + ->Event("DetachPrefab", &PrefabPublicRequests::DetachPrefab) + ->Event("DuplicateEntitiesInInstance", &PrefabPublicRequests::DuplicateEntitiesInInstance) + ->Event("GetOwningInstancePrefabPath", &PrefabPublicRequests::GetOwningInstancePrefabPath) ; } } @@ -62,5 +65,19 @@ namespace AzToolsFramework return m_prefabPublicInterface->DeleteEntitiesAndAllDescendantsInInstance(entityIds); } + PrefabOperationResult PrefabPublicRequestHandler::DetachPrefab(const AZ::EntityId& containerEntityId) + { + return m_prefabPublicInterface->DetachPrefab(containerEntityId); + } + + DuplicatePrefabResult PrefabPublicRequestHandler::DuplicateEntitiesInInstance(const EntityIdList& entityIds) + { + return m_prefabPublicInterface->DuplicateEntitiesInInstance(entityIds); + } + + AZStd::string PrefabPublicRequestHandler::GetOwningInstancePrefabPath(AZ::EntityId entityId) const + { + return m_prefabPublicInterface->GetOwningInstancePrefabPath(entityId).Native(); + } } // namespace Prefab } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h index ae0ed2a5d1..b24ea7ec2a 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabPublicRequestHandler.h @@ -34,6 +34,9 @@ namespace AzToolsFramework CreatePrefabResult CreatePrefabInMemory(const EntityIdList& entityIds, AZStd::string_view filePath) override; InstantiatePrefabResult InstantiatePrefab(AZStd::string_view filePath, AZ::EntityId parent, const AZ::Vector3& position) override; PrefabOperationResult DeleteEntitiesAndAllDescendantsInInstance(const EntityIdList& entityIds) override; + PrefabOperationResult DetachPrefab(const AZ::EntityId& containerEntityId) override; + DuplicatePrefabResult DuplicateEntitiesInInstance(const EntityIdList& entityIds) override; + AZStd::string GetOwningInstancePrefabPath(AZ::EntityId entityId) const override; private: PrefabPublicInterface* m_prefabPublicInterface = nullptr; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp index 6c96209d56..1c2230fa83 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.cpp @@ -23,7 +23,7 @@ namespace AzToolsFramework } //PrefabInstanceUndo - PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation) + PrefabUndoInstance::PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation) : PrefabUndoBase(undoOperationName) { m_useImmediatePropagation = useImmediatePropagation; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h index 0946a36951..bc0b86a8c6 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Prefab/PrefabUndo.h @@ -45,7 +45,7 @@ namespace AzToolsFramework : public PrefabUndoBase { public: - explicit PrefabUndoInstance(const AZStd::string& undoOperationName, const bool useImmediatePropagation = true); + explicit PrefabUndoInstance(const AZStd::string& undoOperationName, bool useImmediatePropagation = true); void Capture( const PrefabDom& initialState, diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp index aca667b9aa..c13f7dd848 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/Slice/SliceUtilities.cpp @@ -2648,16 +2648,15 @@ namespace AzToolsFramework } else { - QString cleanSaveAs(QDir::cleanPath(slicePath)); + AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(slicePath.toUtf8().constData()).LexicallyNormal(); bool isPathSafeForAssets = false; - for (AZStd::string assetSafeFolder : assetSafeFolders) + for (const AZStd::string& assetSafeFolder : assetSafeFolders) { - QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str())); - // Compare using clean paths so slash direction does not matter. - // Note that this comparison is case sensitive because some file systems - // Open 3D Engine supports are case sensitive. - if (cleanSaveAs.startsWith(cleanAssetSafeFolder)) + AZ::IO::PathView assetSafeFolderView(assetSafeFolder); + // Check if the slice path is relative to the safe asset directory. + // The Path classes are being used to make this check case insensitive. + if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView)) { isPathSafeForAssets = true; break; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp index acb2cd4b1c..6f6f8dccff 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.cpp @@ -80,14 +80,7 @@ namespace AzToolsFramework // set up signals before we start thread. m_shutdownThreadSignal = false; - - // Check to see if the 'p4' command is available at the command line - int p4VersionExitCode = QProcess::execute("p4", QStringList{ "-V" }); - m_p4ApplicationDetected = (p4VersionExitCode == 0); - if (m_p4ApplicationDetected) - { - m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this)); - } + m_WorkerThread = AZStd::thread(AZStd::bind(&PerforceComponent::ThreadWorker, this)); SourceControlConnectionRequestBus::Handler::BusConnect(); SourceControlCommandBus::Handler::BusConnect(); @@ -98,13 +91,10 @@ namespace AzToolsFramework SourceControlCommandBus::Handler::BusDisconnect(); SourceControlConnectionRequestBus::Handler::BusDisconnect(); - if (m_p4ApplicationDetected) - { - m_shutdownThreadSignal = true; // tell the thread to die. - m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal - m_WorkerThread.join(); // wait for the thread to finish. - m_WorkerThread = AZStd::thread(); - } + m_shutdownThreadSignal = true; // tell the thread to die. + m_WorkerSemaphore.release(1); // wake up the thread so that it sees the signal + m_WorkerThread.join(); // wait for the thread to finish. + m_WorkerThread = AZStd::thread(); SetConnection(nullptr); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h index e5ccf7ad29..858d9c0103 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/SourceControl/PerforceComponent.h @@ -260,7 +260,5 @@ namespace AzToolsFramework AZStd::atomic_bool m_validConnection; SourceControlState m_connectionState; - - bool m_p4ApplicationDetected { false }; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp index 3cdcade1b0..866080a83f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.cpp @@ -101,8 +101,25 @@ namespace AzToolsFramework { } - void EditorEntityUiHandlerBase::OnDoubleClick([[maybe_unused]] AZ::EntityId entityId) const + bool EditorEntityUiHandlerBase::OnOutlinerItemClick( + [[maybe_unused]] const QPoint& position, + [[maybe_unused]] const QStyleOptionViewItem& option, + [[maybe_unused]] const QModelIndex& index) const + { + return false; + } + + void EditorEntityUiHandlerBase::OnOutlinerItemExpand([[maybe_unused]] const QModelIndex& index) const + { + } + + void EditorEntityUiHandlerBase::OnOutlinerItemCollapse([[maybe_unused]] const QModelIndex& index) const { } + bool EditorEntityUiHandlerBase::OnEntityDoubleClick([[maybe_unused]] AZ::EntityId entityId) const + { + return false; + } + } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h index 9554ad01fc..96d393efa1 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/EditorEntityUi/EditorEntityUiHandlerBase.h @@ -61,8 +61,17 @@ namespace AzToolsFramework virtual void PaintDescendantForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const QModelIndex& descendantIndex) const; - //! Triggered when the entity is double clicked in the Outliner. - virtual void OnDoubleClick(AZ::EntityId entityId) const; + //! Triggered when the entity is clicked in the Outliner. + //! @return True if the click has been handled and should not be propagated, false otherwise. + virtual bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const; + //! Triggered when an entity's children are expanded in the Outliner. + virtual void OnOutlinerItemExpand(const QModelIndex& index) const; + //! Triggered when an entity's children are collapsed in the Outliner. + virtual void OnOutlinerItemCollapse(const QModelIndex& index) const; + + //! Triggered when the entity is double clicked in the Outliner or in the Viewport. + //! @return True if the double click has been handled and should not be propagated, false otherwise. + virtual bool OnEntityDoubleClick(AZ::EntityId entityId) const; private: EditorEntityUiHandlerId m_handlerId = 0; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp index 0ef0bf9a7b..e039230783 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.cpp @@ -177,4 +177,14 @@ namespace AzToolsFramework DisplayQueuedNotification(); } } + + void ToastNotificationsView::SetOffset(const QPoint& offset) + { + m_offset = offset; + } + + void ToastNotificationsView::SetAnchorPoint(const QPointF& anchorPoint) + { + m_anchorPoint = anchorPoint; + } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h index c4220331d8..e13f129467 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Notifications/ToastNotificationsView.h @@ -50,6 +50,9 @@ namespace AzToolsFramework void OnShow(); void UpdateToastPosition(); + void SetOffset(const QPoint& offset); + void SetAnchorPoint(const QPointF& anchorPoint); + private: ToastId CreateToastNotification(const AzQtComponents::ToastConfiguration& toastConfiguration); void DisplayQueuedNotification(); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp index 3f8023c1e3..a5f1e29942 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerListModel.cpp @@ -11,10 +11,12 @@ #include #include #include +#include #include #include #include #include +#include #include #include #include @@ -2287,7 +2289,14 @@ namespace AzToolsFramework // Now we setup a Text Document so it can draw the rich text QTextDocument textDoc; textDoc.setDefaultFont(optionV4.font); - textDoc.setDefaultStyleSheet("body {color: white}"); + if (option.state & QStyle::State_Enabled) + { + textDoc.setDefaultStyleSheet("body {color: white}"); + } + else + { + textDoc.setDefaultStyleSheet("body {color: #7C7C7C}"); + } textDoc.setHtml("" + entityNameRichText + ""); painter->translate(textRect.topLeft()); textDoc.setTextWidth(textRect.width()); @@ -2326,6 +2335,23 @@ namespace AzToolsFramework return true; } + if (event->type() == QEvent::MouseButtonPress) + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + + if (auto editorEntityUiInterface = AZ::Interface::Get(); editorEntityUiInterface != nullptr) + { + auto mouseEvent = static_cast(event); + + auto entityUiHandler = editorEntityUiInterface->GetHandler(entityId); + + if (entityUiHandler && entityUiHandler->OnOutlinerItemClick(mouseEvent->pos(), option, index)) + { + return true; + } + } + } + return QStyledItemDelegate::editorEvent(event, model, option, index); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp index d3138f5139..d94ced392c 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.cpp @@ -73,6 +73,8 @@ namespace AzToolsFramework void EntityOutlinerTreeView::leaveEvent([[maybe_unused]] QEvent* event) { m_mousePosition = QPoint(); + m_currentHoveredIndex = QModelIndex(); + update(); } void EntityOutlinerTreeView::mousePressEvent(QMouseEvent* event) @@ -129,6 +131,11 @@ namespace AzToolsFramework } m_mousePosition = event->pos(); + if (QModelIndex hoveredIndex = indexAt(m_mousePosition); m_currentHoveredIndex != indexAt(m_mousePosition)) + { + m_currentHoveredIndex = hoveredIndex; + update(); + } //process mouse movement as normal, potentially triggering drag and drop QTreeView::mouseMoveEvent(event); diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx index 5d76ec6db2..42b42a59b4 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerTreeView.hxx @@ -90,6 +90,8 @@ namespace AzToolsFramework const QColor m_selectedColor = QColor(255, 255, 255, 45); const QColor m_hoverColor = QColor(255, 255, 255, 30); + QModelIndex m_currentHoveredIndex; + EditorEntityUiInterface* m_editorEntityFrameworkInterface; }; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp index 4d53102edb..3e97f967fc 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Outliner/EntityOutlinerWidget.cpp @@ -902,6 +902,7 @@ namespace AzToolsFramework EditorPickModeRequestBus::Broadcast( &EditorPickModeRequests::StopEntityPickMode); + return; } switch (index.column()) @@ -918,18 +919,30 @@ namespace AzToolsFramework { if (AZ::EntityId entityId = GetEntityIdFromIndex(index); auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) { - entityUiHandler->OnDoubleClick(entityId); + entityUiHandler->OnEntityDoubleClick(entityId); } } void EntityOutlinerWidget::OnTreeItemExpanded(const QModelIndex& index) { - m_listModel->OnEntityExpanded(GetEntityIdFromIndex(index)); + AZ::EntityId entityId = GetEntityIdFromIndex(index); + if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) + { + entityUiHandler->OnOutlinerItemExpand(index); + } + + m_listModel->OnEntityExpanded(entityId); } void EntityOutlinerWidget::OnTreeItemCollapsed(const QModelIndex& index) { - m_listModel->OnEntityCollapsed(GetEntityIdFromIndex(index)); + AZ::EntityId entityId = GetEntityIdFromIndex(index); + if (auto entityUiHandler = m_editorEntityUiInterface->GetHandler(entityId)) + { + entityUiHandler->OnOutlinerItemCollapse(index); + } + + m_listModel->OnEntityCollapsed(entityId); } void EntityOutlinerWidget::OnExpandEntity(const AZ::EntityId& entityId, bool expand) @@ -1163,7 +1176,7 @@ namespace AzToolsFramework { QTimer::singleShot(1, this, [this]() { m_gui->m_objectTree->setUpdatesEnabled(true); - m_gui->m_objectTree->expandToDepth(0); + m_gui->m_objectTree->expand(m_proxyModel->index(0,0)); }); } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp index 65f7b0cdfb..ae0d18b077 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabIntegrationManager.cpp @@ -805,16 +805,15 @@ namespace AzToolsFramework } else { - QString cleanSaveAs(QDir::cleanPath(prefabPath)); + AZ::IO::FixedMaxPath lexicallyNormalPath = AZ::IO::PathView(prefabPath.toUtf8().constData()).LexicallyNormal(); bool isPathSafeForAssets = false; - for (AZStd::string assetSafeFolder : assetSafeFolders) + for (const AZStd::string& assetSafeFolder : assetSafeFolders) { - QString cleanAssetSafeFolder(QDir::cleanPath(assetSafeFolder.c_str())); - // Compare using clean paths so slash direction does not matter. - // Note that this comparison is case sensitive because some file systems - // Open 3D Engine supports are case sensitive. - if (cleanSaveAs.startsWith(cleanAssetSafeFolder)) + AZ::IO::PathView assetSafeFolderView(assetSafeFolder); + // Check if the prefabPath is relative to the safe asset directory. + // The Path classes are being used to make this check case insensitive. + if (lexicallyNormalPath.IsRelativeTo(assetSafeFolderView)) { isPathSafeForAssets = true; break; diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp index 7c2c65d204..447f94fc15 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.cpp @@ -21,10 +21,16 @@ namespace AzToolsFramework { + const QColor PrefabUiHandler::m_backgroundColor = QColor("#444444"); + const QColor PrefabUiHandler::m_backgroundHoverColor = QColor("#5A5A5A"); + const QColor PrefabUiHandler::m_backgroundSelectedColor = QColor("#656565"); const QColor PrefabUiHandler::m_prefabCapsuleColor = QColor("#1E252F"); + const QColor PrefabUiHandler::m_prefabCapsuleDisabledColor = QColor("#35383C"); const QColor PrefabUiHandler::m_prefabCapsuleEditColor = QColor("#4A90E2"); const QString PrefabUiHandler::m_prefabIconPath = QString(":/Entity/prefab.svg"); const QString PrefabUiHandler::m_prefabEditIconPath = QString(":/Entity/prefab_edit.svg"); + const QString PrefabUiHandler::m_prefabEditOpenIconPath = QString(":/Entity/prefab_edit_open.svg"); + const QString PrefabUiHandler::m_prefabEditCloseIconPath = QString(":/Entity/prefab_edit_close.svg"); PrefabUiHandler::PrefabUiHandler() { @@ -75,7 +81,7 @@ namespace AzToolsFramework if (!path.empty()) { - tooltip = QObject::tr("%1").arg(path.Native().data()); + tooltip = QObject::tr("Double click to edit.\n%1").arg(path.Native().data()); } return tooltip; @@ -102,13 +108,20 @@ namespace AzToolsFramework AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName; const bool isLastColumn = index.column() == EntityOutlinerListModel::ColumnLockToggle; - const bool hasVisibleChildren = index.data(EntityOutlinerListModel::ExpandedRole).value() && index.model()->hasChildren(index); + QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName); + const bool hasVisibleChildren = + firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value() && + firstColumnIndex.model()->hasChildren(firstColumnIndex); QColor backgroundColor = m_prefabCapsuleColor; if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { backgroundColor = m_prefabCapsuleEditColor; } + else if (!(option.state & QStyle::State_Enabled)) + { + backgroundColor = m_prefabCapsuleDisabledColor; + } QPainterPath backgroundPath; backgroundPath.setFillRule(Qt::WindingFill); @@ -184,7 +197,8 @@ namespace AzToolsFramework const bool isFirstColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnName; const bool isLastColumn = descendantIndex.column() == EntityOutlinerListModel::ColumnLockToggle; - QColor borderColor = m_prefabCapsuleColor; + // There is no legal way of opening prefabs in their default state, so default to disabled. + QColor borderColor = m_prefabCapsuleDisabledColor; if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) { borderColor = m_prefabCapsuleEditColor; @@ -273,6 +287,71 @@ namespace AzToolsFramework painter->restore(); } + void PrefabUiHandler::PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, [[maybe_unused]] const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + const QPoint offset = QPoint(-18, 3); + QModelIndex firstColumnIndex = index.siblingAtColumn(EntityOutlinerListModel::ColumnName); + const int iconSize = 16; + const bool isHovered = (option.state & QStyle::State_MouseOver); + const bool isSelected = index.data(EntityOutlinerListModel::SelectedRole).template value(); + const bool isFirstColumn = index.column() == EntityOutlinerListModel::ColumnName; + const bool isExpanded = + firstColumnIndex.data(EntityOutlinerListModel::ExpandedRole).value() && + firstColumnIndex.model()->hasChildren(firstColumnIndex); + + if (!isFirstColumn || !(option.state & QStyle::State_Enabled)) + { + return; + } + + painter->save(); + painter->setRenderHint(QPainter::Antialiasing, true); + + if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + // Only show the close icon if the prefab is expanded. + // This allows the prefab container to be opened if it was collapsed during propagation. + if (!isExpanded) + { + return; + } + + // Use the same color as the background. + QColor backgroundColor = m_backgroundColor; + if (isSelected) + { + backgroundColor = m_backgroundSelectedColor; + } + else if (isHovered) + { + backgroundColor = m_backgroundHoverColor; + } + + // Paint a rect to cover up the expander. + QRect rect = QRect(0, 0, 16, 16); + rect.translate(option.rect.topLeft() + offset); + painter->fillRect(rect, backgroundColor); + + // Paint the icon. + QIcon closeIcon = QIcon(m_prefabEditCloseIconPath); + painter->drawPixmap(option.rect.topLeft() + offset, closeIcon.pixmap(iconSize)); + } + else + { + // Only show the edit icon on hover. + if (!isHovered) + { + return; + } + + QIcon openIcon = QIcon(m_prefabEditOpenIconPath); + painter->drawPixmap(option.rect.topLeft() + offset, openIcon.pixmap(iconSize)); + } + + painter->restore(); + } + bool PrefabUiHandler::IsLastVisibleChild(const QModelIndex& parent, const QModelIndex& child) { QModelIndex lastVisibleItemIndex = GetLastVisibleChild(parent); @@ -314,9 +393,53 @@ namespace AzToolsFramework return Internal_GetLastVisibleChild(model, lastChild); } - void PrefabUiHandler::OnDoubleClick(AZ::EntityId entityId) const + bool PrefabUiHandler::OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + const QPoint offset = QPoint(-18, 3); + + if (m_prefabFocusPublicInterface->IsOwningPrefabInFocusHierarchy(entityId)) + { + QRect iconRect = QRect(0, 0, 16, 16); + iconRect.translate(option.rect.topLeft() + offset); + + if (iconRect.contains(position)) + { + if (!m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + // Focus on this prefab. + m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + } + + // Don't propagate event. + return true; + } + } + + return false; + } + + void PrefabUiHandler::OnOutlinerItemCollapse(const QModelIndex& index) const + { + AZ::EntityId entityId(index.data(EntityOutlinerListModel::EntityIdRole).value()); + + if (m_prefabFocusPublicInterface->IsOwningPrefabBeingFocused(entityId)) + { + auto editorEntityContextId = AzFramework::EntityContextId::CreateNull(); + EditorEntityContextRequestBus::BroadcastResult(editorEntityContextId, &EditorEntityContextRequests::GetEditorEntityContextId); + + // Go one level up. + int length = m_prefabFocusPublicInterface->GetPrefabFocusPathLength(editorEntityContextId); + m_prefabFocusPublicInterface->FocusOnPathIndex(editorEntityContextId, length - 2); + } + } + + bool PrefabUiHandler::OnEntityDoubleClick(AZ::EntityId entityId) const { // Focus on this prefab m_prefabFocusPublicInterface->FocusOnOwningPrefab(entityId); + + // Don't propagate event. + return true; } } diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h index 7c68d9fd95..6c78afc5b7 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/UI/Prefab/PrefabUiHandler.h @@ -36,7 +36,10 @@ namespace AzToolsFramework void PaintItemBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; void PaintDescendantBackground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index, const QModelIndex& descendantIndex) const override; - void OnDoubleClick(AZ::EntityId entityId) const override; + void PaintItemForeground(QPainter* painter, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + bool OnOutlinerItemClick(const QPoint& position, const QStyleOptionViewItem& option, const QModelIndex& index) const override; + void OnOutlinerItemCollapse(const QModelIndex& index) const override; + bool OnEntityDoubleClick(AZ::EntityId entityId) const override; private: Prefab::PrefabFocusPublicInterface* m_prefabFocusPublicInterface = nullptr; @@ -48,9 +51,15 @@ namespace AzToolsFramework static constexpr int m_prefabCapsuleRadius = 6; static constexpr int m_prefabBorderThickness = 2; + static const QColor m_backgroundColor; + static const QColor m_backgroundHoverColor; + static const QColor m_backgroundSelectedColor; static const QColor m_prefabCapsuleColor; + static const QColor m_prefabCapsuleDisabledColor; static const QColor m_prefabCapsuleEditColor; static const QString m_prefabIconPath; static const QString m_prefabEditIconPath; + static const QString m_prefabEditOpenIconPath; + static const QString m_prefabEditCloseIconPath; }; } // namespace AzToolsFramework diff --git a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp index a289d914d6..43e2a6793f 100644 --- a/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp +++ b/Code/Framework/AzToolsFramework/AzToolsFramework/ViewportUi/ViewportUiDisplay.cpp @@ -20,7 +20,7 @@ namespace AzToolsFramework::ViewportUi::Internal { const static int HighlightBorderSize = 5; const static int TopHighlightBorderSize = 25; - const static char* HighlightBorderColor = "#44B2F8"; + const static char* HighlightBorderColor = "#4A90E2"; static void UnparentWidgets(ViewportUiElementIdInfoLookup& viewportUiElementIdInfoLookup) { diff --git a/Code/Tools/ProjectManager/CMakeLists.txt b/Code/Tools/ProjectManager/CMakeLists.txt index 28c1871616..d34abcbc6c 100644 --- a/Code/Tools/ProjectManager/CMakeLists.txt +++ b/Code/Tools/ProjectManager/CMakeLists.txt @@ -43,7 +43,7 @@ ly_add_target( 3rdParty::pybind11 AZ::AzCore AZ::AzFramework - AZ::AzQtComponents + AZ::AzToolsFramework ) ly_add_target( diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc index aeaf9a9248..8dd7e4c9b5 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qrc +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qrc @@ -40,5 +40,6 @@ Delete.svg Download.svg in_progress.gif + gem.svg diff --git a/Code/Tools/ProjectManager/Resources/ProjectManager.qss b/Code/Tools/ProjectManager/Resources/ProjectManager.qss index 5f7826dbac..712c01aa02 100644 --- a/Code/Tools/ProjectManager/Resources/ProjectManager.qss +++ b/Code/Tools/ProjectManager/Resources/ProjectManager.qss @@ -61,6 +61,24 @@ QTabBar::tab:focus { color: #4082eb; } +#ToastNotification { + background-color: black; + border-radius: 20px; + border:1px solid #dddddd; + qproperty-minimumSize: 100px 50px; +} + +#ToastNotification #icon_frame { + border-radius: 4px; + qproperty-minimumSize: 44px 20px; +} + +#ToastNotification #iconLabel { + qproperty-minimumSize: 30px 20px; + qproperty-maximumSize: 30px 20px; + margin-left: 6px; +} + /************** General (Forms) **************/ #formLineEditWidget, @@ -218,6 +236,10 @@ QTabBar::tab:focus { color: #666666; } +#verticalSeparatingLine { + color: #888888; +} + /************** Project Settings **************/ #projectSettings { margin-top:42px; @@ -481,6 +503,26 @@ QProgressBar::chunk { font-weight: 600; } +#gemCatalogMenuButton { + qproperty-flat: true; + max-width:36px; + min-width:36px; + max-height:24px; + min-height:24px; +} + +#GemCatalogCartOverlayGemDownloadHeader { + margin:0; + padding: 0px; + background-color: #333333; +} + +#GemCatalogCartOverlayGemDownloadBG { + margin:0; + padding: 0px; + background-color: #444444; +} + #GemCatalogHeaderLabel { font-size: 12px; color: #FFFFFF; diff --git a/Code/Tools/ProjectManager/Resources/gem.svg b/Code/Tools/ProjectManager/Resources/gem.svg new file mode 100644 index 0000000000..2b688d5db5 --- /dev/null +++ b/Code/Tools/ProjectManager/Resources/gem.svg @@ -0,0 +1,3 @@ + + + diff --git a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp index 84b931cb30..65e01803aa 100644 --- a/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/CreateProjectCtrl.cpp @@ -49,6 +49,8 @@ namespace O3DE::ProjectManager m_stack->addWidget(m_gemCatalogScreen); vLayout->addWidget(m_stack); + connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, &CreateProjectCtrl::OnChangeScreenRequest); + // When there are multiple project templates present, we re-gather the gems when changing the selected the project template. connect(m_newProjectSettingsScreen, &NewProjectSettingsScreen::OnTemplateSelectionChanged, this, [=](int oldIndex, [[maybe_unused]] int newIndex) { @@ -133,7 +135,7 @@ namespace O3DE::ProjectManager } else { - emit GotoPreviousScreenRequest(); + emit GoToPreviousScreenRequest(); } } diff --git a/Code/Tools/ProjectManager/Source/DownloadController.cpp b/Code/Tools/ProjectManager/Source/DownloadController.cpp new file mode 100644 index 0000000000..fa3fdb10d1 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadController.cpp @@ -0,0 +1,84 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include + +#include + + +namespace O3DE::ProjectManager +{ + DownloadController::DownloadController(QWidget* parent) + : QObject() + , m_lastProgress(0) + , m_parent(parent) + { + m_worker = new DownloadWorker(); + m_worker->moveToThread(&m_workerThread); + + connect(&m_workerThread, &QThread::started, m_worker, &DownloadWorker::StartDownload); + connect(m_worker, &DownloadWorker::Done, this, &DownloadController::HandleResults); + connect(m_worker, &DownloadWorker::UpdateProgress, this, &DownloadController::UpdateUIProgress); + connect(this, &DownloadController::StartGemDownload, m_worker, &DownloadWorker::StartDownload); + } + + DownloadController::~DownloadController() + { + connect(&m_workerThread, &QThread::finished, m_worker, &DownloadController::deleteLater); + m_workerThread.requestInterruption(); + m_workerThread.quit(); + m_workerThread.wait(); + } + + void DownloadController::AddGemDownload(const QString& gemName) + { + m_gemNames.push_back(gemName); + if (m_gemNames.size() == 1) + { + m_worker->SetGemToDownload(m_gemNames[0], false); + m_workerThread.start(); + } + } + + void DownloadController::UpdateUIProgress(int progress) + { + m_lastProgress = progress; + emit GemDownloadProgress(progress); + } + + void DownloadController::HandleResults(const QString& result) + { + bool succeeded = true; + + if (!result.isEmpty()) + { + QMessageBox::critical(nullptr, tr("Gem download"), result); + succeeded = false; + } + + m_gemNames.erase(m_gemNames.begin()); + emit Done(succeeded); + + if (!m_gemNames.empty()) + { + emit StartGemDownload(m_gemNames[0]); + } + else + { + m_workerThread.quit(); + m_workerThread.wait(); + } + } + + void DownloadController::HandleCancel() + { + m_workerThread.quit(); + emit Done(false); + } +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadController.h b/Code/Tools/ProjectManager/Source/DownloadController.h new file mode 100644 index 0000000000..608d9b1a2b --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadController.h @@ -0,0 +1,72 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#include +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QProcess) + +namespace O3DE::ProjectManager +{ + QT_FORWARD_DECLARE_CLASS(DownloadWorker) + + class DownloadController : public QObject + { + Q_OBJECT + + public: + explicit DownloadController(QWidget* parent = nullptr); + ~DownloadController(); + + void AddGemDownload(const QString& m_gemName); + + bool IsDownloadQueueEmpty() + { + return m_gemNames.empty(); + } + + const AZStd::vector& GetDownloadQueue() const + { + return m_gemNames; + } + + const QString& GetCurrentDownloadingGem() const + { + if (!m_gemNames.empty()) + { + return m_gemNames[0]; + } + else + { + static const QString emptyString; + return emptyString; + } + } + public slots: + void UpdateUIProgress(int progress); + void HandleResults(const QString& result); + void HandleCancel(); + + signals: + void StartGemDownload(const QString& gemName); + void Done(bool success = true); + void GemDownloadProgress(int percentage); + + private: + DownloadWorker* m_worker; + QThread m_workerThread; + QWidget* m_parent; + AZStd::vector m_gemNames; + + int m_lastProgress; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadWorker.cpp b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp new file mode 100644 index 0000000000..9bda1b34cc --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadWorker.cpp @@ -0,0 +1,48 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + + +namespace O3DE::ProjectManager +{ + DownloadWorker::DownloadWorker() + : QObject() + { + } + + void DownloadWorker::StartDownload() + { + auto gemDownloadProgress = [=](int downloadProgress) + { + m_downloadProgress = downloadProgress; + emit UpdateProgress(downloadProgress); + }; + AZ::Outcome gemInfoResult = PythonBindingsInterface::Get()->DownloadGem(m_gemName, gemDownloadProgress); + if (gemInfoResult.IsSuccess()) + { + emit Done(""); + } + else + { + emit Done(tr("Gem download failed")); + } + } + + void DownloadWorker::SetGemToDownload(const QString& gemName, bool downloadNow) + { + m_gemName = gemName; + if (downloadNow) + { + StartDownload(); + } + } + +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/DownloadWorker.h b/Code/Tools/ProjectManager/Source/DownloadWorker.h new file mode 100644 index 0000000000..316a730a78 --- /dev/null +++ b/Code/Tools/ProjectManager/Source/DownloadWorker.h @@ -0,0 +1,42 @@ +/* + * 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 + +#if !defined(Q_MOC_RUN) +#include +#endif + +QT_FORWARD_DECLARE_CLASS(QProcess) + +namespace O3DE::ProjectManager +{ + class DownloadWorker : public QObject + { + // Download was cancelled + inline static const QString DownloadCancelled = QObject::tr("Download Cancelled."); + + Q_OBJECT + + public: + explicit DownloadWorker(); + ~DownloadWorker() = default; + + public slots: + void StartDownload(); + void SetGemToDownload(const QString& gemName, bool downloadNow = true); + + signals: + void UpdateProgress(int progress); + void Done(QString result = ""); + + private: + + QString m_gemName; + int m_downloadProgress; + }; +} // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp index c78a9426db..f30a8e0daa 100644 --- a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.cpp @@ -29,17 +29,17 @@ namespace O3DE::ProjectManager topBarFrameWidget->setLayout(topBarHLayout); - QTabWidget* tabWidget = new QTabWidget(); - tabWidget->setObjectName("engineTab"); - tabWidget->tabBar()->setObjectName("engineTabBar"); - tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus); + m_tabWidget = new QTabWidget(); + m_tabWidget->setObjectName("engineTab"); + m_tabWidget->tabBar()->setObjectName("engineTabBar"); + m_tabWidget->tabBar()->setFocusPolicy(Qt::TabFocus); m_engineSettingsScreen = new EngineSettingsScreen(); m_gemRepoScreen = new GemRepoScreen(); - tabWidget->addTab(m_engineSettingsScreen, tr("General")); - tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories")); - topBarHLayout->addWidget(tabWidget); + m_tabWidget->addTab(m_engineSettingsScreen, tr("General")); + m_tabWidget->addTab(m_gemRepoScreen, tr("Gem Repositories")); + topBarHLayout->addWidget(m_tabWidget); vLayout->addWidget(topBarFrameWidget); @@ -61,4 +61,28 @@ namespace O3DE::ProjectManager return true; } + bool EngineScreenCtrl::ContainsScreen(ProjectManagerScreen screen) + { + if (screen == m_engineSettingsScreen->GetScreenEnum() || screen == m_gemRepoScreen->GetScreenEnum()) + { + return true; + } + + return false; + } + + void EngineScreenCtrl::GoToScreen(ProjectManagerScreen screen) + { + if (screen == m_engineSettingsScreen->GetScreenEnum()) + { + m_tabWidget->setCurrentWidget(m_engineSettingsScreen); + m_engineSettingsScreen->NotifyCurrentScreen(); + } + else if (screen == m_gemRepoScreen->GetScreenEnum()) + { + m_tabWidget->setCurrentWidget(m_gemRepoScreen); + m_gemRepoScreen->NotifyCurrentScreen(); + } + } + } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h index 9e799f13e7..b7142ba226 100644 --- a/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h +++ b/Code/Tools/ProjectManager/Source/EngineScreenCtrl.h @@ -11,6 +11,8 @@ #include #endif +QT_FORWARD_DECLARE_CLASS(QTabWidget) + namespace O3DE::ProjectManager { QT_FORWARD_DECLARE_CLASS(EngineSettingsScreen) @@ -26,7 +28,10 @@ namespace O3DE::ProjectManager QString GetTabText() override; bool IsTab() override; + bool ContainsScreen(ProjectManagerScreen screen) override; + void GoToScreen(ProjectManagerScreen screen) override; + QTabWidget* m_tabWidget = nullptr; EngineSettingsScreen* m_engineSettingsScreen = nullptr; GemRepoScreen* m_gemRepoScreen = nullptr; }; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp index 9fca6040d4..0ecea215bf 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.cpp @@ -8,17 +8,21 @@ #include #include +#include + #include #include #include #include -#include +#include +#include namespace O3DE::ProjectManager { - CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, QWidget* parent) + CartOverlayWidget::CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent) : QWidget(parent) , m_gemModel(gemModel) + , m_downloadController(downloadController) { setObjectName("GemCatalogCart"); @@ -42,6 +46,9 @@ namespace O3DE::ProjectManager hLayout->addWidget(closeButton); m_layout->addLayout(hLayout); + // downloading gems + CreateDownloadSection(); + // added CreateGemSection( tr("Gem to be activated"), tr("Gems to be activated"), [=] { @@ -149,6 +156,109 @@ namespace O3DE::ProjectManager update(); } + void CartOverlayWidget::CreateDownloadSection() + { + QWidget* widget = new QWidget(); + widget->setFixedWidth(s_width); + m_layout->addWidget(widget); + + QVBoxLayout* layout = new QVBoxLayout(); + layout->setAlignment(Qt::AlignTop); + widget->setLayout(layout); + + QLabel* titleLabel = new QLabel(); + titleLabel->setObjectName("GemCatalogCartOverlaySectionLabel"); + layout->addWidget(titleLabel); + + titleLabel->setText(tr("Gems to be installed")); + + // Create header section + QWidget* downloadingGemsWidget = new QWidget(); + downloadingGemsWidget->setObjectName("GemCatalogCartOverlayGemDownloadHeader"); + layout->addWidget(downloadingGemsWidget); + QVBoxLayout* gemDownloadLayout = new QVBoxLayout(); + gemDownloadLayout->setMargin(0); + gemDownloadLayout->setAlignment(Qt::AlignTop); + downloadingGemsWidget->setLayout(gemDownloadLayout); + QLabel* processingQueueLabel = new QLabel("Processing Queue"); + gemDownloadLayout->addWidget(processingQueueLabel); + + QWidget* downloadingItemWidget = new QWidget(); + downloadingItemWidget->setObjectName("GemCatalogCartOverlayGemDownloadBG"); + gemDownloadLayout->addWidget(downloadingItemWidget); + QVBoxLayout* downloadingItemLayout = new QVBoxLayout(); + downloadingItemLayout->setAlignment(Qt::AlignTop); + downloadingItemWidget->setLayout(downloadingItemLayout); + + auto update = [=](int downloadProgress) + { + if (m_downloadController->IsDownloadQueueEmpty()) + { + widget->hide(); + } + else + { + widget->setUpdatesEnabled(false); + // remove items + QLayoutItem* layoutItem = nullptr; + while ((layoutItem = downloadingItemLayout->takeAt(0)) != nullptr) + { + if (layoutItem->layout()) + { + // Gem info row + QLayoutItem* rowLayoutItem = nullptr; + while ((rowLayoutItem = layoutItem->layout()->takeAt(0)) != nullptr) + { + rowLayoutItem->widget()->deleteLater(); + } + layoutItem->layout()->deleteLater(); + } + if (layoutItem->widget()) + { + layoutItem->widget()->deleteLater(); + } + } + + // Setup gem download rows + const AZStd::vector& downloadQueue = m_downloadController->GetDownloadQueue(); + + QLabel* downloadsInProgessLabel = new QLabel(""); + downloadsInProgessLabel->setText( + QString("%1 %2").arg(downloadQueue.size()).arg(downloadQueue.size() == 1 ? tr("download in progress...") : tr("downloads in progress..."))); + downloadingItemLayout->addWidget(downloadsInProgessLabel); + + for (int downloadingGemNumber = 0; downloadingGemNumber < downloadQueue.size(); ++downloadingGemNumber) + { + QHBoxLayout* nameProgressLayout = new QHBoxLayout(); + TagWidget* newTag = new TagWidget(downloadQueue[downloadingGemNumber]); + nameProgressLayout->addWidget(newTag); + QLabel* progress = new QLabel(downloadingGemNumber == 0? QString("%1%").arg(downloadProgress) : tr("Queued")); + nameProgressLayout->addWidget(progress); + QSpacerItem* spacer = new QSpacerItem(0, 0, QSizePolicy::Expanding, QSizePolicy::Minimum); + nameProgressLayout->addSpacerItem(spacer); + QLabel* cancelText = new QLabel(tr("Cancel")); + nameProgressLayout->addWidget(cancelText); + downloadingItemLayout->addLayout(nameProgressLayout); + QProgressBar* downloadProgessBar = new QProgressBar(); + downloadingItemLayout->addWidget(downloadProgessBar); + downloadProgessBar->setValue(downloadingGemNumber == 0 ? downloadProgress : 0); + } + + widget->setUpdatesEnabled(true); + widget->show(); + } + }; + + auto downloadEnded = [=](bool /*success*/) + { + update(0); // update the list to remove the gem that has finished + }; + // connect to download controller data changed + connect(m_downloadController, &DownloadController::GemDownloadProgress, this, update); + connect(m_downloadController, &DownloadController::Done, this, downloadEnded); + update(0); + } + QStringList CartOverlayWidget::ConvertFromModelIndices(const QVector& gems) const { QStringList gemNames; @@ -160,9 +270,10 @@ namespace O3DE::ProjectManager return gemNames; } - CartButton::CartButton(GemModel* gemModel, QWidget* parent) + CartButton::CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent) : QWidget(parent) , m_gemModel(gemModel) + , m_downloadController(downloadController) { m_layout = new QHBoxLayout(); m_layout->setMargin(0); @@ -239,7 +350,7 @@ namespace O3DE::ProjectManager delete m_cartOverlay; } - m_cartOverlay = new CartOverlayWidget(m_gemModel, this); + m_cartOverlay = new CartOverlayWidget(m_gemModel, m_downloadController, this); connect(m_cartOverlay, &QWidget::destroyed, this, [=] { // Reset the overlay pointer on destruction to prevent dangling pointers. @@ -265,7 +376,7 @@ namespace O3DE::ProjectManager } } - GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent) + GemCatalogHeaderWidget::GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent) : QFrame(parent) { QHBoxLayout* hLayout = new QHBoxLayout(); @@ -293,8 +404,30 @@ namespace O3DE::ProjectManager hLayout->addSpacerItem(new QSpacerItem(0, 0, QSizePolicy::Expanding)); hLayout->addSpacerItem(new QSpacerItem(75, 0, QSizePolicy::Fixed)); - CartButton* cartButton = new CartButton(gemModel); + CartButton* cartButton = new CartButton(gemModel, downloadController); hLayout->addWidget(cartButton); + + hLayout->addSpacing(16); + + // Separating line + QFrame* vLine = new QFrame(); + vLine->setFrameShape(QFrame::VLine); + vLine->setObjectName("verticalSeparatingLine"); + hLayout->addWidget(vLine); + + hLayout->addSpacing(16); + + QMenu* gemMenu = new QMenu(this); + m_openGemReposAction = gemMenu->addAction(tr("Show Gem Repos")); + + connect(m_openGemReposAction, &QAction::triggered, this,[this](){ emit OpenGemsRepo(); }); + + QPushButton* gemMenuButton = new QPushButton(this); + gemMenuButton->setObjectName("gemCatalogMenuButton"); + gemMenuButton->setMenu(gemMenu); + gemMenuButton->setIcon(QIcon(":/menu.svg")); + gemMenuButton->setIconSize(QSize(36, 24)); + hLayout->addWidget(gemMenuButton); } void GemCatalogHeaderWidget::ReinitForProject() diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h index 2cfda4c790..fa381e54ae 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogHeaderWidget.h @@ -15,12 +15,15 @@ #include #include #include +#include + #include #include #include #include #include #include +#include #endif namespace O3DE::ProjectManager @@ -31,16 +34,18 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - CartOverlayWidget(GemModel* gemModel, QWidget* parent = nullptr); + CartOverlayWidget(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr); private: QStringList ConvertFromModelIndices(const QVector& gems) const; using GetTagIndicesCallback = AZStd::function()>; void CreateGemSection(const QString& singularTitle, const QString& pluralTitle, GetTagIndicesCallback getTagIndices); + void CreateDownloadSection(); QVBoxLayout* m_layout = nullptr; GemModel* m_gemModel = nullptr; + DownloadController* m_downloadController = nullptr; inline constexpr static int s_width = 240; }; @@ -51,7 +56,7 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - CartButton(GemModel* gemModel, QWidget* parent = nullptr); + CartButton(GemModel* gemModel, DownloadController* downloadController, QWidget* parent = nullptr); ~CartButton(); void ShowOverlay(); @@ -64,6 +69,7 @@ namespace O3DE::ProjectManager QLabel* m_countLabel = nullptr; QPushButton* m_dropDownButton = nullptr; CartOverlayWidget* m_cartOverlay = nullptr; + DownloadController* m_downloadController = nullptr; inline constexpr static int s_iconSize = 24; inline constexpr static int s_arrowDownIconSize = 8; @@ -75,13 +81,18 @@ namespace O3DE::ProjectManager Q_OBJECT // AUTOMOC public: - explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, QWidget* parent = nullptr); + explicit GemCatalogHeaderWidget(GemModel* gemModel, GemSortFilterProxyModel* filterProxyModel, DownloadController* downloadController, QWidget* parent = nullptr); ~GemCatalogHeaderWidget() = default; void ReinitForProject(); + signals: + void OpenGemsRepo(); + private: AzQtComponents::SearchLineEdit* m_filterLineEdit = nullptr; inline constexpr static int s_height = 60; + + QAction* m_openGemReposAction = nullptr; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp index 945878768d..69c5844e84 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -32,9 +33,13 @@ namespace O3DE::ProjectManager vLayout->setSpacing(0); setLayout(vLayout); - m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel); + m_downloadController = new DownloadController(); + + m_headerWidget = new GemCatalogHeaderWidget(m_gemModel, m_proxModel, m_downloadController); vLayout->addWidget(m_headerWidget); + connect(m_headerWidget, &GemCatalogHeaderWidget::OpenGemsRepo, this, &GemCatalogScreen::HandleOpenGemRepo); + QHBoxLayout* hLayout = new QHBoxLayout(); hLayout->setMargin(0); vLayout->addLayout(hLayout); @@ -61,6 +66,9 @@ namespace O3DE::ProjectManager hLayout->addWidget(filterWidget); hLayout->addLayout(middleVLayout); hLayout->addWidget(m_gemInspector); + + m_notificationsView = AZStd::make_unique(this, AZ_CRC("GemCatalogNotificationsView")); + m_notificationsView->SetOffset(QPoint(10, 70)); } void GemCatalogScreen::ReinitForProject(const QString& projectPath) @@ -81,6 +89,7 @@ namespace O3DE::ProjectManager m_headerWidget->ReinitForProject(); connect(m_gemModel, &GemModel::dataChanged, m_filterWidget, &GemFilterWidget::ResetGemStatusFilter); + connect(m_gemModel, &GemModel::gemStatusChanged, this, &GemCatalogScreen::OnGemStatusChanged); // Select the first entry after everything got correctly sized QTimer::singleShot(200, [=]{ @@ -89,6 +98,72 @@ namespace O3DE::ProjectManager }); } + void GemCatalogScreen::OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies) + { + if (m_notificationsEnabled) + { + bool added = GemModel::IsAdded(modelIndex); + bool dependency = GemModel::IsAddedDependency(modelIndex); + + bool gemStateChanged = (added && !dependency) || (!added && !dependency); + if (!gemStateChanged && !numChangedDependencies) + { + // no actual changes made + return; + } + + QString notification; + if (gemStateChanged) + { + notification = GemModel::GetDisplayName(modelIndex); + if (numChangedDependencies > 0) + { + notification += " " + tr("and") + " "; + } + } + + if (numChangedDependencies == 1 ) + { + notification += "1 Gem " + tr("dependency"); + } + else if (numChangedDependencies > 1) + { + notification += QString("%d Gem ").arg(numChangedDependencies) + tr("dependencies"); + } + notification += " " + (added ? tr("activated") : tr("deactivated")); + + AzQtComponents::ToastConfiguration toastConfiguration(AzQtComponents::ToastType::Custom, notification, ""); + toastConfiguration.m_customIconImage = ":/gem.svg"; + toastConfiguration.m_borderRadius = 4; + toastConfiguration.m_duration = AZStd::chrono::milliseconds(3000); + m_notificationsView->ShowToastNotification(toastConfiguration); + } + } + + void GemCatalogScreen::hideEvent(QHideEvent* event) + { + ScreenWidget::hideEvent(event); + m_notificationsView->OnHide(); + } + + void GemCatalogScreen::showEvent(QShowEvent* event) + { + ScreenWidget::showEvent(event); + m_notificationsView->OnShow(); + } + + void GemCatalogScreen::resizeEvent(QResizeEvent* event) + { + ScreenWidget::resizeEvent(event); + m_notificationsView->UpdateToastPosition(); + } + + void GemCatalogScreen::moveEvent(QMoveEvent* event) + { + ScreenWidget::moveEvent(event); + m_notificationsView->UpdateToastPosition(); + } + void GemCatalogScreen::FillModel(const QString& projectPath) { AZ::Outcome, AZStd::string> allGemInfosResult = PythonBindingsInterface::Get()->GetAllGemInfos(projectPath); @@ -102,6 +177,7 @@ namespace O3DE::ProjectManager } m_gemModel->UpdateGemDependencies(); + m_notificationsEnabled = false; // Gather enabled gems for the given project. auto enabledGemNamesResult = PythonBindingsInterface::Get()->GetEnabledGemNames(projectPath); @@ -128,6 +204,8 @@ namespace O3DE::ProjectManager { QMessageBox::critical(nullptr, tr("Operation failed"), QString("Cannot retrieve enabled gems for project %1.\n\nError:\n%2").arg(projectPath, enabledGemNamesResult.GetError().c_str())); } + + m_notificationsEnabled = true; } else { @@ -191,6 +269,27 @@ namespace O3DE::ProjectManager return EnableDisableGemsResult::Success; } + void GemCatalogScreen::HandleOpenGemRepo() + { + QVector gemsToBeAdded = m_gemModel->GatherGemsToBeAdded(true); + QVector gemsToBeRemoved = m_gemModel->GatherGemsToBeRemoved(true); + + if (!gemsToBeAdded.empty() || !gemsToBeRemoved.empty()) + { + QMessageBox::StandardButton warningResult = QMessageBox::warning( + nullptr, "Pending Changes", + "There are some unsaved changes to the gem selection,
they will be lost if you change screens.
Are you sure?", + QMessageBox::No | QMessageBox::Yes); + + if (warningResult != QMessageBox::Yes) + { + return; + } + } + + emit ChangeScreenRequest(ProjectManagerScreen::GemRepos); + } + ProjectManagerScreen GemCatalogScreen::GetScreenEnum() { return ProjectManagerScreen::GemCatalog; diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h index 72e8d44f65..62528ba942 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemCatalogScreen.h @@ -10,6 +10,8 @@ #if !defined(Q_MOC_RUN) #include +#include +#include #include #include #include @@ -39,10 +41,26 @@ namespace O3DE::ProjectManager EnableDisableGemsResult EnableDisableGemsForProject(const QString& projectPath); GemModel* GetGemModel() const { return m_gemModel; } + DownloadController* GetDownloadController() const { return m_downloadController; } + + public slots: + void OnGemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + + protected: + void hideEvent(QHideEvent* event) override; + void showEvent(QShowEvent* event) override; + void resizeEvent(QResizeEvent* event) override; + void moveEvent(QMoveEvent* event) override; + + private slots: + void HandleOpenGemRepo(); + private: void FillModel(const QString& projectPath); + AZStd::unique_ptr m_notificationsView; + GemListView* m_gemListView = nullptr; GemInspector* m_gemInspector = nullptr; GemModel* m_gemModel = nullptr; @@ -50,5 +68,7 @@ namespace O3DE::ProjectManager GemSortFilterProxyModel* m_proxModel = nullptr; QVBoxLayout* m_filterWidgetLayout = nullptr; GemFilterWidget* m_filterWidget = nullptr; + DownloadController* m_downloadController = nullptr; + bool m_notificationsEnabled = true; }; } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp index 35491f4ddd..acdef483ae 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.cpp @@ -10,6 +10,7 @@ #include #include #include +#include namespace O3DE::ProjectManager { @@ -299,23 +300,50 @@ namespace O3DE::ProjectManager AZ_Assert(gemModel, "Failed to obtain GemModel"); QVector dependencies = gemModel->GatherGemDependencies(modelIndex); + uint32_t numChangedDependencies = 0; + if (IsAdded(modelIndex)) { for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, true); + if (!IsAddedDependency(dependency)) + { + SetIsAddedDependency(*gemModel, dependency, true); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } else { // still a dependency if some added gem depends on this one - SetIsAddedDependency(model, modelIndex, gemModel->HasDependentGems(modelIndex)); + bool hasDependentGems = gemModel->HasDependentGems(modelIndex); + if (IsAddedDependency(modelIndex) != hasDependentGems) + { + SetIsAddedDependency(model, modelIndex, hasDependentGems); + } for (const QModelIndex& dependency : dependencies) { - SetIsAddedDependency(*gemModel, dependency, gemModel->HasDependentGems(dependency)); + hasDependentGems = gemModel->HasDependentGems(dependency); + if (IsAddedDependency(dependency) != hasDependentGems) + { + SetIsAddedDependency(*gemModel, dependency, hasDependentGems); + + // if the gem was already added then the state didn't really change + if (!IsAdded(dependency)) + { + numChangedDependencies++; + } + } } } + + gemModel->emit gemStatusChanged(modelIndex, numChangedDependencies); } void GemModel::SetIsAddedDependency(QAbstractItemModel& model, const QModelIndex& modelIndex, bool isAdded) @@ -488,5 +516,4 @@ namespace O3DE::ProjectManager } return result; } - } // namespace O3DE::ProjectManager diff --git a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h index 0d1c225f74..938543eb39 100644 --- a/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h +++ b/Code/Tools/ProjectManager/Source/GemCatalog/GemModel.h @@ -77,6 +77,9 @@ namespace O3DE::ProjectManager int TotalAddedGems(bool includeDependencies = false) const; + signals: + void gemStatusChanged(const QModelIndex& modelIndex, uint32_t numChangedDependencies); + private: void FindGemDisplayNamesByNameStrings(QStringList& inOutGemNames); void GetAllDependingGems(const QModelIndex& modelIndex, QSet& inOutGems); diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.cpp b/Code/Tools/ProjectManager/Source/PythonBindings.cpp index 633116e8b6..bc8773b0c8 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.cpp +++ b/Code/Tools/ProjectManager/Source/PythonBindings.cpp @@ -301,6 +301,7 @@ namespace O3DE::ProjectManager m_enableGemProject = pybind11::module::import("o3de.enable_gem"); m_disableGemProject = pybind11::module::import("o3de.disable_gem"); m_editProjectProperties = pybind11::module::import("o3de.project_properties"); + m_download = pybind11::module::import("o3de.download"); m_pathlib = pybind11::module::import("pathlib"); // make sure the engine is registered @@ -1075,4 +1076,30 @@ namespace O3DE::ProjectManager std::sort(gemRepos.begin(), gemRepos.end()); return AZ::Success(AZStd::move(gemRepos)); } + + AZ::Outcome PythonBindings::DownloadGem(const QString& gemName, std::function gemProgressCallback) + { + bool downloadSucceeded = false; + auto result = ExecuteWithLockErrorHandling( + [&] + { + auto downloadResult = m_download.attr("download_gem")( + QString_To_Py_String(gemName), // gem name + pybind11::none(), // destination path + false// skip auto register + ); + downloadSucceeded = (downloadResult.cast() == 0); + }); + + if (!result.IsSuccess()) + { + return result; + } + else if (!downloadSucceeded) + { + return AZ::Failure("Failed to download gem."); + } + + return AZ::Success(); + } } diff --git a/Code/Tools/ProjectManager/Source/PythonBindings.h b/Code/Tools/ProjectManager/Source/PythonBindings.h index d0704d0bd1..638ce6b1d4 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindings.h +++ b/Code/Tools/ProjectManager/Source/PythonBindings.h @@ -61,6 +61,7 @@ namespace O3DE::ProjectManager bool AddGemRepo(const QString& repoUri) override; bool RemoveGemRepo(const QString& repoUri) override; AZ::Outcome, AZStd::string> GetAllGemRepoInfos() override; + AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) override; private: AZ_DISABLE_COPY_MOVE(PythonBindings); @@ -87,6 +88,7 @@ namespace O3DE::ProjectManager pybind11::handle m_enableGemProject; pybind11::handle m_disableGemProject; pybind11::handle m_editProjectProperties; + pybind11::handle m_download; pybind11::handle m_pathlib; }; } diff --git a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h index 5daf543c11..19442540a4 100644 --- a/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h +++ b/Code/Tools/ProjectManager/Source/PythonBindingsInterface.h @@ -187,6 +187,8 @@ namespace O3DE::ProjectManager * @return A list of gem repo infos. */ virtual AZ::Outcome, AZStd::string> GetAllGemRepoInfos() = 0; + + virtual AZ::Outcome DownloadGem(const QString& gemName, std::function gemProgressCallback) = 0; }; using PythonBindingsInterface = AZ::Interface; diff --git a/Code/Tools/ProjectManager/Source/ScreenWidget.h b/Code/Tools/ProjectManager/Source/ScreenWidget.h index 9563fc2f6a..148dcdb8c8 100644 --- a/Code/Tools/ProjectManager/Source/ScreenWidget.h +++ b/Code/Tools/ProjectManager/Source/ScreenWidget.h @@ -47,6 +47,14 @@ namespace O3DE::ProjectManager return tr("Missing"); } + virtual bool ContainsScreen([[maybe_unused]] ProjectManagerScreen screen) + { + return false; + } + virtual void GoToScreen([[maybe_unused]] ProjectManagerScreen screen) + { + } + //! Notify this screen it is the current screen virtual void NotifyCurrentScreen() { @@ -55,7 +63,7 @@ namespace O3DE::ProjectManager signals: void ChangeScreenRequest(ProjectManagerScreen screen); - void GotoPreviousScreenRequest(); + void GoToPreviousScreenRequest(); void ResetScreenRequest(ProjectManagerScreen screen); void NotifyCurrentProject(const QString& projectPath); void NotifyBuildProject(const ProjectInfo& projectInfo); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp index df0bdb29f4..314765def0 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.cpp @@ -83,11 +83,28 @@ namespace O3DE::ProjectManager bool ScreensCtrl::ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit) { + ScreenWidget* newScreen = nullptr; + const auto iterator = m_screenMap.find(screen); if (iterator != m_screenMap.end()) + { + newScreen = iterator.value(); + } + else + { + // Check if screen is contained by another screen + for (ScreenWidget* checkingScreen : m_screenMap) + { + if (checkingScreen->ContainsScreen(screen)) + { + newScreen = checkingScreen; + break; + } + } + } + if (newScreen) { ScreenWidget* currentScreen = GetCurrentScreen(); - ScreenWidget* newScreen = iterator.value(); if (currentScreen != newScreen) { @@ -109,6 +126,11 @@ namespace O3DE::ProjectManager newScreen->NotifyCurrentScreen(); + if (iterator == m_screenMap.end()) + { + newScreen->GoToScreen(screen); + } + return true; } } @@ -116,7 +138,7 @@ namespace O3DE::ProjectManager return false; } - bool ScreensCtrl::GotoPreviousScreen() + bool ScreensCtrl::GoToPreviousScreen() { if (!m_screenVisitOrder.isEmpty()) { @@ -171,7 +193,7 @@ namespace O3DE::ProjectManager m_screenMap.insert(screen, newScreen); connect(newScreen, &ScreenWidget::ChangeScreenRequest, this, &ScreensCtrl::ChangeToScreen); - connect(newScreen, &ScreenWidget::GotoPreviousScreenRequest, this, &ScreensCtrl::GotoPreviousScreen); + connect(newScreen, &ScreenWidget::GoToPreviousScreenRequest, this, &ScreensCtrl::GoToPreviousScreen); connect(newScreen, &ScreenWidget::ResetScreenRequest, this, &ScreensCtrl::ResetScreen); connect(newScreen, &ScreenWidget::NotifyCurrentProject, this, &ScreensCtrl::NotifyCurrentProject); connect(newScreen, &ScreenWidget::NotifyBuildProject, this, &ScreensCtrl::NotifyBuildProject); diff --git a/Code/Tools/ProjectManager/Source/ScreensCtrl.h b/Code/Tools/ProjectManager/Source/ScreensCtrl.h index 7132d64dd0..ab69a09ea3 100644 --- a/Code/Tools/ProjectManager/Source/ScreensCtrl.h +++ b/Code/Tools/ProjectManager/Source/ScreensCtrl.h @@ -41,7 +41,7 @@ namespace O3DE::ProjectManager public slots: bool ChangeToScreen(ProjectManagerScreen screen); bool ForceChangeToScreen(ProjectManagerScreen screen, bool addVisit = true); - bool GotoPreviousScreen(); + bool GoToPreviousScreen(); void ResetScreen(ProjectManagerScreen screen); void ResetAllScreens(); void DeleteScreen(ProjectManagerScreen screen); diff --git a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp index e952ada57a..1c8f9a6931 100644 --- a/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp +++ b/Code/Tools/ProjectManager/Source/UpdateProjectCtrl.cpp @@ -40,6 +40,10 @@ namespace O3DE::ProjectManager m_updateSettingsScreen = new UpdateProjectSettingsScreen(); m_gemCatalogScreen = new GemCatalogScreen(); + connect(m_gemCatalogScreen, &ScreenWidget::ChangeScreenRequest, this, [this](ProjectManagerScreen screen){ + emit ChangeScreenRequest(screen); + }); + m_stack = new QStackedWidget(this); m_stack->setObjectName("body"); m_stack->setSizePolicy(QSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding)); @@ -118,7 +122,7 @@ namespace O3DE::ProjectManager { if (UpdateProjectSettings(true)) { - emit GotoPreviousScreenRequest(); + emit GoToPreviousScreenRequest(); } } } @@ -136,6 +140,11 @@ namespace O3DE::ProjectManager } else if (m_stack->currentIndex() == ScreenOrder::Gems && m_gemCatalogScreen) { + if (!m_gemCatalogScreen->GetDownloadController()->IsDownloadQueueEmpty()) + { + QMessageBox::critical(this, tr("Gems downloading"), tr("You must wait for gems to finish downloading before continuing.")); + return; + } // Enable or disable the gems that got adjusted in the gem catalog and apply them to the given project. const GemCatalogScreen::EnableDisableGemsResult result = m_gemCatalogScreen->EnableDisableGemsForProject(m_projectInfo.m_path); if (result == GemCatalogScreen::EnableDisableGemsResult::Failed) diff --git a/Code/Tools/ProjectManager/project_manager_files.cmake b/Code/Tools/ProjectManager/project_manager_files.cmake index fd8389ca4f..e2e35717f6 100644 --- a/Code/Tools/ProjectManager/project_manager_files.cmake +++ b/Code/Tools/ProjectManager/project_manager_files.cmake @@ -29,6 +29,10 @@ set(FILES Source/FormImageBrowseEditWidget.cpp Source/GemsSubWidget.h Source/GemsSubWidget.cpp + Source/DownloadController.h + Source/DownloadController.cpp + Source/DownloadWorker.h + Source/DownloadWorker.cpp Source/PathValidator.h Source/PathValidator.cpp Source/ProjectManagerWindow.h diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp index 79a39ff793..3edc7bbe67 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslCompiler.cpp @@ -404,18 +404,18 @@ namespace AZ bool AzslCompiler::ParseSrgPopulateRootConstantData(const rapidjson::Document& input, RootConstantData& rootConstantData) const { - if (input.HasMember("InlineConstantBuffer")) + if (input.HasMember("RootConstantBuffer")) { - const rapidjson::Value& rootConstantBufferValue = input["InlineConstantBuffer"]; - AZ_Assert(rootConstantBufferValue.IsObject(), "InlineConstantBuffer is not an object"); + const rapidjson::Value& rootConstantBufferValue = input["RootConstantBuffer"]; + AZ_Assert(rootConstantBufferValue.IsObject(), "RootConstantBuffer is not an object"); for (rapidjson::Value::ConstMemberIterator itr = rootConstantBufferValue.MemberBegin(); itr != rootConstantBufferValue.MemberEnd(); ++itr) { AZStd::string_view rootConstantBufferMemberName = itr->name.GetString(); const rapidjson::Value& rootConstantBufferMemberValue = itr->value; - if (rootConstantBufferMemberName == "bufferForInlineConstants") + if (rootConstantBufferMemberName == "bufferForRootConstants") { - AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForInlineConstants is not an object"); + AZ_Assert(rootConstantBufferMemberValue.IsObject(), "bufferForRootConstants is not an object"); for (rapidjson::Value::ConstMemberIterator itr2 = rootConstantBufferMemberValue.MemberBegin(); itr2 != rootConstantBufferMemberValue.MemberEnd(); ++itr2) { @@ -442,14 +442,14 @@ namespace AZ } } } - else if (rootConstantBufferMemberName == "inputsForInlineConstants") + else if (rootConstantBufferMemberName == "inputsForRootConstants") { - AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForInlineConstants is not an array"); + AZ_Assert(rootConstantBufferMemberValue.IsArray(), "inputsForRootConstants is not an array"); for (rapidjson::Value::ConstValueIterator itr2 = rootConstantBufferMemberValue.Begin(); itr2 != rootConstantBufferMemberValue.End(); ++itr2) { const rapidjson::Value& rootConstantBufferValue2 = *itr2; - AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForInlineConstants is not an object"); + AZ_Assert(rootConstantBufferValue2.IsObject(), "Entry in inputsForRootConstants is not an object"); SrgConstantData rootConstantInputs; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp index cacb310918..ffc5f40ef4 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/AzslShaderBuilderSystemComponent.cpp @@ -81,7 +81,7 @@ namespace AZ // Register Shader Asset Builder AssetBuilderSDK::AssetBuilderDesc shaderAssetBuilderDescriptor; shaderAssetBuilderDescriptor.m_name = "Shader Asset Builder"; - shaderAssetBuilderDescriptor.m_version = 104; // ATOM-15871 + shaderAssetBuilderDescriptor.m_version = 105; // [AZSL] Changing inlineConstant to rootConstant keyword work. // .shader file changes trigger rebuilds shaderAssetBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern( AZStd::string::format("*.%s", RPI::ShaderSourceData::Extension), AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); shaderAssetBuilderDescriptor.m_busId = azrtti_typeid(); @@ -96,7 +96,7 @@ namespace AZ 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 = 25; // ATOM-15871 + 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); diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp index e9ac18a432..2c87a79068 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderBuilderUtility.cpp @@ -183,7 +183,7 @@ namespace AZ // access the root constants reflection if (!azslc.ParseSrgPopulateRootConstantData( outcomes[AzslSubProducts::srg].GetValue(), - rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section) + rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section) { AZ_Error(builderName, false, "Failed to obtain root constant data reflection"); return AssetBuilderSDK::ProcessJobResult_Failed; diff --git a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp index 27ae90dcdc..bb40baca7d 100644 --- a/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp +++ b/Gems/Atom/Asset/Shader/Code/Source/Editor/ShaderVariantAssetBuilder.cpp @@ -561,7 +561,7 @@ namespace AZ // Access the root constants reflection if (!azslCompiler.ParseSrgPopulateRootConstantData( jsonOutcome.GetValue(), - rootConstantData)) // consuming data from --srg ("InlineConstantBuffer" subjson section) + rootConstantData)) // consuming data from --srg ("RootConstantBuffer" subjson section) { AZ_Error(ShaderVariantAssetBuilderName, false, "Failed to obtain root constant data reflection"); return false; diff --git a/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype index 17209771e5..30062205a8 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/ReflectionProbe/ReflectionProbeVisualization.materialtype @@ -1,7 +1,7 @@ { "description": "Base material for the reflection probe visualization model.", + "version": 1, "propertyLayout": { - "version": 1, "properties": { "general": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl index 69f86cbe9f..1fc8a410a1 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.azsl @@ -54,6 +54,7 @@ VSOutput ShadowCatcherVS(VSInput IN) DirectionalLightShadow::GetShadowCoords( ViewSrg::m_shadowIndexDirectionalLight, worldPosition, + OUT.m_worldNormal, OUT.m_shadowCoords); return OUT; diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype index 74246f85db..d05f03c9a9 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Special/ShadowCatcher.materialtype @@ -1,7 +1,7 @@ { "description": "Base material for the reflection probe visualization model.", + "version": 1, "propertyLayout": { - "version": 1, "properties": { "settings": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype index bed3b69c4c..f4bcfb2673 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR.materialtype @@ -1,7 +1,7 @@ { "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, "propertyLayout": { - "version": 3, "groups": [ { "name": "baseColor", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl index 11859de7d6..0f9771e480 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/EnhancedPBR_ForwardPass.azsl @@ -112,13 +112,15 @@ VSOutput EnhancedPbr_ForwardPassVS(VSInput IN) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depth) { + const float3 vertexNormal = normalize(IN.m_normal); + // ------- Tangents & Bitangets ------- float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz }; if ((o_parallax_feature_enabled && !o_enableSubsurfaceScattering) || o_normal_useTexture || (o_clearCoat_enabled && o_clearCoat_normal_useTexture) || o_detail_normal_useTexture) { - PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); + PrepareGeneratedTangent(vertexNormal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); } // ------- Depth & Parallax ------- @@ -137,7 +139,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float3x3 uvMatrix = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); float3x3 uvMatrixInverse = MaterialSrg::m_parallaxUvIndex == 0 ? MaterialSrg::m_uvMatrixInverse : CreateIdentity3x3(); - GetParallaxInput(IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex], MaterialSrg::m_heightmapScale, MaterialSrg::m_heightmapOffset, + GetParallaxInput(vertexNormal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex], MaterialSrg::m_heightmapScale, MaterialSrg::m_heightmapOffset, ObjectSrg::GetWorldMatrix(), uvMatrix, uvMatrixInverse, IN.m_uv[MaterialSrg::m_parallaxUvIndex], IN.m_worldPosition, depth, IN.m_position.w, displacementIsClipped); @@ -150,7 +152,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords); } } } @@ -185,7 +187,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex]; float3x3 uvMatrix = MaterialSrg::m_normalMapUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); // By design, only UV0 is allowed to apply transforms. float detailLayerNormalFactor = MaterialSrg::m_detail_normal_factor * detailLayerBlendFactor; - surface.vertexNormal = normalize(IN.m_normal); + surface.vertexNormal = vertexNormal; surface.normal = GetDetailedNormalInputWS( isFrontFace, IN.m_normal, tangents[MaterialSrg::m_normalMapUvIndex], bitangents[MaterialSrg::m_normalMapUvIndex], MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_normalFactor, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, uvMatrix, o_normal_useTexture, diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype index e4da9c6022..e2a05aa916 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/Skin.materialtype @@ -1,7 +1,7 @@ { "description": "Material Type tailored for rendering skin, with support for blended wrinkle maps that work with animated vertex blend shapes.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "baseColor", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype index 5fa0dcb217..107b525ae4 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Similar to StandardPBR but supports multiple layers blended together.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "blend", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl index 6a97c0e785..a53dab7a01 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.azsl @@ -302,6 +302,7 @@ ProcessedMaterialInputs ProcessStandardMaterialInputs(StandardMaterialInputs inp PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC) { + const float3 vertexNormal = normalize(IN.m_normal); depthNDC = IN.m_position.z; s_blendMaskFromVertexStream = IN.m_blendMask; @@ -321,7 +322,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float || o_layer3_o_clearCoat_normal_useTexture ) { - PrepareGeneratedTangent(IN.m_normal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); + PrepareGeneratedTangent(vertexNormal, IN.m_worldPosition, isFrontFace, IN.m_uv, UvSetCount, tangents, bitangents); } // ------- Debug Modes ------- @@ -368,7 +369,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords); } } } @@ -445,7 +446,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float normalTS = ReorientTangentSpaceNormal(normalTS, lightingInputLayer3.m_normalTS); } // [GFX TODO][ATOM-14591]: This will only work if the normal maps all use the same UV stream. We would need to add support for having them in different UV streams. - surface.vertexNormal = normalize(IN.m_normal); + surface.vertexNormal = vertexNormal; surface.normal = normalize(TangentSpaceToWorld(normalTS, IN.m_normal, tangents[MaterialSrg::m_parallaxUvIndex], bitangents[MaterialSrg::m_parallaxUvIndex])); // ------- Combine Albedo, roughness, specular, roughness --------- diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader index 28322d68ed..00efca6056 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass.shader @@ -48,6 +48,14 @@ } ] }, + + "Supervariants": + [ + { + "Name": "", + "PlusArguments": "--no-alignment-validation" + } + ], "DrawList" : "forward" } diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader index 42366d6067..983245ffb0 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardMultilayerPBR_ForwardPass_EDS.shader @@ -49,5 +49,13 @@ ] }, + "Supervariants": + [ + { + "Name": "", + "PlusArguments": "--no-alignment-validation" + } + ], + "DrawList" : "forward" } diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype index 6eb82b85ae..7527a7658a 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Material Type with properties used to define Standard PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "baseColor", diff --git a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl index 8df5e1ba56..1d5e4ad9e3 100644 --- a/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl +++ b/Gems/Atom/Feature/Common/Assets/Materials/Types/StandardPBR_ForwardPass.azsl @@ -98,6 +98,8 @@ VSOutput StandardPbr_ForwardPassVS(VSInput IN) PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float depthNDC) { + const float3 vertexNormal = normalize(IN.m_normal); + // ------- Tangents & Bitangets ------- float3 tangents[UvSetCount] = { IN.m_tangent.xyz, IN.m_tangent.xyz }; float3 bitangents[UvSetCount] = { IN.m_bitangent.xyz, IN.m_bitangent.xyz }; @@ -128,7 +130,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, IN.m_shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, IN.m_worldPosition, vertexNormal, IN.m_shadowCoords); } } } @@ -146,7 +148,7 @@ PbrLightingOutput ForwardPassPS_Common(VSOutput IN, bool isFrontFace, out float float2 normalUv = IN.m_uv[MaterialSrg::m_normalMapUvIndex]; float3x3 uvMatrix = MaterialSrg::m_normalMapUvIndex == 0 ? MaterialSrg::m_uvMatrix : CreateIdentity3x3(); // By design, only UV0 is allowed to apply transforms. - surface.vertexNormal = normalize(IN.m_normal); + surface.vertexNormal = vertexNormal; surface.normal = GetNormalInputWS(MaterialSrg::m_normalMap, MaterialSrg::m_sampler, normalUv, MaterialSrg::m_flipNormalX, MaterialSrg::m_flipNormalY, isFrontFace, IN.m_normal, tangents[MaterialSrg::m_normalMapUvIndex], bitangents[MaterialSrg::m_normalMapUvIndex], uvMatrix, o_normal_useTexture, MaterialSrg::m_normalFactor); diff --git a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset index 7770b326a6..81b7d36484 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset +++ b/Gems/Atom/Feature/Common/Assets/Passes/PassTemplates.azasset @@ -469,12 +469,12 @@ "Path": "Passes/OpaqueParent.pass" }, { - "Name": "ThumbnailPipeline", - "Path": "Passes/ThumbnailPipeline.pass" + "Name": "ToolsPipeline", + "Path": "Passes/ToolsPipeline.pass" }, { - "Name": "ThumbnailPipelineRenderToTexture", - "Path": "Passes/ThumbnailPipelineRenderToTexture.pass" + "Name": "ToolsPipelineRenderToTexture", + "Path": "Passes/ToolsPipelineRenderToTexture.pass" }, { "Name": "TransparentParentTemplate", diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass similarity index 99% rename from Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass rename to Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass index 932b6ac435..51c169348b 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipeline.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipeline.pass @@ -4,7 +4,7 @@ "ClassName": "PassAsset", "ClassData": { "PassTemplate": { - "Name": "ThumbnailPipeline", + "Name": "ToolsPipeline", "PassClass": "ParentPass", "Slots": [ { diff --git a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass similarity index 88% rename from Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass rename to Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass index 11e2cb717a..b98ec46d0e 100644 --- a/Gems/Atom/Feature/Common/Assets/Passes/ThumbnailPipelineRenderToTexture.pass +++ b/Gems/Atom/Feature/Common/Assets/Passes/ToolsPipelineRenderToTexture.pass @@ -4,7 +4,7 @@ "ClassName": "PassAsset", "ClassData": { "PassTemplate": { - "Name": "ThumbnailPipelineRenderToTexture", + "Name": "ToolsPipelineRenderToTexture", "PassClass": "RenderToTexturePass", "PassData": { "$type": "RenderToTexturePassData", @@ -15,7 +15,7 @@ "PassRequests": [ { "Name": "Pipeline", - "TemplateName": "ThumbnailPipeline", + "TemplateName": "ToolsPipeline", "Connections": [ { "LocalSlot": "SwapChainOutput", diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl index 099a6394d4..a380a0a547 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/PostProcessing/HDRColorGradingCommon.azsl @@ -138,7 +138,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_colorFilterMultiply, PassSrg::m_colorFilterIntensity), PassSrg::m_colorAdjustmentWeight); frameColor = max(frameColor, 0.0); frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPreSaturation), PassSrg::m_colorAdjustmentWeight); - + frameColor = max(frameColor, 0.0); frameColor = ColorGradeSplitTone(frameColor, PassSrg::m_splitToneBalance, PassSrg::m_splitToneWeight, PassSrg::m_splitToneShadowsColor, PassSrg::m_splitToneHighlightsColor); frameColor = ColorGradeChannelMixer(frameColor, PassSrg::m_channelMixingRed, PassSrg::m_channelMixingGreen, PassSrg::m_channelMixingBlue); @@ -147,8 +147,7 @@ float3 ColorGrade(float3 frameColor) PassSrg::m_smhHighlightsStart, PassSrg::m_smhHighlightsEnd, PassSrg::m_smhWeight, PassSrg::m_smhShadowsColor, PassSrg::m_smhMidtonesColor, PassSrg::m_smhHighlightsColor); - - frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); frameColor = lerp(frameColor, ColorGradeHueShift(frameColor, PassSrg::m_colorGradingHueShift), PassSrg::m_finalAdjustmentWeight); + frameColor = lerp(frameColor, ColorGradeSaturation(frameColor, PassSrg::m_colorGradingPostSaturation), PassSrg::m_finalAdjustmentWeight); return max(frameColor.rgb, 0.0); } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader index 5512b1ad4d..475a1b1b54 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/RayTracing/RayTracingSrgs.shader @@ -37,7 +37,7 @@ [ { "Name": "", - "PlusArguments": "", + "PlusArguments": "--no-alignment-validation", "MinusArguments": "--strip-unused-srgs" } ] diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli index b53dda13aa..a7122aaf3a 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/DirectionalLightShadow.azsli @@ -14,6 +14,7 @@ #include "ShadowmapAtlasLib.azsli" #include "BicubicPcfFilters.azsli" #include "ReceiverPlaneDepthBias.azsli" +#include "NormalOffsetShadows.azsli" // Before including this azsli file, a PassSrg must be defined with the following members: // Texture2DArray m_directionalLightShadowmap; @@ -45,6 +46,7 @@ class DirectionalLightShadow static void GetShadowCoords( uint lightIndex, float3 worldPosition, + float3 worldNormal, out float3 shadowCoords[ViewSrg::MaxCascadeCount]); //! This calculates visibility ratio of the surface from the light origin. @@ -109,18 +111,22 @@ class DirectionalLightShadow void DirectionalLightShadow::GetShadowCoords( uint lightIndex, float3 worldPosition, + float3 worldNormal, out float3 shadowCoords[ViewSrg::MaxCascadeCount]) { - const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount; const float shadowBias = ViewSrg::m_directionalLightShadows[lightIndex].m_shadowBias; + const float4x4 lightViewToShadowmapMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_lightViewToShadowmapMatrices; const float4x4 worldToLightViewMatrices[ViewSrg::MaxCascadeCount] = ViewSrg::m_directionalLightShadows[lightIndex].m_worldToLightViewMatrices; - - for (uint index = 0; index < cascadeCount; ++index) - { - float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition, 1.)); - lightSpacePos.z += shadowBias; + const uint cascadeCount = ViewSrg::m_directionalLightShadows[lightIndex].m_cascadeCount; + const float3 shadowOffset = ComputeNormalShadowOffset(ViewSrg::m_directionalLightShadows[lightIndex].m_normalShadowBias, worldNormal, ViewSrg::m_directionalLightShadows[lightIndex].m_shadowmapSize); + + for (uint index = 0; index < cascadeCount; ++index) + { + float4 lightSpacePos = mul(worldToLightViewMatrices[index], float4(worldPosition + shadowOffset, 1.)); + lightSpacePos.z += shadowBias; + const float4 clipSpacePos = mul(lightViewToShadowmapMatrices[index], lightSpacePos); shadowCoords[index] = clipSpacePos.xyz / clipSpacePos.w; } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli new file mode 100644 index 0000000000..1e78cec89e --- /dev/null +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Shadow/NormalOffsetShadows.azsli @@ -0,0 +1,22 @@ +/* + * 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 + +// Helper functions for normal offset shadow mapping. +// Normal Offset is an alternative to slope-scale depth bias. +// We bias the shadow map lookup by transforming the world-position along the geometric normal before hand. +// http://web.archive.org/web/20140810230446/https://www.dissidentlogic.com/old/#Normal%20Offset%20Shadows +// + +// Apply the following to the world position. Then use this modified world position to look up in the shadow map +float3 ComputeNormalShadowOffset(const float normalOffsetBias, const float3 worldNormal, const float shadowMapDimension) +{ + const float shadowmapSize = 2.0f / shadowMapDimension; + return float3(worldNormal * normalOffsetBias * shadowmapSize); +} diff --git a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli index 4b5047d4c3..c93986cea1 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderLib/Atom/Features/Vertex/VertexHelper.azsli @@ -47,6 +47,7 @@ void VertexHelper(in VSInput IN, inout VSOutput OUT, float3 worldPosition, bool DirectionalLightShadow::GetShadowCoords( shadowIndex, worldPosition, + OUT.m_normal, OUT.m_shadowCoords); } } diff --git a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli index 27fa36182c..b3bcc186b6 100644 --- a/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli +++ b/Gems/Atom/Feature/Common/Assets/ShaderResourceGroups/CoreLights/ViewSrg.azsli @@ -81,10 +81,10 @@ partial ShaderResourceGroup ViewSrg uint m_shadowmapArraySlice; // array slice who has shadowmap in the atlas. uint m_shadowFilterMethod; float m_boundaryScale; - uint m_predictionSampleCount; uint m_filteringSampleCount; float2 m_unprojectConstants; float m_bias; + float m_normalShadowBias; float m_esmExponent; float3 m_padding; }; @@ -109,7 +109,7 @@ partial ShaderResourceGroup ViewSrg uint m_shadowmapSize; // width and height of shadowmap uint m_cascadeCount; float m_shadowBias; - uint m_predictionSampleCount; + float m_normalShadowBias; uint m_filteringSampleCount; uint m_debugFlags; uint m_shadowFilterMethod; diff --git a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake index 123e5da7ba..e13465307a 100644 --- a/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake +++ b/Gems/Atom/Feature/Common/Assets/atom_feature_common_asset_files.cmake @@ -161,8 +161,8 @@ set(FILES Passes/LutGeneration.pass Passes/MainPipeline.pass Passes/MainPipelineRenderToTexture.pass - Passes/ThumbnailPipeline.pass - Passes/ThumbnailPipelineRenderToTexture.pass + Passes/ToolsPipeline.pass + Passes/ToolsPipelineRenderToTexture.pass Passes/MeshMotionVector.pass Passes/ModulateTexture.pass Passes/MorphTarget.pass diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h index 2bba1338a1..fa987a7156 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/CoreLights/DirectionalLightFeatureProcessorInterface.h @@ -160,6 +160,9 @@ namespace AZ //! Reduces acne by applying a small amount of bias along shadow-space z. virtual void SetShadowBias(LightHandle handle, float bias) = 0; + + //! Reduces acne by biasing the shadowmap lookup along the geometric normal. + virtual void SetNormalShadowBias(LightHandle handle, float normalShadowBias) = 0; }; } // namespace Render } // namespace AZ diff --git a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h index 46560f435d..1517d655bb 100644 --- a/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h +++ b/Gems/Atom/Feature/Common/Code/Include/Atom/Feature/Shadows/ProjectedShadowFeatureProcessorInterface.h @@ -48,11 +48,13 @@ namespace AZ::Render virtual void SetAspectRatio(ShadowId id, float aspectRatio) = 0; //! Sets the field of view for the shadow in radians in the Y direction. virtual void SetFieldOfViewY(ShadowId id, float fieldOfView) = 0; - //! Sets the maximum resolution of the shadow map + //! Sets the maximum resolution of the shadow map. virtual void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) = 0; - //! Sets the shadow bias + //! Sets the shadow bias. virtual void SetShadowBias(ShadowId id, float bias) = 0; - //! Sets the shadow filter method + //! Sets the normal shadow bias. + virtual void SetNormalShadowBias(ShadowId id, float normalShadowBias) = 0; + //! Sets the shadow filter method. virtual void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) = 0; //! Sets the sample count for filtering of the shadow boundary, max 64. virtual void SetFilteringSampleCount(ShadowId id, uint16_t count) = 0; diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp index 4774ac31a1..6c24b7d35b 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.cpp @@ -598,6 +598,15 @@ namespace AZ m_shadowBufferNeedsUpdate = true; } + void DirectionalLightFeatureProcessor::SetNormalShadowBias(LightHandle handle, float normalShadowBias) + { + for (auto& it : m_shadowData) + { + it.second.GetData(handle.GetIndex()).m_normalShadowBias = normalShadowBias; + } + m_shadowBufferNeedsUpdate = true; + } + void DirectionalLightFeatureProcessor::OnRenderPipelineAdded(RPI::RenderPipelinePtr pipeline) { PrepareForChangingRenderPipelineAndCameraView(); diff --git a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h index 2cf5b0b1e6..8d7a9d76e4 100644 --- a/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/CoreLights/DirectionalLightFeatureProcessor.h @@ -92,8 +92,9 @@ namespace AZ uint32_t m_shadowmapSize = 1; // width and height of shadowmap uint32_t m_cascadeCount = 1; // Reduce acne by applying a small amount of bias to apply along shadow-space z. - float m_shadowBias = 0.0f; - uint32_t m_predictionSampleCount = 0; + float m_shadowBias = 0.0f; + // Reduces acne by biasing the shadowmap lookup along the geometric normal. + float m_normalShadowBias; uint32_t m_filteringSampleCount = 0; uint32_t m_debugFlags = 0; uint32_t m_shadowFilterMethod = 0; @@ -101,6 +102,8 @@ namespace AZ float m_padding[3]; }; + static_assert(sizeof(DirectionalLightShadowData) % 16 == 0); // Structured buffers need alignment to be a multiple of 16 bytes. + class DirectionalLightFeatureProcessor final : public DirectionalLightFeatureProcessorInterface { @@ -216,6 +219,7 @@ namespace AZ void SetFilteringSampleCount(LightHandle handle, uint16_t count) override; void SetShadowReceiverPlaneBiasEnabled(LightHandle handle, bool enable) override; void SetShadowBias(LightHandle handle, float bias) override; + void SetNormalShadowBias(LightHandle handle, float normalShadowBias) override; const Data::Instance GetLightBuffer() const; uint32_t GetLightCount() const; diff --git a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp index da92cc04e7..7c0b3563c7 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp +++ b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.cpp @@ -151,6 +151,14 @@ namespace AZ::Render shadowProperty.m_bias = bias; } + void ProjectedShadowFeatureProcessor::SetNormalShadowBias(ShadowId id, float normalShadowBias) + { + AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetNormalShadowBias()."); + + ShadowProperty& shadowProperty = GetShadowPropertyFromShadowId(id); + shadowProperty.m_normalShadowBias = normalShadowBias; + } + void ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) { AZ_Assert(id.IsValid(), "Invalid ShadowId passed to ProjectedShadowFeatureProcessor::SetShadowmapMaxResolution()."); diff --git a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h index 6269166827..fafcb25a08 100644 --- a/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h +++ b/Gems/Atom/Feature/Common/Code/Source/Shadows/ProjectedShadowFeatureProcessor.h @@ -48,6 +48,7 @@ namespace AZ::Render void SetFieldOfViewY(ShadowId id, float fieldOfViewYRadians) override; void SetShadowmapMaxResolution(ShadowId id, ShadowmapSize size) override; void SetShadowBias(ShadowId id, float bias) override; + void SetNormalShadowBias(ShadowId id, float normalShadowBias) override; void SetShadowFilterMethod(ShadowId id, ShadowFilterMethod method) override; void SetFilteringSampleCount(ShadowId id, uint16_t count) override; void SetShadowProperties(ShadowId id, const ProjectedShadowDescriptor& descriptor) override; @@ -64,10 +65,10 @@ namespace AZ::Render uint32_t m_shadowmapArraySlice = 0; // array slice who has shadowmap in the atlas. uint32_t m_shadowFilterMethod = 0; // filtering method of shadows. float m_boundaryScale = 0.f; // the half of boundary of lit/shadowed areas. (in degrees) - uint32_t m_predictionSampleCount = 0; // sample count to judge whether it is on the shadow boundary or not. uint32_t m_filteringSampleCount = 0; AZStd::array m_unprojectConstants = { {0, 0} }; float m_bias; + float m_normalShadowBias; float m_esmExponent = 87.0f; float m_padding[3]; }; @@ -78,6 +79,7 @@ namespace AZ::Render ProjectedShadowDescriptor m_desc; RPI::ViewPtr m_shadowmapView; float m_bias = 0.1f; + float m_normalShadowBias = 0.0f; ShadowId m_shadowId; }; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h index befbb7c990..29371618cf 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialPropertyValueSerializer.h @@ -24,6 +24,13 @@ namespace AZ AZ_RTTI(AZ::RPI::JsonMaterialPropertyValueSerializer, "{A52B1ED8-C849-4269-9AA7-9D0814D2EC59}", BaseJsonSerializer); AZ_CLASS_ALLOCATOR_DECL; + //! A LoadContext object must be passed down to the serializer via JsonDeserializerContext::GetMetadata().Add(...) + struct LoadContext + { + AZ_TYPE_INFO(JsonMaterialPropertyValueSerializer::LoadContext, "{5E0A891A-27F6-4AD7-88A5-B9EA50F88B45}"); + uint32_t m_materialTypeVersion; //!< The version number from the .materialtype file + }; + JsonSerializationResult::Result Load(void* outputValue, const Uuid& outputValueTypeId, const rapidjson::Value& inputValue, JsonDeserializerContext& context) override; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h index 02607a7954..a67477f061 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialSourceData.h @@ -50,7 +50,7 @@ namespace AZ AZStd::string m_parentMaterial; //!< The immediate parent of this material - uint32_t m_propertyLayoutVersion = 0; //!< The version of the property layout, defined in the material type, which was used to configure this material + uint32_t m_materialTypeVersion = 0; //!< The version of the material type that was used to configure this material struct Property { @@ -64,6 +64,18 @@ namespace AZ PropertyGroupMap m_properties; + enum class ApplyVersionUpdatesResult + { + Failed, + NoUpdates, + UpdatesApplied + }; + + //! Checks the material type version and potentially applies a series of property changes (most common are simple property renames) + //! based on the MaterialTypeAsset's version update procedure. + //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for resolving file-relative paths. + ApplyVersionUpdatesResult ApplyVersionUpdates(AZStd::string_view materialSourceFilePath = ""); + //! Creates a MaterialAsset from the MaterialSourceData content. //! @param assetId ID for the MaterialAsset //! @param materialSourceFilePath Indicates the path of the .material file that the MaterialSourceData represents. Used for resolving file-relative paths. diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h index 04b6222404..1234b15f95 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Edit/Material/MaterialTypeSourceData.h @@ -13,6 +13,7 @@ #include #include #include +#include namespace AZ { @@ -119,12 +120,36 @@ namespace AZ using PropertyList = AZStd::vector; + struct VersionUpdatesRenameOperationDefinition + { + AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::VersionUpdatesRenameOperationDefinition, "{F2295489-E15A-46CC-929F-8D42DEDBCF14}"); + + AZStd::string m_operation; + + AZStd::string m_renameFrom; + AZStd::string m_renameTo; + }; + + // TODO: Support script operations--At that point, we'll likely need to replace VersionUpdatesRenameOperationDefinition with a more generic + // data structure that has a custom JSON serialize. We will only be supporting rename for now. + using VersionUpdateActions = AZStd::vector; + + struct VersionUpdateDefinition + { + AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::VersionUpdateDefinition, "{2C9D3B91-0585-4BC9-91D2-4CF0C71BC4B7}"); + + uint32_t m_toVersion; + VersionUpdateActions m_actions; + }; + + using VersionUpdates = AZStd::vector; + struct PropertyLayout { AZ_TYPE_INFO(AZ::RPI::MaterialTypeSourceData::PropertyLayout, "{AE53CF3F-5C3B-44F5-B2FB-306F0EB06393}"); - - //! Indicates the version of the set of available properties. Can be used to detect materials that might need to be updated. - uint32_t m_version = 0; + + //! This field is unused, and has been replaced by MaterialTypeSourceData::m_version below. It is kept for legacy file compatibility to suppress warnings and errors. + uint32_t m_versionOld = 0; //! List of groups that will contain the available properties AZStd::vector m_groups; @@ -135,6 +160,11 @@ namespace AZ AZStd::string m_description; + //! Version 1 is the default and should not contain any version update. + uint32_t m_version = 1; + + VersionUpdates m_versionUpdates; + PropertyLayout m_propertyLayout; //! A list of shader variants that are always used at runtime; they cannot be turned off @@ -153,7 +183,12 @@ namespace AZ const GroupDefinition* FindGroup(AZStd::string_view groupName) const; - const PropertyDefinition* FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName) const; + //! Searches for a specific property. + //! Note this function can find properties using old versions of the property name; in that case, + //! the name in the returned PropertyDefinition* will not match the @propertyName that was searched for. + //! @param materialTypeVersion indicates the version number of the property name being passed in. Only renames above this version number will be applied. + //! @return the requested property, or null if it could not be found + const PropertyDefinition* FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName, uint32_t materialTypeVersion = 0) const; //! Construct a complete list of group definitions, including implicit groups, arranged in the same order as the source data //! Groups with the same name will be consolidated into a single entry @@ -179,6 +214,11 @@ namespace AZ bool ConvertPropertyValueToSourceDataFormat(const PropertyDefinition& propertyDefinition, MaterialPropertyValue& propertyValue) const; Outcome> CreateMaterialTypeAsset(Data::AssetId assetId, AZStd::string_view materialTypeSourceFilePath = "", bool elevateWarnings = true) const; + + //! Possibly renames @propertyId based on the material version update steps. + //! @param materialTypeVersion indicates the version number of the property name being passed in. Only renames above this version number will be applied. + //! @return true if the property was renamed + bool ApplyPropertyRenames(MaterialPropertyId& propertyId, uint32_t materialTypeVersion = 0) const; }; //! The wrapper class for derived material functors. diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h index 6ca3bca652..2a1de6debd 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialAsset.h @@ -22,6 +22,7 @@ namespace UnitTest { class MaterialTests; + class MaterialAssetTests; } namespace AZ @@ -42,10 +43,12 @@ namespace AZ , public MaterialReloadNotificationBus::Handler , public AssetInitBus::Handler { + friend class MaterialVersionUpdate; friend class MaterialAssetCreator; friend class MaterialAssetHandler; friend class MaterialAssetCreatorCommon; friend class UnitTest::MaterialTests; + friend class UnitTest::MaterialAssetTests; public: AZ_RTTI(MaterialAsset, "{522C7BE0-501D-463E-92C6-15184A2B7AD8}", AZ::Data::AssetData); @@ -119,6 +122,10 @@ namespace AZ //! from m_materialTypeAsset. void RealignPropertyValuesAndNames(); + //! Checks the material type version and potentially applies a series of property changes (most common are simple property renames) + //! based on the MaterialTypeAsset's version update procedure. + void ApplyVersionUpdates(); + //! Called by asset creators to assign the asset to a ready state. void SetReady(); @@ -143,6 +150,10 @@ namespace AZ //! If empty, this implies that m_propertyValues is aligned with the entries in m_materialPropertiesLayout. AZStd::vector m_propertyNames; + //! The materialTypeVersion this materialAsset was based of. If the versions do not match at runtime when a + //! materialTypeAsset is loaded, an update will be performed on m_propertyNames if populated. + uint32_t m_materialTypeVersion = 1; + //! A flag to determine if m_propertyValues needs to be aligned with MaterialPropertiesLayout. Set to true whenever //! m_materialTypeAsset is reinitializing. bool m_isDirty = true; diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h index e705a8041b..9bc0e018d3 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAsset.h @@ -18,6 +18,7 @@ #include #include #include +#include namespace AZ { @@ -123,6 +124,11 @@ namespace AZ //! Returns a map from the UV shader inputs to a custom name. MaterialUvNameMap GetUvNameMap() const; + //! Returns the version of the MaterialTypeAsset. + uint32_t GetVersion() const; + + const AZStd::vector& GetMaterialVersionUpdateList() const { return m_materialVersionUpdates; } + private: bool PostLoadInit() override; @@ -162,6 +168,12 @@ namespace AZ //! Index in @m_shaderCollection of the shader asset that contains the ObjectSrg. uint32_t m_objectSrgShaderIndex = InvalidShaderIndex; + //! The version of this MaterialTypeAsset. If the version is greater than 1, actions performed + //! to update this MaterialTypeAsset will be in m_materialVersionUpdateMap + uint32_t m_version = 1; + + //! Contains actions to perform for each material update version. + AZStd::vector m_materialVersionUpdates; }; class MaterialTypeAssetHandler : public AssetHandler diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h index 70488245a2..5e5f94da6d 100644 --- a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h @@ -38,6 +38,11 @@ namespace AZ void AddShader(const AZ::Data::Asset& shaderAsset, const ShaderVariantId& shaderVaraintId = ShaderVariantId{}, const AZ::Name& shaderTag = Uuid::CreateRandom().ToString()); void AddShader(const AZ::Data::Asset& shaderAsset, const AZ::Name& shaderTag); + //! Sets the version of the MaterialTypeAsset + void SetVersion(uint32_t version); + //! Adds a version update object into the MaterialTypeAsset + void AddVersionUpdate(const MaterialVersionUpdate& materialVersionUpdate); + //! Indicates that this MaterialType will own the specified shader option. //! Material-owned shader options can be connected to material properties (either directly or through functors). //! They cannot be accessed externally (for example, through the Material::SetSystemShaderOption() function). @@ -112,6 +117,7 @@ namespace AZ //! Saves the per-material SRG layout in m_shaderResourceGroupLayout for easier access void CacheMaterialSrgLayout(); + bool ValidateMaterialVersion(); bool ValidateBeginMaterialProperty(); bool ValidateEndMaterialProperty(); diff --git a/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h new file mode 100644 index 0000000000..eb71ad9cb7 --- /dev/null +++ b/Gems/Atom/RPI/Code/Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h @@ -0,0 +1,62 @@ +/* + * 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 AZ +{ + namespace RPI + { + class MaterialAsset; + + // This class contains a toVersion and a list of actions to specify what operations were performed to upgrade a materialType. + class MaterialVersionUpdate + { + public: + AZ_TYPE_INFO(AZ::RPI::MaterialVersionUpdate, "{B36E7712-AED8-46AA-AFE0-01F8F884C44A}"); + + static void Reflect(ReflectContext* context); + + // At this time, the only supported operation is rename. If/when we add more actions in the future, + // we'll need to improve this, possibly with some virtual interface or union data. + struct RenamePropertyAction + { + AZ_TYPE_INFO(AZ::RPI::MaterialVersionUpdate::RenameAction, "{A1FBEB19-EA05-40F0-9700-57D048DF572B}"); + + static void Reflect(ReflectContext* context); + + AZ::Name m_fromPropertyId; + AZ::Name m_toPropertyId; + }; + + explicit MaterialVersionUpdate() = default; + explicit MaterialVersionUpdate(uint32_t toVersion); + + uint32_t GetVersion() const; + void SetVersion(uint32_t toVersion); + + //! Apply version updates to the given material asset. + //! @return true if any changes were made + bool ApplyVersionUpdates(MaterialAsset& materialAsset) const; + + using Actions = AZStd::vector; + const Actions& GetActions() const; + void AddAction(const RenamePropertyAction& action); + + private: + uint32_t m_toVersion; + Actions m_actions; + }; + + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp b/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp index 86ea8ab688..e9cebf29c7 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Builders/Material/MaterialBuilder.cpp @@ -47,7 +47,7 @@ namespace AZ { AssetBuilderSDK::AssetBuilderDesc materialBuilderDescriptor; materialBuilderDescriptor.m_name = JobKey; - materialBuilderDescriptor.m_version = 109; // Changed "id" to "name" in serialization + materialBuilderDescriptor.m_version = 110; // Material version auto update feature materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.material", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); materialBuilderDescriptor.m_patterns.push_back(AssetBuilderSDK::AssetBuilderPattern("*.materialtype", AssetBuilderSDK::AssetBuilderPattern::PatternType::Wildcard)); materialBuilderDescriptor.m_busId = azrtti_typeid(); @@ -287,6 +287,11 @@ namespace AZ return {}; } + if (MaterialSourceData::ApplyVersionUpdatesResult::Failed == material.GetValue().ApplyVersionUpdates(materialSourceFilePath)) + { + return {}; + } + auto materialAssetOutcome = material.GetValue().CreateMaterialAsset(Uuid::CreateRandom(), materialSourceFilePath, true); if (!materialAssetOutcome.IsSuccess()) { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp index 3b2d36451a..5e04365ffb 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialPropertyValueSerializer.cpp @@ -62,6 +62,8 @@ namespace AZ return context.Report(JsonSerializationResult::Tasks::ReadField, JsonSerializationResult::Outcomes::Catastrophic, "Material type reference not found."); } + const JsonMaterialPropertyValueSerializer::LoadContext* loadContext = context.GetMetadata().Find(); + // Construct the full property name (groupName.propertyName) by parsing it from the JSON path string. size_t startPropertyName = context.GetPath().Get().rfind('/'); size_t startGroupName = context.GetPath().Get().rfind('/', startPropertyName-1); @@ -70,7 +72,7 @@ namespace AZ JSR::ResultCode result(JSR::Tasks::ReadField); - auto propertyDefinition = materialType->FindProperty(groupName, propertyName); + auto propertyDefinition = materialType->FindProperty(groupName, propertyName, loadContext->m_materialTypeVersion); if (!propertyDefinition) { AZStd::string message = AZStd::string::format("Property '%.*s.%.*s' not found in material type.", AZ_STRING_ARG(groupName), AZ_STRING_ARG(propertyName)); 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 f697f33a3f..5aed6b2993 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceData.cpp @@ -72,6 +72,62 @@ namespace AZ materialAssetCreator.SetPropertyValue(propertyId, entry.second); } } + + MaterialSourceData::ApplyVersionUpdatesResult MaterialSourceData::ApplyVersionUpdates(AZStd::string_view materialSourceFilePath) + { + AZStd::string materialTypeFullPath = AssetUtils::ResolvePathReference(materialSourceFilePath, m_materialType); + auto materialTypeSourceDataOutcome = MaterialUtils::LoadMaterialTypeSourceData(materialTypeFullPath); + if (!materialTypeSourceDataOutcome.IsSuccess()) + { + return ApplyVersionUpdatesResult::Failed; + } + + MaterialTypeSourceData materialTypeSourceData = materialTypeSourceDataOutcome.TakeValue(); + + if (m_materialTypeVersion == materialTypeSourceData.m_version) + { + return ApplyVersionUpdatesResult::NoUpdates; + } + + bool changesWereApplied = false; + + // Note that the only kind of property update currently supported is rename... + + 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; + } + } + + propertyMap = newPropertyMap; + } + + if (changesWereApplied) + { + 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.", + m_materialTypeVersion, m_materialType.c_str(), materialTypeSourceData.m_version); + } + + m_materialTypeVersion = materialTypeSourceData.m_version; + + return changesWereApplied ? ApplyVersionUpdatesResult::UpdatesApplied : ApplyVersionUpdatesResult::NoUpdates; + } Outcome > MaterialSourceData::CreateMaterialAsset(Data::AssetId assetId, AZStd::string_view materialSourceFilePath, bool elevateWarnings, bool includeMaterialPropertyNames) const { diff --git a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp index 5e4af07aae..2a504fc345 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialSourceDataSerializer.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -45,9 +46,9 @@ namespace AZ } result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_description, azrtti_typeid(), inputValue, "description", context)); - result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_materialType, azrtti_typeid(), inputValue, "materialType", context)); result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_parentMaterial, azrtti_typeid(), inputValue, "parentMaterial", context)); - result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_propertyLayoutVersion, azrtti_typeid(), inputValue, "propertyLayoutVersion", context)); + result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_materialType, azrtti_typeid(), inputValue, "materialType", context)); + result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_materialTypeVersion, azrtti_typeid(), inputValue, "materialTypeVersion", context)); if (materialSourceData->m_materialType.empty()) { @@ -118,6 +119,10 @@ namespace AZ context.GetMetadata().Add(AZStd::move(materialTypeData)); + JsonMaterialPropertyValueSerializer::LoadContext materialPropertyValueLoadContext; + materialPropertyValueLoadContext.m_materialTypeVersion = materialSourceData->m_materialTypeVersion; + context.GetMetadata().Add(materialPropertyValueLoadContext); + result.Combine(ContinueLoadingFromJsonObjectField(&materialSourceData->m_properties, azrtti_typeid(), inputValue, "properties", context)); if (result.GetProcessing() == JsonSerializationResult::Processing::Completed) @@ -146,9 +151,9 @@ namespace AZ JSR::ResultCode resultCode(JSR::Tasks::ReadField); resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "description", &materialSourceData->m_description, nullptr, azrtti_typeid(), context)); - resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "materialType", &materialSourceData->m_materialType, nullptr, azrtti_typeid(), context)); resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "parentMaterial", &materialSourceData->m_parentMaterial, nullptr, azrtti_typeid(), context)); - resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "propertyLayoutVersion", &materialSourceData->m_propertyLayoutVersion, nullptr, azrtti_typeid(), context)); + resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "materialType", &materialSourceData->m_materialType, nullptr, azrtti_typeid(), context)); + resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "materialTypeVersion", &materialSourceData->m_materialTypeVersion, nullptr, azrtti_typeid(), context)); resultCode.Combine(ContinueStoringToJsonObjectField(outputValue, "properties", &materialSourceData->m_properties, nullptr, azrtti_typeid(), context)); return context.Report(resultCode, "Processed material."); 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 1cc57f4d47..74250647c3 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Edit/Material/MaterialTypeSourceData.cpp @@ -16,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -59,6 +60,23 @@ namespace AZ serializeContext->RegisterGenericType(); + serializeContext->Class() + ->Version(1) + ->Field("op", &VersionUpdatesRenameOperationDefinition::m_operation) + ->Field("from", &VersionUpdatesRenameOperationDefinition::m_renameFrom) + ->Field("to", &VersionUpdatesRenameOperationDefinition::m_renameTo) + ; + + serializeContext->RegisterGenericType(); + + serializeContext->Class() + ->Version(1) + ->Field("toVersion", &VersionUpdateDefinition::m_toVersion) + ->Field("actions", &VersionUpdateDefinition::m_actions) + ; + + serializeContext->RegisterGenericType(); + serializeContext->Class() ->Version(2) ->Field("file", &ShaderVariantReferenceData::m_shaderFilePath) @@ -67,8 +85,8 @@ namespace AZ ; serializeContext->Class() - ->Version(1) - ->Field("version", &PropertyLayout::m_version) + ->Version(2) // Material Version Update + ->Field("version", &PropertyLayout::m_versionOld) ->Field("groups", &PropertyLayout::m_groups) ->Field("properties", &PropertyLayout::m_properties) ; @@ -76,8 +94,10 @@ namespace AZ serializeContext->RegisterGenericType(); serializeContext->Class() - ->Version(3) + ->Version(4) // Material Version Update ->Field("description", &MaterialTypeSourceData::m_description) + ->Field("version", &MaterialTypeSourceData::m_version) + ->Field("versionUpdates", &MaterialTypeSourceData::m_versionUpdates) ->Field("propertyLayout", &MaterialTypeSourceData::m_propertyLayout) ->Field("shaders", &MaterialTypeSourceData::m_shaderCollection) ->Field("functors", &MaterialTypeSourceData::m_materialFunctorSourceData) @@ -110,7 +130,38 @@ namespace AZ return nullptr; } - const MaterialTypeSourceData::PropertyDefinition* MaterialTypeSourceData::FindProperty(AZStd::string_view groupName, AZStd::string_view propertyName) const + bool MaterialTypeSourceData::ApplyPropertyRenames(MaterialPropertyId& propertyId, uint32_t materialTypeVersion) const + { + bool renamed = false; + + for (const VersionUpdateDefinition& versionUpdate : m_versionUpdates) + { + if (materialTypeVersion >= versionUpdate.m_toVersion) + { + continue; + } + + for (const VersionUpdatesRenameOperationDefinition& action : versionUpdate.m_actions) + { + if (action.m_operation == "rename") + { + if (action.m_renameFrom == propertyId.GetFullName().GetStringView()) + { + propertyId = MaterialPropertyId::Parse(action.m_renameTo); + renamed = true; + } + } + else + { + AZ_Warning("Material source data", false, "Unsupported material version update operation '%s'", action.m_operation.c_str()); + } + } + } + + return renamed; + } + + 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()) @@ -126,6 +177,27 @@ namespace AZ } } + // Property has not been found, try looking for renames in the version history + + MaterialPropertyId propertyId = MaterialPropertyId{groupName, propertyName}; + ApplyPropertyRenames(propertyId, materialTypeVersion); + + // Do the search again with the new names + + groupIter = m_propertyLayout.m_properties.find(propertyId.GetGroupName().GetStringView()); + if (groupIter == m_propertyLayout.m_properties.end()) + { + return nullptr; + } + + for (const PropertyDefinition& property : groupIter->second) + { + if (property.m_name == propertyId.GetPropertyName().GetStringView()) + { + return &property; + } + } + return nullptr; } @@ -280,6 +352,41 @@ namespace AZ materialTypeAssetCreator.SetElevateWarnings(elevateWarnings); materialTypeAssetCreator.Begin(assetId); + if (m_propertyLayout.m_versionOld != 0) + { + materialTypeAssetCreator.ReportError( + "The field '/propertyLayout/version' is deprecated and moved to '/version'. " + "Please edit this material type source file and move the '\"version\": %u' setting up one level.", + m_propertyLayout.m_versionOld); + return Failure(); + } + + // Set materialtype version and add each version update object into MaterialTypeAsset. + materialTypeAssetCreator.SetVersion(m_version); + { + const AZ::Name rename = AZ::Name{ "rename" }; + + for (const auto& versionUpdate : m_versionUpdates) + { + MaterialVersionUpdate materialVersionUpdate{versionUpdate.m_toVersion}; + for (const auto& action : versionUpdate.m_actions) + { + if (action.m_operation == rename.GetStringView()) + { + materialVersionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction{ + AZ::Name{ action.m_renameFrom }, + AZ::Name{ action.m_renameTo } + }); + } + else + { + materialTypeAssetCreator.ReportWarning("Unsupported material version update operation '%s'", action.m_operation.c_str()); + } + } + materialTypeAssetCreator.AddVersionUpdate(materialVersionUpdate); + } + } + // Used to gather all the UV streams used in this material type from its shaders in alphabetical order. auto semanticComp = [](const RHI::ShaderSemantic& lhs, const RHI::ShaderSemantic& rhs) -> bool { 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 5c7af601fb..e9d8a42641 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAsset.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include @@ -32,8 +33,9 @@ namespace AZ if (auto* serializeContext = azrtti_cast(context)) { serializeContext->Class() - ->Version(10) + ->Version(11) // Material version update ->Field("materialTypeAsset", &MaterialAsset::m_materialTypeAsset) + ->Field("materialTypeVersion", &MaterialAsset::m_materialTypeVersion) ->Field("propertyValues", &MaterialAsset::m_propertyValues) ->Field("propertyNames", &MaterialAsset::m_propertyNames) ; @@ -103,9 +105,25 @@ namespace AZ AZStd::array_view MaterialAsset::GetPropertyValues() const { - if (!m_propertyNames.empty() && m_isDirty) + // If property names are included, they are used to re-arrange the property value list to align with the + // MaterialPropertiesLayout. This realignment would be necessary if the material type is updated with + // a new property layout, and a corresponding material is not reprocessed by the AP and continues using the + // old property layout. + if (!m_propertyNames.empty()) { - const_cast(this)->RealignPropertyValuesAndNames(); + const uint32_t materialTypeVersion = m_materialTypeAsset->GetVersion(); + if (m_materialTypeVersion < materialTypeVersion) + { + // It is possible that the material type has had some properties renamed. If that's the case, and this material + // is still referencing the old property layout, we need to apply any auto updates to rename those properties + // before using them to realign the property values. + const_cast(this)->ApplyVersionUpdates(); + } + + if (m_isDirty) + { + const_cast(this)->RealignPropertyValuesAndNames(); + } } return m_propertyValues; @@ -183,6 +201,40 @@ namespace AZ m_isDirty = false; } + void MaterialAsset::ApplyVersionUpdates() + { + if (m_materialTypeVersion == m_materialTypeAsset->GetVersion()) + { + return; + } + + const uint32_t originalVersion = m_materialTypeVersion; + + bool changesWereApplied = false; + + for (const MaterialVersionUpdate& versionUpdate : m_materialTypeAsset->GetMaterialVersionUpdateList()) + { + if (m_materialTypeVersion < versionUpdate.GetVersion()) + { + if (versionUpdate.ApplyVersionUpdates(*this)) + { + changesWereApplied = true; + m_materialTypeVersion = versionUpdate.GetVersion(); + } + } + } + + if (changesWereApplied) + { + AZ_Warning("MaterialAsset", 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.", + originalVersion, m_materialTypeAsset.ToString().c_str(), m_materialTypeAsset->GetVersion()); + } + + m_materialTypeVersion = m_materialTypeAsset->GetVersion(); + } + void MaterialAsset::ReinitializeMaterialTypeAsset(Data::Asset asset) { Data::Asset newMaterialTypeAsset = { asset.GetAs(), AZ::Data::AssetLoadBehavior::PreLoad }; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp index 79d19c15a2..b62a91a98d 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialAssetCreator.cpp @@ -23,6 +23,7 @@ namespace AZ if (ValidateIsReady()) { m_asset->m_materialTypeAsset = parentMaterial.m_materialTypeAsset; + m_asset->m_materialTypeVersion = m_asset->m_materialTypeAsset->GetVersion(); if (!m_asset->m_materialTypeAsset) { @@ -69,6 +70,7 @@ namespace AZ ReportError("MaterialTypeAsset is null"); return; } + m_asset->m_materialTypeVersion = m_asset->m_materialTypeAsset->GetVersion(); m_materialPropertiesLayout = m_asset->GetMaterialPropertiesLayout(); if (includeMaterialPropertyNames) diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp index 913c3206fb..522ba74119 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAsset.cpp @@ -37,6 +37,7 @@ namespace AZ void MaterialTypeAsset::Reflect(ReflectContext* context) { + MaterialVersionUpdate::Reflect(context); UvNamePair::Reflect(context); if (auto* serializeContext = azrtti_cast(context)) @@ -44,7 +45,9 @@ namespace AZ serializeContext->RegisterGenericType(); serializeContext->Class() - ->Version(4) // ATOM-15472 + ->Version(5) // Material version update + ->Field("Version", &MaterialTypeAsset::m_version) + ->Field("VersionUpdates", &MaterialTypeAsset::m_materialVersionUpdates) ->Field("ShaderCollection", &MaterialTypeAsset::m_shaderCollection) ->Field("MaterialFunctors", &MaterialTypeAsset::m_materialFunctors) ->Field("MaterialSrgShaderIndex", &MaterialTypeAsset::m_materialSrgShaderIndex) @@ -161,6 +164,11 @@ namespace AZ return m_uvNameMap; } + uint32_t MaterialTypeAsset::GetVersion() const + { + return m_version; + } + void MaterialTypeAsset::SetReady() { m_status = AssetStatus::Ready; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp index 4405e835d6..46086dfecc 100644 --- a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp @@ -38,7 +38,7 @@ namespace AZ bool MaterialTypeAssetCreator::End(Data::Asset& result) { - if (!ValidateIsReady() || !ValidateEndMaterialProperty()) + if (!ValidateIsReady() || !ValidateEndMaterialProperty() || !ValidateMaterialVersion()) { return false; } @@ -100,6 +100,48 @@ namespace AZ } } + bool MaterialTypeAssetCreator::ValidateMaterialVersion() + { + if (m_asset->m_materialVersionUpdates.empty()) + { + return true; + } + + uint32_t prevVersion = 0; + for(const MaterialVersionUpdate& versionUpdate : m_asset->m_materialVersionUpdates) + { + if (versionUpdate.GetVersion() <= prevVersion) + { + ReportError("Version updates are not sequential. See version update '%u'.", versionUpdate.GetVersion()); + return false; + } + + if (versionUpdate.GetVersion() > m_asset->m_version) + { + ReportError("Version updates go beyond the current material type version. See version update '%u'.", versionUpdate.GetVersion()); + return false; + } + + prevVersion = versionUpdate.GetVersion(); + } + + const auto& lastMaterialVersionUpdate = m_asset->m_materialVersionUpdates.back(); + for (const auto& action : lastMaterialVersionUpdate.GetActions()) + { + const auto propertyIndex = m_asset->m_materialPropertiesLayout->FindPropertyIndex(AZ::Name{ action.m_toPropertyId }); + if (!propertyIndex.IsValid()) + { + ReportError("Renamed property '%s' not found in material property layout. Check that the property name has been " + "upgraded to the correct version", + action.m_toPropertyId.GetCStr()); + return false; + } + + } + + return true; + } + void MaterialTypeAssetCreator::AddShader(const AZ::Data::Asset& shaderAsset, const ShaderVariantId& shaderVaraintId, const AZ::Name& shaderTag) { if (ValidateIsReady() && ValidateNotNull(shaderAsset, "ShaderAsset")) @@ -123,6 +165,16 @@ namespace AZ AddShader(shaderAsset, ShaderVariantId{}, shaderTag); } + void MaterialTypeAssetCreator::SetVersion(uint32_t version) + { + m_asset->m_version = version; + } + + void MaterialTypeAssetCreator::AddVersionUpdate(const MaterialVersionUpdate& materialVersionUpdate) + { + m_asset->m_materialVersionUpdates.push_back(materialVersionUpdate); + } + void MaterialTypeAssetCreator::ClaimShaderOptionOwnership(const Name& shaderOptionName) { bool optionFound = false; diff --git a/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp new file mode 100644 index 0000000000..b387e502e2 --- /dev/null +++ b/Gems/Atom/RPI/Code/Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp @@ -0,0 +1,89 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace AZ +{ + namespace RPI + { + void MaterialVersionUpdate::RenamePropertyAction::Reflect(ReflectContext* context) + { + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("From", &RenamePropertyAction::m_fromPropertyId) + ->Field("To", &RenamePropertyAction::m_toPropertyId) + ; + } + } + + void MaterialVersionUpdate::Reflect(ReflectContext* context) + { + MaterialVersionUpdate::RenamePropertyAction::Reflect(context); + + if (auto* serializeContext = azrtti_cast(context)) + { + serializeContext->RegisterGenericType(); + + serializeContext->Class() + ->Version(1) + ->Field("ToVersion", &MaterialVersionUpdate::m_toVersion) + ->Field("Actions", &MaterialVersionUpdate::m_actions) + ; + } + } + + MaterialVersionUpdate::MaterialVersionUpdate(uint32_t toVersion) + : m_toVersion(toVersion) + { + } + + uint32_t MaterialVersionUpdate::GetVersion() const + { + return m_toVersion; + } + + void MaterialVersionUpdate::SetVersion(uint32_t toVersion) + { + m_toVersion = toVersion; + } + + bool MaterialVersionUpdate::ApplyVersionUpdates(MaterialAsset& materialAsset) const + { + bool changesWereApplied = false; + + for (auto& propertyName : materialAsset.m_propertyNames) + { + for (const auto& action : m_actions) + { + if (propertyName == action.m_fromPropertyId) + { + propertyName = action.m_toPropertyId; + changesWereApplied = true; + } + } + } + + return changesWereApplied; + } + + const AZ::RPI::MaterialVersionUpdate::Actions& MaterialVersionUpdate::GetActions() const + { + return m_actions; + } + + void MaterialVersionUpdate::AddAction(const RenamePropertyAction& action) + { + m_actions.push_back(action); + } + } // namespace RPI +} // namespace AZ diff --git a/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp b/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp index a5511edea1..31a46e9a6f 100644 --- a/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp +++ b/Gems/Atom/RPI/Code/Tests/Common/AssetSystemStub.cpp @@ -7,6 +7,7 @@ */ #include +#include namespace UnitTest { @@ -36,12 +37,17 @@ namespace UnitTest // Because GetSourceInfoBySourcePath should always return 0 for the sub-id, since it's about the source file not product file. sourceInfo.m_assetInfo.m_assetId.m_subId = 0; - m_sourceInfoMap.emplace(sourcePath, sourceInfo); + AZStd::string normalizedSourcePath = sourcePath; + AzFramework::StringFunc::Path::Normalize(normalizedSourcePath); + m_sourceInfoMap.emplace(normalizedSourcePath, sourceInfo); } bool AssetSystemStub::GetSourceInfoBySourcePath(const char* sourcePath, AZ::Data::AssetInfo& assetInfo, AZStd::string& watchFolder) { - auto iter = m_sourceInfoMap.find(sourcePath); + AZStd::string normalizedSourcePath = sourcePath; + AzFramework::StringFunc::Path::Normalize(normalizedSourcePath); + + auto iter = m_sourceInfoMap.find(normalizedSourcePath); if (iter != m_sourceInfoMap.end()) { diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp index ce223ceb35..58a852f176 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialAssetTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -63,6 +64,11 @@ namespace UnitTest RPITestFixture::TearDown(); } + + void ReplaceMaterialType(Data::Asset materialAsset, Data::Asset upgradedMaterialTypeAsset) + { + materialAsset->m_materialTypeAsset = upgradedMaterialTypeAsset; + } }; TEST_F(MaterialAssetTests, Basic) @@ -202,6 +208,81 @@ namespace UnitTest EXPECT_EQ(serializedAsset->GetPropertyValues()[8].GetValue>(), streamingImageAsset); } + TEST_F(MaterialAssetTests, UpgradeMaterialAsset) + { + // Here we test the main way that a material asset upgrade would be applied at runtime: A material type is updated to + // both rename a property *and* change the order in which properties appear in the layout. In this case, the new name + // must be identified and then that new name is used to find the appropriate index in the property layout. + + auto materialSrgLayout = CreateCommonTestMaterialSrgLayout(); + + auto shaderAsset = CreateTestShaderAsset(Uuid::CreateRandom(), materialSrgLayout); + + Data::Asset testMaterialTypeAssetV1; + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(Uuid::CreateRandom()); + materialTypeCreator.AddShader(shaderAsset); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyInt" }, MaterialPropertyDataType::Int, Name{ "m_int" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyUInt" }, MaterialPropertyDataType::UInt, Name{ "m_uint" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyFloat" }, MaterialPropertyDataType::Float, Name{ "m_float" }); + EXPECT_TRUE(materialTypeCreator.End(testMaterialTypeAssetV1)); + + // Construct the material asset with materialTypeAsset version 1 + Data::AssetId assetId(Uuid::CreateRandom()); + + MaterialAssetCreator creator; + const bool includePropertyNames = true; + creator.Begin(assetId, *testMaterialTypeAssetV1, includePropertyNames); + creator.SetPropertyValue(Name{ "MyInt" }, 7); + creator.SetPropertyValue(Name{ "MyUInt" }, 8u); + creator.SetPropertyValue(Name{ "MyFloat" }, 9.0f); + Data::Asset materialAsset; + EXPECT_TRUE(creator.End(materialAsset)); + + // Prepare material type asset version 2 with the update actions + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction( + { + Name{ "MyInt" }, + Name{ "MyIntRenamed" } + })); + + Data::Asset testMaterialTypeAssetV2; + materialTypeCreator.Begin(Uuid::CreateRandom()); + materialTypeCreator.SetVersion(versionUpdate.GetVersion()); + materialTypeCreator.AddVersionUpdate(versionUpdate); + materialTypeCreator.AddShader(shaderAsset); + // Now we add the properties in a different order from before, and use the new name for MyInt. + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyUInt" }, MaterialPropertyDataType::UInt, Name{ "m_uint" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyFloat" }, MaterialPropertyDataType::Float, Name{ "m_float" }); + AddMaterialPropertyForSrg(materialTypeCreator, Name{ "MyIntRenamed" }, MaterialPropertyDataType::Int, Name{ "m_int" }); + EXPECT_TRUE(materialTypeCreator.End(testMaterialTypeAssetV2)); + + // This is our way of faking the idea that an old version of the MaterialAsset could be loaded with a new version of the MaterialTypeAsset. + ReplaceMaterialType(materialAsset, testMaterialTypeAssetV2); + + // This can find errors and warnings, we are looking for a warning when the version update is applied + ErrorMessageFinder warningFinder; + warningFinder.AddExpectedErrorMessage("Automatic updates are available. Consider updating the .material source file"); + warningFinder.AddExpectedErrorMessage("This material is based on version '1'"); + warningFinder.AddExpectedErrorMessage("material type is now at version '2'"); + + // Even though this material was created using the old version of the material type, it's property values should get automatically + // updated to align with the new property layout in the latest MaterialTypeAsset. + MaterialPropertyIndex myIntIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(Name{"MyIntRenamed"}); + EXPECT_EQ(2, myIntIndex.GetIndex()); + EXPECT_EQ(7, materialAsset->GetPropertyValues()[myIntIndex.GetIndex()].GetValue()); + + warningFinder.CheckExpectedErrorsFound(); + + // Since the MaterialAsset has already been updated, and the warning reported once, we should not see the "consider updating" + // warning reported again on subsequent property accesses. + warningFinder.Reset(); + myIntIndex = materialAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(Name{"MyIntRenamed"}); + EXPECT_EQ(2, myIntIndex.GetIndex()); + EXPECT_EQ(7, materialAsset->GetPropertyValues()[myIntIndex.GetIndex()].GetValue()); + } + TEST_F(MaterialAssetTests, Error_NoBegin) { Data::AssetId assetId(Uuid::CreateRandom()); diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp index dd3b3b2711..acce52ae8e 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialSourceDataTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -17,6 +18,7 @@ #include #include #include +#include #include #include @@ -60,22 +62,72 @@ namespace UnitTest localFileIO->SetAlias("@exefolder@", rootPath); m_testMaterialSrgLayout = CreateCommonTestMaterialSrgLayout(); - m_testShaderAsset = CreateTestShaderAsset(Uuid::CreateRandom(), m_testMaterialSrgLayout); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.shader", m_testShaderAsset.GetId()); - MaterialTypeAssetCreator materialTypeCreator; - materialTypeCreator.Begin(Uuid::CreateRandom()); - materialTypeCreator.AddShader(m_testShaderAsset); - AddCommonTestMaterialProperties(materialTypeCreator, "general."); - materialTypeCreator.End(m_testMaterialTypeAsset); + // The MaterialSourceData relies on both MaterialTypeSourceData and MaterialTypeAsset. We have to make sure the + // .materialtype file is present on disk, and that the MaterialTypeAsset is available through the asset database stub... + + const char* materialTypeJson = R"( + { + "version": 10, + "propertyLayout": { + "properties": { + "general": [ + {"name": "MyBool", "type": "bool"}, + {"name": "MyInt", "type": "Int"}, + {"name": "MyUInt", "type": "UInt"}, + {"name": "MyFloat", "type": "Float"}, + {"name": "MyFloat2", "type": "Vector2"}, + {"name": "MyFloat3", "type": "Vector3"}, + {"name": "MyFloat4", "type": "Vector4"}, + {"name": "MyColor", "type": "Color"}, + {"name": "MyImage", "type": "Image"}, + {"name": "MyEnum", "type": "Enum", "enumValues": ["Enum0", "Enum1", "Enum2"], "defaultValue": "Enum0"} + ] + } + }, + "shaders": [ + { + "file": "@exefolder@/Temp/test.shader" + } + ], + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + {"op": "rename", "from": "general.testColorNameA", "to": "general.testColorNameB"} + ] + }, + { + "toVersion": 4, + "actions": [ + {"op": "rename", "from": "general.testColorNameB", "to": "general.testColorNameC"} + ] + }, + { + "toVersion": 10, + "actions": [ + {"op": "rename", "from": "general.testColorNameC", "to": "general.MyColor"} + ] + } + ] + } + )"; + + AZ::Utils::WriteFile(materialTypeJson, "@exefolder@/Temp/test.materialtype"); + + MaterialTypeSourceData materialTypeSourceData; + LoadTestDataFromJson(materialTypeSourceData, materialTypeJson); + m_testMaterialTypeAsset = materialTypeSourceData.CreateMaterialTypeAsset(Uuid::CreateRandom()).TakeValue(); // Since this test doesn't actually instantiate a Material, it won't need to instantiate this ImageAsset, so all we // need is an asset reference with a valid ID. m_testImageAsset = Data::Asset{ Data::AssetId{Uuid::CreateRandom(), StreamingImageAsset::GetImageAssetSubId()}, azrtti_typeid() }; // Register the test assets with the AssetSystemStub so CreateMaterialAsset() can use AssetUtils. - m_assetSystemStub.RegisterSourceInfo("test.materialtype", m_testMaterialTypeAsset.GetId()); - m_assetSystemStub.RegisterSourceInfo("test.streamingimage", m_testImageAsset.GetId()); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.materialtype", m_testMaterialTypeAsset.GetId()); + m_assetSystemStub.RegisterSourceInfo("@exefolder@/Temp/test.streamingimage", m_testImageAsset.GetId()); } void TearDown() override @@ -88,12 +140,12 @@ namespace UnitTest RPITestFixture::TearDown(); } }; - + void AddPropertyGroup(MaterialSourceData& material, AZStd::string_view groupName) { material.m_properties.insert(groupName); } - + void AddProperty(MaterialSourceData& material, AZStd::string_view groupName, AZStd::string_view propertyName, const MaterialPropertyValue& anyValue) { material.m_properties[groupName][propertyName].m_value = anyValue; @@ -103,7 +155,7 @@ namespace UnitTest { MaterialSourceData sourceData; - sourceData.m_materialType = "test.materialtype"; + sourceData.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceData, "general"); AddProperty(sourceData, "general", "MyBool", true); AddProperty(sourceData, "general", "MyInt", -10); @@ -113,7 +165,7 @@ namespace UnitTest AddProperty(sourceData, "general", "MyFloat2", AZ::Vector2(2.1f, 2.2f)); AddProperty(sourceData, "general", "MyFloat3", AZ::Vector3(3.1f, 3.2f, 3.3f)); AddProperty(sourceData, "general", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f)); - AddProperty(sourceData, "general", "MyImage", AZStd::string("test.streamingimage")); + AddProperty(sourceData, "general", "MyImage", AZStd::string("@exefolder@/Temp/test.streamingimage")); AddProperty(sourceData, "general", "MyEnum", AZStd::string("Enum1")); auto materialAssetOutcome = sourceData.CreateMaterialAsset(Uuid::CreateRandom(), "", true); @@ -139,7 +191,7 @@ namespace UnitTest EXPECT_STREQ(a.m_materialType.data(), b.m_materialType.data()); EXPECT_STREQ(a.m_description.data(), b.m_description.data()); EXPECT_STREQ(a.m_parentMaterial.data(), b.m_parentMaterial.data()); - EXPECT_EQ(a.m_propertyLayoutVersion, b.m_propertyLayoutVersion); + EXPECT_EQ(a.m_materialTypeVersion, b.m_materialTypeVersion); EXPECT_EQ(a.m_properties.size(), b.m_properties.size()); for (auto& groupA : a.m_properties) @@ -170,7 +222,7 @@ namespace UnitTest auto& propertyA = propertyIterA.second; auto& propertyB = propertyIterB->second; - + bool typesMatch = propertyA.m_value.GetTypeId() == propertyB.m_value.GetTypeId(); EXPECT_TRUE(typesMatch); if (typesMatch) @@ -229,8 +281,8 @@ namespace UnitTest " } \n" "} \n"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/roundTripTest.materialtype"; - + const char* materialTypeFilePath = "@exefolder@/Temp/roundTripTest.materialtype"; + AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); file.Write(strlen(materialTypeJson), materialTypeJson); @@ -240,7 +292,7 @@ namespace UnitTest sourceDataOriginal.m_materialType = materialTypeFilePath; sourceDataOriginal.m_parentMaterial = materialTypeFilePath; sourceDataOriginal.m_description = "This is a description"; - sourceDataOriginal.m_propertyLayoutVersion = 7; + sourceDataOriginal.m_materialTypeVersion = 7; AddPropertyGroup(sourceDataOriginal, "groupA"); AddProperty(sourceDataOriginal, "groupA", "MyBool", true); AddProperty(sourceDataOriginal, "groupA", "MyInt", -10); @@ -252,14 +304,14 @@ namespace UnitTest AddPropertyGroup(sourceDataOriginal, "groupC"); AddProperty(sourceDataOriginal, "groupC", "MyFloat4", AZ::Vector4(4.1f, 4.2f, 4.3f, 4.4f)); AddProperty(sourceDataOriginal, "groupC", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f}); - AddProperty(sourceDataOriginal, "groupC", "MyImage", AZStd::string("test.streamingimage")); + AddProperty(sourceDataOriginal, "groupC", "MyImage", AZStd::string("@exefolder@/Temp/test.streamingimage")); AZStd::string sourceDataSerialized; JsonTestResult storeResult = StoreTestDataToJson(sourceDataOriginal, sourceDataSerialized); MaterialSourceData sourceDataCopy; JsonTestResult loadResult = LoadTestDataFromJson(sourceDataCopy, sourceDataSerialized); - + CheckEqual(sourceDataOriginal, sourceDataCopy); } @@ -277,10 +329,10 @@ namespace UnitTest ] } } - } + } )"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype"; + const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype"; AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); @@ -296,7 +348,7 @@ namespace UnitTest "testColor": [0.1,0.2,0.3] } }, - "materialType": "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype" + "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype" } )"; @@ -330,7 +382,7 @@ namespace UnitTest { const AZStd::string inputJson = R"( { - "propertyLayoutVersion": 1, + "materialTypeVersion": 1, "properties": { "baseColor": { "color": [1.0,1.0,1.0] @@ -354,7 +406,7 @@ namespace UnitTest const AZStd::string inputJson = R"( { "materialType": "DoesNotExist.materialtype", - "propertyLayoutVersion": 1, + "materialTypeVersion": 1, "properties": { "baseColor": { "color": [1.0,1.0,1.0] @@ -387,10 +439,10 @@ namespace UnitTest ] } } - } + } )"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype"; + const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype"; AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); @@ -399,8 +451,8 @@ namespace UnitTest const AZStd::string inputJson = R"( { - "materialType": "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype", - "propertyLayoutVersion": 1, + "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype", + "materialTypeVersion": 1, "properties": { "general": { "testColor": [1.0,1.0,1.0] @@ -433,10 +485,10 @@ namespace UnitTest ] } } - } + } )"; - const char* materialTypeFilePath = "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype"; + const char* materialTypeFilePath = "@exefolder@/Temp/simpleMaterialType.materialtype"; AZ::IO::FileIOStream file; EXPECT_TRUE(file.Open(materialTypeFilePath, AZ::IO::OpenMode::ModeWrite | AZ::IO::OpenMode::ModeCreatePath)); @@ -445,8 +497,8 @@ namespace UnitTest const AZStd::string inputJson = R"( { - "materialType": "@exefolder@/Gems/Atom/RPI/Code/Tests/Material/Temp/simpleMaterialType.materialtype", - "propertyLayoutVersion": 1, + "materialType": "@exefolder@/Temp/simpleMaterialType.materialtype", + "materialTypeVersion": 1, "properties": { "general": { "doesNotExist": [1.0,1.0,1.0] @@ -467,20 +519,20 @@ namespace UnitTest TEST_F(MaterialSourceDataTests, CreateMaterialAsset_MultiLevelDataInheritance) { MaterialSourceData sourceDataLevel1; - sourceDataLevel1.m_materialType = "test.materialtype"; + sourceDataLevel1.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceDataLevel1, "general"); AddProperty(sourceDataLevel1, "general", "MyFloat", 1.5f); AddProperty(sourceDataLevel1, "general", "MyColor", AZ::Color{0.1f, 0.2f, 0.3f, 0.4f}); MaterialSourceData sourceDataLevel2; - sourceDataLevel2.m_materialType = "test.materialtype"; + sourceDataLevel2.m_materialType = "@exefolder@/Temp/test.materialtype"; sourceDataLevel2.m_parentMaterial = "level1.material"; AddPropertyGroup(sourceDataLevel2, "general"); AddProperty(sourceDataLevel2, "general", "MyColor", AZ::Color{0.15f, 0.25f, 0.35f, 0.45f}); AddProperty(sourceDataLevel2, "general", "MyFloat2", AZ::Vector2{4.1f, 4.2f}); MaterialSourceData sourceDataLevel3; - sourceDataLevel3.m_materialType = "test.materialtype"; + sourceDataLevel3.m_materialType = "@exefolder@/Temp/test.materialtype"; sourceDataLevel3.m_parentMaterial = "level2.material"; AddPropertyGroup(sourceDataLevel3, "general"); AddProperty(sourceDataLevel3, "general", "MyFloat", 3.5f); @@ -497,7 +549,7 @@ namespace UnitTest auto materialAssetLevel3 = sourceDataLevel3.CreateMaterialAsset(Uuid::CreateRandom(), "", true); EXPECT_TRUE(materialAssetLevel3.IsSuccess()); - + auto layout = m_testMaterialTypeAsset->GetMaterialPropertiesLayout(); MaterialPropertyIndex myFloat = layout->FindPropertyIndex(Name("general.MyFloat")); MaterialPropertyIndex myFloat2 = layout->FindPropertyIndex(Name("general.MyFloat2")); @@ -535,14 +587,14 @@ namespace UnitTest m_assetSystemStub.RegisterSourceInfo("otherBase.materialtype", otherMaterialType.GetId()); MaterialSourceData sourceDataLevel1; - sourceDataLevel1.m_materialType = "test.materialtype"; + sourceDataLevel1.m_materialType = "@exefolder@/Temp/test.materialtype"; MaterialSourceData sourceDataLevel2; - sourceDataLevel2.m_materialType = "test.materialtype"; + sourceDataLevel2.m_materialType = "@exefolder@/Temp/test.materialtype"; sourceDataLevel2.m_parentMaterial = "level1.material"; MaterialSourceData sourceDataLevel3; - sourceDataLevel3.m_materialType = "otherBase.materialtype"; + sourceDataLevel3.m_materialType = "@exefolder@/Temp/otherBase.materialtype"; sourceDataLevel3.m_parentMaterial = "level2.material"; auto materialAssetLevel1 = sourceDataLevel1.CreateMaterialAsset(Uuid::CreateRandom(), "", true); @@ -570,7 +622,7 @@ namespace UnitTest { MaterialSourceData sourceData; - sourceData.m_materialType = "test.materialtype"; + sourceData.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceData, "general"); @@ -587,7 +639,7 @@ namespace UnitTest { MaterialSourceData sourceData; - sourceData.m_materialType = "test.materialtype"; + sourceData.m_materialType = "@exefolder@/Temp/test.materialtype"; AddPropertyGroup(sourceData, "general"); @@ -629,7 +681,7 @@ namespace UnitTest expectWarning([](MaterialSourceData& materialSourceData) { - AddProperty(materialSourceData, "general", "DoesNotExist", AZStd::string("test.streamingimage")); + AddProperty(materialSourceData, "general", "DoesNotExist", AZStd::string("@exefolder@/Temp/test.streamingimage")); }); // Missing image reference @@ -638,6 +690,124 @@ namespace UnitTest AddProperty(materialSourceData, "general", "MyImage", AZStd::string("doesNotExist.streamingimage")); }, 3); // Expect a 3rd error because AssetUtils reports its own assertion failure } + + + TEST_F(MaterialSourceDataTests, Load_MaterialTypeVersionUpdate) + { + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 1, + "properties": { + "general": { + "testColorNameA": [0.1, 0.2, 0.3] + } + } + } + )"; + + 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["general"].find("testColorNameA"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameB"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameC"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("MyColor"), material.m_properties["general"].end()); + + AZ::Color testColor = material.m_properties["general"]["testColorNameA"].m_value.GetValue(); + EXPECT_TRUE(AZ::Color(0.1f, 0.2f, 0.3f, 1.0f).IsClose(testColor, 0.01)); + + EXPECT_EQ(1, 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 '1'"); + 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["general"].find("testColorNameA"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameB"), material.m_properties["general"].end()); + EXPECT_EQ(material.m_properties["general"].find("testColorNameC"), material.m_properties["general"].end()); + EXPECT_NE(material.m_properties["general"].find("MyColor"), material.m_properties["general"].end()); + + testColor = material.m_properties["general"]["MyColor"].m_value.GetValue(); + EXPECT_TRUE(AZ::Color(0.1f, 0.2f, 0.3f, 1.0f).IsClose(testColor, 0.01)); + + 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 + // version so only some of the version updates are applied. + + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 3, + "properties": { + "general": { + "testColorNameB": [0.1, 0.2, 0.3] + } + } + } + )"; + + 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()); + + material.ApplyVersionUpdates(); + + AZ::Color testColor = material.m_properties["general"]["MyColor"].m_value.GetValue(); + EXPECT_TRUE(AZ::Color(0.1f, 0.2f, 0.3f, 1.0f).IsClose(testColor, 0.01)); + + EXPECT_EQ(10, material.m_materialTypeVersion); + } + + TEST_F(MaterialSourceDataTests, Load_Error_MaterialTypeVersionUpdateWithMismatchedVersion) + { + const AZStd::string inputJson = R"( + { + "materialType": "@exefolder@/Temp/test.materialtype", + "materialTypeVersion": 3, // At this version, the property should be testColorNameB not testColorNameA + "properties": { + "general": { + "testColorNameA": [0.1, 0.2, 0.3] + } + } + } + )"; + + MaterialSourceData material; + JsonTestResult loadResult = LoadTestDataFromJson(material, inputJson); + + loadResult.ContainsMessage("/properties/general/testColorNameA", "Property 'general.testColorNameA' not found in material type."); + + EXPECT_FALSE(material.m_properties["general"]["testColorNameA"].m_value.IsValid()); + + material.ApplyVersionUpdates(); + + EXPECT_FALSE(material.m_properties["general"]["MyColor"].m_value.IsValid()); + } + } diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp index b9774c84d9..81b723ec04 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeAssetTests.cpp @@ -14,6 +14,7 @@ #include #include +#include #include #include #include @@ -153,6 +154,16 @@ namespace UnitTest MaterialTypeAssetCreator materialTypeCreator; materialTypeCreator.Begin(assetId); + // Version updates + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction( + { + Name{ "EnableSpecialPassPrev" }, + Name{ "EnableSpecialPass" } + })); + materialTypeCreator.SetVersion(versionUpdate.GetVersion()); + materialTypeCreator.AddVersionUpdate(versionUpdate); + // Built-in shader materialTypeCreator.AddShader(m_testShaderAsset); @@ -198,7 +209,7 @@ namespace UnitTest { EXPECT_EQ(m_testMaterialSrgLayout, materialTypeAsset->GetMaterialSrgLayout()); EXPECT_EQ(5, materialTypeAsset->GetMaterialPropertiesLayout()->GetPropertyCount()); - + EXPECT_EQ(2, materialTypeAsset->GetVersion()); // Check aliased properties const MaterialPropertyIndex colorIndex = materialTypeAsset->GetMaterialPropertiesLayout()->FindPropertyIndex(Name{ "MyColor" }); @@ -490,6 +501,106 @@ namespace UnitTest }); } + TEST_F(MaterialTypeAssetTests, Error_InvalidMaterialVersionUpdate_WrongName) + { + Data::Asset materialTypeAsset; + + Data::AssetId assetId(Uuid::CreateRandom()); + + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(assetId); + + // Invalid version updates + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction( + { + Name{ "EnableSpecialPassPrev" }, + Name{ "InvalidPropertyName" } + })); + materialTypeCreator.SetVersion(versionUpdate.GetVersion()); + materialTypeCreator.AddVersionUpdate(versionUpdate); + materialTypeCreator.AddShader(m_testShaderAsset); + + materialTypeCreator.BeginMaterialProperty(Name{ "EnableSpecialPass" }, MaterialPropertyDataType::Bool); + materialTypeCreator.EndMaterialProperty(); + + AZ_TEST_START_ASSERTTEST; + EXPECT_FALSE(materialTypeCreator.End(materialTypeAsset)); + AZ_TEST_STOP_ASSERTTEST(1); + EXPECT_EQ(1, materialTypeCreator.GetErrorCount()); + } + + TEST_F(MaterialTypeAssetTests, Error_InvalidMaterialVersionUpdate_WrongOrder) + { + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(Uuid::CreateRandom()); + + materialTypeCreator.SetVersion(4); + materialTypeCreator.AddShader(m_testShaderAsset); + materialTypeCreator.BeginMaterialProperty(Name{ "d" }, MaterialPropertyDataType::Bool); + materialTypeCreator.EndMaterialProperty(); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Version updates are not sequential. See version update '3'"); + + { + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "a" },Name{ "b" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + { + MaterialVersionUpdate versionUpdate(4); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "b" },Name{ "c" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + { + MaterialVersionUpdate versionUpdate(3); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "c" },Name{ "d" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + Data::Asset materialTypeAsset; + EXPECT_FALSE(materialTypeCreator.End(materialTypeAsset)); + + errorMessageFinder.CheckExpectedErrorsFound(); + + EXPECT_EQ(1, materialTypeCreator.GetErrorCount()); + } + + TEST_F(MaterialTypeAssetTests, Error_InvalidMaterialVersionUpdate_GoesTooFar) + { + MaterialTypeAssetCreator materialTypeCreator; + materialTypeCreator.Begin(Uuid::CreateRandom()); + + materialTypeCreator.SetVersion(3); + materialTypeCreator.AddShader(m_testShaderAsset); + materialTypeCreator.BeginMaterialProperty(Name{ "d" }, MaterialPropertyDataType::Bool); + materialTypeCreator.EndMaterialProperty(); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Version updates go beyond the current material type version. See version update '4'"); + + { + MaterialVersionUpdate versionUpdate(2); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "a" },Name{ "b" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + { + MaterialVersionUpdate versionUpdate(4); + versionUpdate.AddAction(MaterialVersionUpdate::RenamePropertyAction({Name{ "b" },Name{ "c" }})); + materialTypeCreator.AddVersionUpdate(versionUpdate); + } + + Data::Asset materialTypeAsset; + EXPECT_FALSE(materialTypeCreator.End(materialTypeAsset)); + + errorMessageFinder.CheckExpectedErrorsFound(); + + EXPECT_EQ(1, materialTypeCreator.GetErrorCount()); + } TEST_F(MaterialTypeAssetTests, MaterialTypeWithNoSRGOrProperties) { diff --git a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp index ba4a58b9ff..b811e630d0 100644 --- a/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp +++ b/Gems/Atom/RPI/Code/Tests/Material/MaterialTypeSourceDataTests.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -978,8 +979,16 @@ namespace UnitTest const AZStd::string inputJson = R"( { "description": "This is a general description about the material", + "version": 2, + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + { "op": "rename", "from": "groupA.fooPrev", "to": "groupA.foo" } + ] + } + ], "propertyLayout": { - "version": 2, "groups": [ { "name": "groupA", @@ -1062,7 +1071,12 @@ namespace UnitTest EXPECT_EQ(material.m_description, "This is a general description about the material"); - EXPECT_EQ(material.m_propertyLayout.m_version, 2); + EXPECT_EQ(material.m_version, 2); + EXPECT_EQ(material.m_versionUpdates.size(), 1); + EXPECT_EQ(material.m_versionUpdates[0].m_toVersion, 2); + EXPECT_EQ(material.m_versionUpdates[0].m_actions[0].m_operation, "rename"); + EXPECT_EQ(material.m_versionUpdates[0].m_actions[0].m_renameFrom, "groupA.fooPrev"); + EXPECT_EQ(material.m_versionUpdates[0].m_actions[0].m_renameTo, "groupA.foo"); EXPECT_EQ(material.m_propertyLayout.m_groups.size(), 2); EXPECT_TRUE(material.FindGroup("groupA") != nullptr); @@ -1208,8 +1222,6 @@ namespace UnitTest EXPECT_EQ(material.m_description, "This is a general description about the material"); - EXPECT_EQ(material.m_propertyLayout.m_version, 2); - EXPECT_EQ(material.m_propertyLayout.m_groups.size(), 2); EXPECT_TRUE(material.FindGroup("groupA") != nullptr); EXPECT_TRUE(material.FindGroup("groupB") != nullptr); @@ -1266,7 +1278,6 @@ namespace UnitTest { "description": "", "propertyLayout": { - "version": 2, "groups": [ { "name": "general", @@ -1305,4 +1316,191 @@ namespace UnitTest CheckPropertyValue>(materialTypeAsset, Name{ "general.absolute" }, m_testImageAsset2); CheckPropertyValue>(materialTypeAsset, Name{ "general.relative" }, m_testImageAsset2); } + + + TEST_F(MaterialTypeSourceDataTests, FindPropertyUsingOldName) + { + const AZStd::string inputJson = R"( + { + "version": 10, + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + { "op": "rename", "from": "general.fooA", "to": "general.fooB" } + ] + }, + { + "toVersion": 4, + "actions": [ + { "op": "rename", "from": "general.barA", "to": "general.barB" } + ] + }, + { + "toVersion": 6, + "actions": [ + { "op": "rename", "from": "general.fooB", "to": "general.fooC" }, + { "op": "rename", "from": "general.barB", "to": "general.barC" } + ] + }, + { + "toVersion": 7, + "actions": [ + { "op": "rename", "from": "general.bazA", "to": "otherGroup.bazB" } + ] + } + ], + "propertyLayout": { + "properties": { + "general": [ + { + "name": "fooC", + "type": "Bool" + }, + { + "name": "barC", + "type": "Float" + } + ], + "otherGroup": [ + { + "name": "dontMindMe", + "type": "Bool" + }, + { + "name": "bazB", + "type": "Float" + } + ] + } + } + } + )"; + + MaterialTypeSourceData materialType; + JsonTestResult loadResult = LoadTestDataFromJson(materialType, inputJson); + + EXPECT_EQ(materialType.m_version, 10); + + // First find the properties using their correct current names + const MaterialTypeSourceData::PropertyDefinition* foo = materialType.FindProperty("general", "fooC"); + const MaterialTypeSourceData::PropertyDefinition* bar = materialType.FindProperty("general", "barC"); + const MaterialTypeSourceData::PropertyDefinition* baz = materialType.FindProperty("otherGroup", "bazB"); + + EXPECT_TRUE(foo); + EXPECT_TRUE(bar); + EXPECT_TRUE(baz); + EXPECT_EQ(foo->m_name, "fooC"); + EXPECT_EQ(bar->m_name, "barC"); + EXPECT_EQ(baz->m_name, "bazB"); + + // Now try doing the property lookup using old versions of the name and make sure the same property can be found + + EXPECT_EQ(foo, materialType.FindProperty("general", "fooA")); + EXPECT_EQ(foo, materialType.FindProperty("general", "fooB")); + EXPECT_EQ(bar, materialType.FindProperty("general", "barA")); + EXPECT_EQ(bar, materialType.FindProperty("general", "barB")); + EXPECT_EQ(baz, materialType.FindProperty("general", "bazA")); + + 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")); + } + + TEST_F(MaterialTypeSourceDataTests, FindPropertyUsingOldName_Error_UnsupportedVersionUpdate) + { + const AZStd::string inputJson = R"( + { + "version": 10, + "versionUpdates": [ + { + "toVersion": 2, + "actions": [ + { "op": "notRename", "from": "general.fooA", "to": "general.fooB" } + ] + } + ], + "propertyLayout": { + "properties": { + "general": [ + { + "name": "fooB", + "type": "Bool" + } + ] + } + } + } + )"; + + MaterialTypeSourceData materialType; + JsonTestResult loadResult = LoadTestDataFromJson(materialType, inputJson); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Unsupported material version update operation 'notRename'"); + + + const MaterialTypeSourceData::PropertyDefinition* foo = materialType.FindProperty("general", "fooA"); + + EXPECT_EQ(nullptr, foo); + + errorMessageFinder.CheckExpectedErrorsFound(); + } + + TEST_F(MaterialTypeSourceDataTests, CreateMaterialTypeAsset_Error_UnsupportedVersionUpdate) + { + MaterialTypeSourceData sourceData; + + MaterialTypeSourceData::PropertyDefinition propertySource; + propertySource.m_name = "a"; + propertySource.m_dataType = MaterialPropertyDataType::Int; + propertySource.m_value = 0; + sourceData.m_propertyLayout.m_properties["general"].push_back(propertySource); + + sourceData.m_version = 2; + + MaterialTypeSourceData::VersionUpdateDefinition versionUpdate; + versionUpdate.m_toVersion = 2; + MaterialTypeSourceData::VersionUpdatesRenameOperationDefinition updateAction; + updateAction.m_operation = "operationNotKnown"; + versionUpdate.m_actions.push_back(updateAction); + sourceData.m_versionUpdates.push_back(versionUpdate); + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("Unsupported material version update operation 'operationNotKnown'"); + errorMessageFinder.AddIgnoredErrorMessage("Failed to build MaterialTypeAsset", true); + + auto materialTypeOutcome = sourceData.CreateMaterialTypeAsset(Uuid::CreateRandom()); + EXPECT_FALSE(materialTypeOutcome.IsSuccess()); + + errorMessageFinder.CheckExpectedErrorsFound(); + } + + TEST_F(MaterialTypeSourceDataTests, CreateMaterialTypeAsset_Error_VersionInWrongLocation) + { + // The version field used to be under the propertyLayout section, but it has been moved up to the top level. + // If any users have their own custom .materialtype with an older format that has the version in the wrong place + // then we will report an error with instructions to move it to the correct location. + + ErrorMessageFinder errorMessageFinder; + errorMessageFinder.AddExpectedErrorMessage("The field '/propertyLayout/version' is deprecated and moved to '/version'. Please edit this material type source file and move the '\"version\": 4' setting up one level"); + + const AZStd::string inputJson = R"( + { + "propertyLayout": { + "version": 4 + } + } + )"; + + MaterialTypeSourceData materialType; + JsonTestResult loadResult = LoadTestDataFromJson(materialType, inputJson); + + auto materialTypeOutcome = materialType.CreateMaterialTypeAsset(Uuid::CreateRandom()); + EXPECT_FALSE(materialTypeOutcome.IsSuccess()); + + errorMessageFinder.CheckExpectedErrorsFound(); + } } diff --git a/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake b/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake index 49c7231fed..4f0e432511 100644 --- a/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake +++ b/Gems/Atom/RPI/Code/atom_rpi_reflect_files.cmake @@ -61,6 +61,7 @@ set(FILES Include/Atom/RPI.Reflect/Material/MaterialTypeAssetCreator.h Include/Atom/RPI.Reflect/Material/ShaderCollection.h Include/Atom/RPI.Reflect/Material/MaterialFunctor.h + Include/Atom/RPI.Reflect/Material/MaterialVersionUpdate.h Include/Atom/RPI.Reflect/Pass/ComputePassData.h Include/Atom/RPI.Reflect/Pass/CopyPassData.h Include/Atom/RPI.Reflect/Pass/DownsampleMipChainPassData.h @@ -141,6 +142,7 @@ set(FILES Source/RPI.Reflect/Material/MaterialTypeAssetCreator.cpp Source/RPI.Reflect/Material/ShaderCollection.cpp Source/RPI.Reflect/Material/MaterialFunctor.cpp + Source/RPI.Reflect/Material/MaterialVersionUpdate.cpp Source/RPI.Reflect/Pass/PassAsset.cpp Source/RPI.Reflect/Pass/PassAttachmentReflect.cpp Source/RPI.Reflect/Pass/PassRequest.cpp diff --git a/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype b/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype index 00f11663f7..cf0bffa058 100644 --- a/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype +++ b/Gems/Atom/TestData/TestData/Materials/Types/AutoBrick.materialtype @@ -1,7 +1,7 @@ { "description": "This is an example of a custom material type using Atom's PBR shading model: procedurally generated brick or tile.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "shape", diff --git a/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype b/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype index 81ebd63c28..5d99737576 100644 --- a/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype +++ b/Gems/Atom/TestData/TestData/Materials/Types/MinimalPBR.materialtype @@ -1,7 +1,7 @@ { "description": "Base Material with properties used to define Standard PBR, a metallic-roughness Physically-Based Rendering (PBR) material shading model.", + "version": 3, "propertyLayout": { - "version": 3, "groups": [ { "name": "settings", diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp index 58b07d8351..e8ec77e7bd 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/Application/AtomToolsApplication.cpp @@ -36,6 +36,8 @@ #include #include +#include "AtomToolsFramework_Traits_Platform.h" + AZ_PUSH_DISABLE_WARNING(4251 4800, "-Wunknown-warning-option") // disable warnings spawned by QT #include #include @@ -217,7 +219,7 @@ namespace AtomToolsFramework AzFramework::AssetSystemRequestBus::Broadcast(&AzFramework::AssetSystem::AssetSystemRequests::StartDisconnectingAssetProcessor); #if AZ_TRAIT_ATOMTOOLSFRAMEWORK_SKIP_APP_DESTROY - _exit(0); + ::_exit(0); #else Base::Destroy(); #endif diff --git a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp index a0d2034082..2b39a87623 100644 --- a/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp +++ b/Gems/Atom/Tools/AtomToolsFramework/Code/Source/PreviewRenderer/PreviewRenderer.cpp @@ -61,7 +61,7 @@ namespace AtomToolsFramework AZ::RPI::RenderPipelineDescriptor pipelineDesc; pipelineDesc.m_mainViewTagName = "MainCamera"; pipelineDesc.m_name = pipelineName; - pipelineDesc.m_rootPassTemplate = "MainPipelineRenderToTexture"; + pipelineDesc.m_rootPassTemplate = "ToolsPipelineRenderToTexture"; // We have to set the samples to 4 to match the pipeline passes' setting, otherwise it may lead to device lost issue // [GFX TODO] [ATOM-13551] Default value sand validation required to prevent pipeline crash and device lost diff --git a/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp b/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp index 17e292ac16..98af749261 100644 --- a/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp +++ b/Gems/Atom/Tools/MaterialEditor/Code/Source/Document/MaterialDocument.cpp @@ -230,9 +230,11 @@ namespace MaterialEditor // create source data from properties MaterialSourceData sourceData; - sourceData.m_propertyLayoutVersion = m_materialTypeSourceData.m_propertyLayout.m_version; sourceData.m_materialType = m_materialSourceData.m_materialType; sourceData.m_parentMaterial = m_materialSourceData.m_parentMaterial; + + AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); + sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); @@ -302,9 +304,11 @@ namespace MaterialEditor // create source data from properties MaterialSourceData sourceData; - sourceData.m_propertyLayoutVersion = m_materialTypeSourceData.m_propertyLayout.m_version; sourceData.m_materialType = m_materialSourceData.m_materialType; sourceData.m_parentMaterial = m_materialSourceData.m_parentMaterial; + + AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); + sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Force save data to store forward slashes AzFramework::StringFunc::Replace(sourceData.m_materialType, "\\", "/"); @@ -373,8 +377,10 @@ namespace MaterialEditor // create source data from properties MaterialSourceData sourceData; - sourceData.m_propertyLayoutVersion = m_materialTypeSourceData.m_propertyLayout.m_version; sourceData.m_materialType = m_materialSourceData.m_materialType; + + AZ_Assert(m_materialAsset && m_materialAsset->GetMaterialTypeAsset(), "When IsOpen() is true, these assets should not be null."); + sourceData.m_materialTypeVersion = m_materialAsset->GetMaterialTypeAsset()->GetVersion(); // Only assign a parent path if the source was a .material if (AzFramework::StringFunc::Path::IsExtension(m_relativePath.c_str(), MaterialSourceData::Extension)) @@ -679,6 +685,12 @@ namespace MaterialEditor return false; } m_materialTypeSourceData = materialTypeOutcome.GetValue(); + + if (MaterialSourceData::ApplyVersionUpdatesResult::Failed == m_materialSourceData.ApplyVersionUpdates(m_absolutePath)) + { + AZ_Error("MaterialDocument", false, "Material source data could not be auto updated to the latest version of the material type: '%s'.", m_materialSourceData.m_materialType.c_str()); + return false; + } } else if (AzFramework::StringFunc::Path::IsExtension(m_absolutePath.c_str(), MaterialTypeSourceData::Extension)) { diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h index 9ccc4f329b..3cafc183a5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightBus.h @@ -176,6 +176,14 @@ namespace AZ //! Shadow bias reduces acne by applying a small amount of offset along shadow-space z. //! @param Sets the amount of bias to apply. virtual void SetShadowBias(float bias) = 0; + + //! Reduces acne by biasing the shadowmap lookup along the geometric normal. + //! @return Returns the amount of bias to apply. + virtual float GetNormalShadowBias() const = 0; + + //! Reduces acne by biasing the shadowmap lookup along the geometric normal. + //! @param normalShadowBias Sets the amount of normal shadow bias to apply. + virtual void SetNormalShadowBias(float normalShadowBias) = 0; }; using DirectionalLightRequestBus = EBus; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h index 20073f437c..92d5cc9ac0 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Include/AtomLyIntegration/CommonFeatures/CoreLights/DirectionalLightComponentConfig.h @@ -101,6 +101,9 @@ namespace AZ //! Method of shadow's filtering. ShadowFilterMethod m_shadowFilterMethod = ShadowFilterMethod::None; + // Reduces acne by biasing the shadowmap lookup along the geometric normal. + float m_normalShadowBias = 0.0f; + //! Sample Count for filtering (from 4 to 64) //! It is used only when the pixel is predicted as on the boundary. uint16_t m_filteringSampleCount = 32; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp index 98d3f838c0..78a9cc21d1 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentConfig.cpp @@ -39,7 +39,8 @@ namespace AZ ->Field("ShadowFilterMethod", &DirectionalLightComponentConfig::m_shadowFilterMethod) ->Field("PcfFilteringSampleCount", &DirectionalLightComponentConfig::m_filteringSampleCount) ->Field("ShadowReceiverPlaneBiasEnabled", &DirectionalLightComponentConfig::m_receiverPlaneBiasEnabled) - ->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias); + ->Field("Shadow Bias", &DirectionalLightComponentConfig::m_shadowBias) + ->Field("Normal Shadow Bias", &DirectionalLightComponentConfig::m_normalShadowBias); } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp index c20a9f8e17..e36868c4bb 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.cpp @@ -86,6 +86,8 @@ namespace AZ ->Event("SetShadowReceiverPlaneBiasEnabled", &DirectionalLightRequestBus::Events::SetShadowReceiverPlaneBiasEnabled) ->Event("GetShadowBias", &DirectionalLightRequestBus::Events::GetShadowBias) ->Event("SetShadowBias", &DirectionalLightRequestBus::Events::SetShadowBias) + ->Event("GetNormalShadowBias", &DirectionalLightRequestBus::Events::GetNormalShadowBias) + ->Event("SetNormalShadowBias", &DirectionalLightRequestBus::Events::SetNormalShadowBias) ->VirtualProperty("Color", "GetColor", "SetColor") ->VirtualProperty("Intensity", "GetIntensity", "SetIntensity") ->VirtualProperty("AngularDiameter", "GetAngularDiameter", "SetAngularDiameter") @@ -101,7 +103,8 @@ namespace AZ ->VirtualProperty("ShadowFilterMethod", "GetShadowFilterMethod", "SetShadowFilterMethod") ->VirtualProperty("FilteringSampleCount", "GetFilteringSampleCount", "SetFilteringSampleCount") ->VirtualProperty("ShadowReceiverPlaneBiasEnabled", "GetShadowReceiverPlaneBiasEnabled", "SetShadowReceiverPlaneBiasEnabled") - ->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias"); + ->VirtualProperty("ShadowBias", "GetShadowBias", "SetShadowBias") + ->VirtualProperty("NormalShadowBias", "GetNormalShadowBias", "SetNormalShadowBias"); ; } } @@ -423,6 +426,20 @@ namespace AZ return m_configuration.m_shadowBias; } + void DirectionalLightComponentController::SetNormalShadowBias(float bias) + { + m_configuration.m_normalShadowBias = bias; + if (m_featureProcessor) + { + m_featureProcessor->SetNormalShadowBias(m_lightHandle, bias); + } + } + + float DirectionalLightComponentController::GetNormalShadowBias() const + { + return m_configuration.m_normalShadowBias; + } + void DirectionalLightComponentController::SetFilteringSampleCount(uint32_t count) { const uint16_t count16 = GetMin(Shadow::MaxPcfSamplingCount, aznumeric_cast(count)); @@ -517,6 +534,7 @@ namespace AZ SetDebugColoringEnabled(m_configuration.m_isDebugColoringEnabled); SetShadowFilterMethod(m_configuration.m_shadowFilterMethod); SetShadowBias(m_configuration.m_shadowBias); + SetNormalShadowBias(m_configuration.m_normalShadowBias); SetFilteringSampleCount(m_configuration.m_filteringSampleCount); SetShadowReceiverPlaneBiasEnabled(m_configuration.m_receiverPlaneBiasEnabled); diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h index a0d552cf99..9a6edda666 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/DirectionalLightComponentController.h @@ -81,7 +81,9 @@ namespace AZ bool GetShadowReceiverPlaneBiasEnabled() const override; void SetShadowReceiverPlaneBiasEnabled(bool enable) override; float GetShadowBias() const override; - void SetShadowBias(float width) override; + void SetShadowBias(float bias) override; + float GetNormalShadowBias() const override; + void SetNormalShadowBias(float bias) override; private: friend class EditorDirectionalLightComponent; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp index 1e5b2580f7..db46434d9a 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorAreaLightComponent.cpp @@ -136,7 +136,7 @@ namespace AZ ->Attribute(Edit::Attributes::Min, 0.0f) ->Attribute(Edit::Attributes::Max, 100.0f) ->Attribute(Edit::Attributes::SoftMin, 0.0f) - ->Attribute(Edit::Attributes::SoftMax, 1.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) ->Attribute(Edit::Attributes::Visibility, &AreaLightComponentConfig::SupportsShadows) ->Attribute(Edit::Attributes::ReadOnly, &AreaLightComponentConfig::ShadowsDisabled) diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp index 99e2cc1485..545064b86f 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/CoreLights/EditorDirectionalLightComponent.cpp @@ -134,7 +134,7 @@ namespace AZ ->EnumAttribute(ShadowFilterMethod::EsmPcf, "ESM+PCF") ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) ->DataElement(Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_filteringSampleCount, "Filtering sample count\n", - "This is used only when the pixel is predicted as on the boundary.\n" + "This is used only when the pixel is predicted to be on the boundary.\n" "Specific to PCF and ESM+PCF.") ->Attribute(Edit::Attributes::Min, 4) ->Attribute(Edit::Attributes::Max, 64) @@ -154,6 +154,13 @@ namespace AZ ->Attribute(Edit::Attributes::Min, 0.f) ->Attribute(Edit::Attributes::Max, 0.2) ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) + ->DataElement( + Edit::UIHandlers::Slider, &DirectionalLightComponentConfig::m_normalShadowBias, "Normal Shadow Bias\n", + "Reduces acne by biasing the shadowmap lookup along the geometric normal.\n" + "If this is 0, no biasing is applied.") + ->Attribute(Edit::Attributes::Min, 0.f) + ->Attribute(Edit::Attributes::Max, 10.0f) + ->Attribute(Edit::Attributes::ChangeNotify, Edit::PropertyRefreshLevels::ValuesOnly) ; } } diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp index 070c42fd7c..d15db886d6 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/Material/EditorMaterialComponentUtil.cpp @@ -99,7 +99,6 @@ namespace AZ { // Construct the material source data object that will be exported AZ::RPI::MaterialSourceData exportData; - exportData.m_propertyLayoutVersion = editData.m_materialTypeSourceData.m_propertyLayout.m_version; // Converting absolute material paths to relative paths bool result = false; diff --git a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp index 2d8f83a32a..e32b26f0c5 100644 --- a/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp +++ b/Gems/AtomLyIntegration/CommonFeatures/Code/Source/PostProcess/ColorGrading/EditorHDRColorGradingComponent.cpp @@ -131,16 +131,20 @@ namespace AZ ->Attribute(Edit::Attributes::Max, 1.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsStart, "Shadows Start", "SMH Shadows Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhShadowsEnd, "Shadows End", "SMH Shadows End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsStart, "Highlights Start", "SMH Highlights Start Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Slider, &HDRColorGradingComponentConfig::m_smhHighlightsEnd, "Highlights End", "SMH Highlights End Value") ->Attribute(Edit::Attributes::Min, 0.0f) - ->Attribute(Edit::Attributes::Max, 1.0f) + ->Attribute(Edit::Attributes::Max, 16.0f) + ->Attribute(Edit::Attributes::SoftMax, 2.0f) ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhShadowsColor, "Shadows Color", "SMH Shadows Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhMidtonesColor, "Midtones Color", "SMH Midtones Color") ->DataElement(AZ::Edit::UIHandlers::Color, &HDRColorGradingComponentConfig::m_smhHighlightsColor, "Highlights Color", "SMH Highlights Color") diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env.example similarity index 63% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env.example index 080b4e92f7..29c1739992 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.env.example @@ -4,18 +4,18 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # # -# -- This line is 75 characters ------------------------------------------- +# ------------------------------------------------------------------------- # Sets up the project environment for python scripting using the export DYNACONF_COMPANY=Amazon -# if a lumberyard project isn't set use this gem -export DYNACONF_LY_PROJECT=DccScriptingInterface -export DYNACONF_LY_PROJECT_PATH=`pwd` -export DYNACONF_LY_DEV=${LY_PROJECT_PATH}\..\..\..\.. +# if a O3DE project isn't set use this gem +export DYNACONF_O3DE_PROJECT=DccScriptingInterface +export DYNACONF_O3DE_PROJECT_PATH=`pwd` +export DYNACONF_O3DE_DEV=${O3DE_PROJECT_PATH}\..\..\..\.. # LY build folder -export DYNACONF_LY_BUILD_PATH=${LY_DEV}\build -export DYNACONF_LY_BIN_PATH=${LY_BUILD_PATH}\bin\profile +export DYNACONF_O3DE_BUILD_PATH=${O3DE_DEV}\build +export DYNACONF_O3DE_BIN_PATH=${O3DE_BUILD_PATH}\bin\profile # default IDE and debug settings #export DYNACONF_DCCSI_GDEBUG=false @@ -24,9 +24,9 @@ export DYNACONF_DCCSI_GDEBUGGER=WING export DYNACONF_DCCSI_LOGLEVEL=20 # defaults for DccScriptingInterface (DCCsi) -export DYNACONF_DCCSIG_PATH=${LY_DEV}\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface +export DYNACONF_DCCSIG_PATH=${O3DE_DEV}\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface -# set up default python interpreter (Lumberyard) +# set up default python interpreter (O3DE) # we may want to entirely remove these and rely on config.py to dynamically set up # however VScode can be configured with a .env so might be valueable to keep export DYNACONF_DCCSI_PY_VERSION_MAJOR=3 @@ -40,13 +40,13 @@ export DYNACONF_DCCSI_PYTHON_PATH=${DCCSIG_PATH}\3rdParty\Python export DYNACONF_DCCSI_PYTHON_LIB_PATH=${DCCSI_PYTHON_PATH}\Lib\${DCCSI_PY_VERSION_MAJOR}.x\${DCCSI_PY_VERSION_MAJOR}.${DCCSI_PY_VERSION_MINOR}.x\site-packages # TO DO: figure out how to best deal with OS folder (i.e. 'windows') -export DYNACONF_DCCSI_PYTHON_INSTALL=${LY_DEV}\python -export DYNACONF_DDCCSI_PY_BASE=${DCCSI_PYTHON_INSTALL}\python.cmd +export DYNACONF_O3DE_PYTHON_INSTALL=${O3DE_DEV}\python +export DYNACONF_DCCSI_PY_BASE=${O3DE_PYTHON_INSTALL}\python.cmd # set up Qt / PySide2 # TO DO: These should NOT be set in the global env as they will cause conflicts # with other Qt apps (like DCC tools), only set in local.env, or modify config.py # for utils/tools/apps that need them ( see config.init_ly_pyside() ) -#export DYNACONF_QTFORPYTHON_PATH=${LY_DEV}\Gems\QtForPython\3rdParty\pyside2\windows\release -#export DYNACONF_QT_PLUGIN_PATH=${LY_BUILD_PATH}\bin\profile\EditorPlugins -#export DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH=${LY_BUILD_PATH}\bin\profile\EditorPlugins\platforms +#export DYNACONF_QTFORPYTHON_PATH=${O3DE_DEV}\Gems\QtForPython\3rdParty\pyside2\windows\release +#export DYNACONF_QT_PLUGIN_PATH=${O3DE_BUILD_PATH}\bin\profile\EditorPlugins +#export DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH=${O3DE_BUILD_PATH}\bin\profile\EditorPlugins\platforms diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore index 9ad4717873..086d189eba 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.gitignore @@ -8,4 +8,6 @@ workspace.xml # Ignore dynaconf secret files .secrets.* settings.local.json -azpy/_sample_package_/* \ No newline at end of file +azpy/_sample_package_/* +.env +settings_export.json.tmp \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore index dedde06bc4..2ff9b6b614 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/.p4ignore @@ -19,3 +19,4 @@ __WIP__/* !.gitignore .secrets.* settings.local.json +.env \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/README.txt new file mode 100644 index 0000000000..7f5a72fb1c --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/3rdParty/Python/README.txt @@ -0,0 +1,36 @@ +DccScriptingInterface (DCCsi) + +This location can be extended with additional 3rdParty Python Utils, Tools, Packages, etc. + +These are not installed or distributed with O3DE + +However, there is some stubbed scaffolding in place. + +This is a bootstrapped sandbox for installing python libs (version bootstrapped procedurally): +C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages +C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\3.x\3.7.x\site-packages + +Any libs installed to this location will be accessible (use at your own risk) + +For instance, if you want to add py2.7 compatible libs, for apps like Maya2020: +"C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Tools\DCC\Maya\readme.txt" + +These pyside2-tools can be useful, and they are not pip installed, nor distributed. +C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\pyside2-tools + +pyside2-tools instructions: + +1. clone the repo in this location: C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python + >git clone https://github.com/pyside/pyside2-tools + +2. to use as a python package ... + find this and copy: + "< local DCCsi >\3rdParty\Python\pyside2-tools\pyside2uic\__init__.py.in" + + and rename to this: + "< local DCCsi >\3rdParty\Python\pyside2-tools\pyside2uic\__init__.py" + +3. add to PYTHONPATH: < local DCCsi >\3rdParty\Python + in .py something like: site.addsitedir(DCCSI_PYSIDE2_TOOLS) + +See: "< local DCCsi >\config.py" \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py index d417c572e9..406e2b04ff 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Editor/Scripts/bootstrap.py @@ -7,9 +7,9 @@ # SPDX-License-Identifier: Apache-2.0 OR MIT # # -# -- This line is 75 characters ------------------------------------------- +# ------------------------------------------------------------------------- """This module is for use in boostrapping the DccScriptingInterface Gem -with Lumberyard. Note: this boostrap is only designed fo be py3 compatible. +with O3DE. Note: this boostrap is only designed fo be py3 compatible. If you need DCCsi access in py27 (Autodesk Maya for instance) you may need to implement your own boostrapper module. Currently this is boostrapped from add_dccsi.py, as a temporty measure related to this Jira: @@ -24,40 +24,64 @@ import logging as _logging # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +_O3DE_RUNNING=None +try: + import azlmbr + _O3DE_RUNNING=True +except: + _O3DE_RUNNING=False +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- # we don't use dynaconf setting here as we might not yet have access # to that site-dir. -_MODULE = 'DCCsi.bootstrap' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'O3DE.DCCsi.bootstrap' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) # we need to set up basic access to the DCCsi _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? -_DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../../..')) -_DCCSIG_PATH = os.getenv('DCCSIG_PATH', _DCCSIG_PATH) -site.addsitedir(_DCCSIG_PATH) +_DCCSI_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../../..')) +_DCCSI_PATH = os.getenv('DCCSI_PATH', _DCCSI_PATH) +site.addsitedir(_DCCSI_PATH) -# we can get basic access to the DCCsi.azpy api now -import azpy +# now we have azpy api access +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import ENVAR_DCCSI_LOGLEVEL +from azpy.constants import FRMT_LOG_LONG -# early attach WingIDE debugger (can refactor to include other IDEs later) -while 0: # flag on to attemp to connect wingIDE debugger - from azpy.env_bool import env_bool - if not env_bool('DCCSI_DEBUGGER_ATTACHED', False): - # if not already attached lets do it here - from azpy.test.entry_test import connect_wing - foo = connect_wing() +# set up global space, logging etc. +# set these true if you want them set globally for debugging +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_LOGLEVEL = int(env_bool(ENVAR_DCCSI_LOGLEVEL, int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) + +_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# settings.setenv() # doing this will add the additional DYNACONF_ envars -def get_dccsi_config(DCCSIG_PATH=_DCCSIG_PATH): +# _settings.setenv() # doing this will add the additional DYNACONF_ envars +def get_dccsi_config(DCCSI_PATH=_DCCSI_PATH): """Convenience method to set and retreive settings directly from module.""" # we can go ahead and just make sure the the DCCsi env is set - # config is SO generic this ensures we are importing a specific one - _spec_dccsi_config = importlib.util.spec_from_file_location("dccsi.config", - Path(DCCSIG_PATH, + # _config is SO generic this ensures we are importing a specific one + _spec_dccsi_config = importlib.util.spec_from_file_location("dccsi._config", + Path(DCCSI_PATH, "config.py")) _dccsi_config = importlib.util.module_from_spec(_spec_dccsi_config) _spec_dccsi_config.loader.exec_module(_dccsi_config) @@ -65,9 +89,12 @@ def get_dccsi_config(DCCSIG_PATH=_DCCSIG_PATH): return _dccsi_config # ------------------------------------------------------------------------- -# set and retreive the base settings on import -config = get_dccsi_config() -settings = config.get_config_settings() +# set and retreive the base env context/_settings on import +_config = get_dccsi_config() +_settings = _config.get_config_settings() + +if _DCCSI_DEV_MODE: + _config.attach_debugger() # attempts to start debugger # done with basic setup # --- END ----------------------------------------------------------------- @@ -77,50 +104,99 @@ settings = config.get_config_settings() # ------------------------------------------------------------------------- if __name__ == '__main__': """Run this file as main""" + + # ------------------------------------------------------------------------- + _O3DE_RUNNING=None + try: + import azlmbr + _O3DE_RUNNING=True + except: + _O3DE_RUNNING=False + # ------------------------------------------------------------------------- - _G_DEBUG = False - _G_TEST_PYSIDE = False + _MODULENAME = __name__ + if _MODULENAME is '__main__': + _MODULENAME = 'O3DE.DCCsi.bootstrap' + + from azpy.constants import STR_CROSSBAR + + # module internal debugging flags + while 0: # temp internal debug flag + _DCCSI_GDEBUG = True + break + + # overide logger for standalone to be more verbose and log to file + import azpy + _LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) + # happy print + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('~ constants.py ... Running script as __main__') + _LOGGER.info(STR_CROSSBAR) + + # parse the command line args + import argparse + parser = argparse.ArgumentParser( + description='O3DE DCCsi Boostrap (Test)', + epilog="Will externally test the DCCsi boostrap") _config = get_dccsi_config() - _settings = config.get_config_settings() - - _log_level = int(_settings.DCCSI_LOGLEVEL) - if _G_DEBUG: - _log_level = int(10) # force debug level - _LOGGER = azpy.initialize_logger(_MODULE, - log_to_file=True, - default_log_level=_log_level) + _settings = _config.get_config_settings(enable_o3de_python=True, + enable_o3de_pyside2=True) + parser.add_argument('-gd', '--global-debug', + type=bool, + required=False, + help='Enables global debug flag.') + parser.add_argument('-dm', '--developer-mode', + type=bool, + required=False, + help='Enables dev mode for early auto attaching debugger.') + parser.add_argument('-tp', '--test-pyside2', + type=bool, + required=False, + help='Runs Qt/PySide2 tests and reports.') + args = parser.parse_args() - # we can now grab values from the DCCsi.config.py dynamic env settings - # the rest of this block is basic debug testing the dynamic settings at boot - _LOGGER.info(f'Running module: {_MODULE}') - _LOGGER.info(f'DCCSIG_PATH: {_settings.DCCSIG_PATH}') - _LOGGER.info(f'DCCSI_G_DEBUG: {_settings.DCCSI_GDEBUG}') - _LOGGER.info(f'DCCSI_DEV_MODE: {_settings.DCCSI_DEV_MODE}') - - _LOGGER.info(f'OS_FOLDER: {_settings.OS_FOLDER}') - _LOGGER.info(f'LY_PROJECT: {_settings.LY_PROJECT}') - _LOGGER.info(f'LY_PROJECT_PATH: {_settings.LY_PROJECT_PATH}') - _LOGGER.info(f'LY_DEV: {_settings.LY_DEV}') - _LOGGER.info(f'LY_BUILD_PATH: {_settings.LY_BUILD_PATH}') - _LOGGER.info(f'LY_BIN_PATH: {_settings.LY_BIN_PATH}') + # easy overrides + if args.global_debug: + _DCCSI_GDEBUG = True + if args.developer_mode: + _DCCSI_DEV_MODE = True + _config.attach_debugger() # attempts to start debugger - _LOGGER.info(f'DCCSIG_PATH: {_settings.DCCSIG_PATH}') - _LOGGER.info(f'DCCSI_PYTHON_LIB_PATH: {_settings.DCCSI_PYTHON_LIB_PATH}') - _LOGGER.info(f'DDCCSI_PY_BASE: {_settings.DDCCSI_PY_BASE}') + if _DCCSI_GDEBUG: + _LOGGER.info(f'DCCSI_PATH: {_settings.DCCSI_PATH}') + _LOGGER.info(f'DCCSI_G_DEBUG: {_settings.DCCSI_GDEBUG}') + _LOGGER.info(f'DCCSI_DEV_MODE: {_settings.DCCSI_DEV_MODE}') + + _LOGGER.info(f'DCCSI_OS_FOLDER: {_settings.DCCSI_OS_FOLDER}') + _LOGGER.info(f'O3DE_PROJECT: {_settings.O3DE_PROJECT}') + _LOGGER.info(f'O3DE_PROJECT_PATH: {_settings.O3DE_PROJECT_PATH}') + _LOGGER.info(f'O3DE_DEV: {_settings.O3DE_DEV}') + _LOGGER.info(f'O3DE_BUILD_PATH: {_settings.O3DE_BUILD_PATH}') + _LOGGER.info(f'O3DE_BIN_PATH: {_settings.O3DE_BIN_PATH}') + + _LOGGER.info(f'DCCSI_PATH: {_settings.DCCSI_PATH}') + _LOGGER.info(f'DCCSI_PYTHON_LIB_PATH: {_settings.DCCSI_PYTHON_LIB_PATH}') + _LOGGER.info(f'DCCSI_PY_BASE: {_settings.DCCSI_PY_BASE}') - if _G_TEST_PYSIDE: + if _DCCSI_GDEBUG or args.test_pyside2: try: import PySide2 except: # set up Qt/PySide2 access and test - _settings = _config.get_config_settings(setup_ly_pyside=True) + _settings = _config.get_config_settings(enable_o3de_pyside2=True) import PySide2 _LOGGER.info(f'PySide2: {PySide2}') - _LOGGER.info(f'LY_BIN_PATH: {_settings.LY_BIN_PATH}') + _LOGGER.info(f'O3DE_BIN_PATH: {_settings.O3DE_BIN_PATH}') _LOGGER.info(f'QT_PLUGIN_PATH: {_settings.QT_PLUGIN_PATH}') _LOGGER.info(f'QT_QPA_PLATFORM_PLUGIN_PATH: {_settings.QT_QPA_PLATFORM_PLUGIN_PATH}') _config.test_pyside2() + + if not _O3DE_RUNNING: + # return + sys.exit() # --- END ----------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/README.txt new file mode 100644 index 0000000000..9a3efe589a --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/README.txt @@ -0,0 +1,87 @@ +""" +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 folder contains the DccScriptingInterface (DCCsi) for O3DE + +Notice: The old \\SDK folder is being replaced with \\Tools +The scripts in \\SDK may be out of data (and not run) +When scripts are finished being updated and refactored into \\Tools the \\SDK will be removed + +What is the DCCsi? + +- A shared development environment for technical art oriented to working with Python across a number of DCC tools. +- Leverage the existing python ecosystem for technical art. +- Integrate a DCC app like Substance (or Substance SAT api) from the Python driven VFX and Games ecosystem. +- Extend O3DE and unlock its potential for content creators, and the Technical Artists that service them. + +Tenets: +(1) Interoperability: Design DCC-agnostic modules and DCC-bespoke modules + to work together efficiently and intuitively. + +(2) Encapsulation: Define a module in terms of its essential features and interface to other components, + to facilitate logical layered design and easy maintenance. + +(3) Extensibility: Design the tool set to be easily extensible with new functionality and new tools. + Individual pieces should have a generic communication mechanism to allow newly written tools to slot cleanly and transparently into the tool chain. + +What is provided (High Level): +- DCC-Agnostic Python Framework (as a modular Gem) related to multiple integrations for: + O3DE Editor (python scripting, utils and PySide2 tools) + DCC applications and their Python APIs/SDKs + Custom standalone tools and utils (python based) + external from cmd line + external standalone + integrated to run within O3DE Editor + +What is provided (by folder): + +\3rdParty: Allows third party libs/packages to be integrated outside of O3DE + Example: O3DE is py3, Maya 2020 (and earlier) is py27 + O3DE provides a patterns for Gems to provide a requirements.txt + See: + DccScriptingInterface\reqiurements.txt + ^ These packages will be fetched and installed into O3DE python at build time + + This means for some applications like Maya we need another way to add the same packages + See: + DccScriptingInterface\SDK\Maya\readme.txt + DccScriptingInterface\SDK\Maya\requirements.txt + DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages\* + + Packages that reside in 3rdParty are never commited to the repo (only fetched+installed) + +\Assets: All O3DE Gems can maintain an asset folder + If a Gem contains an \Asset folder, these assets are folded into the projects asset data + These assets are processed by the Asset Processor for use in the Editor and Runtime + In the DCCsi the \Assets folder primarily contains TestData + +\azpy Core (shared) API, A pure python Package and Modules + +\Code Contains the bare bones C++ scaffold to build and integrate the Gem with O3DE + Notes: portions of the DCCsi can be utilized outside of O3DE + thus this Gem doens't have to be enabled and built for some use cases + +\Editor This folder provides an entry point pattern for extending O3DE Editor with python + When a Gem is enabled ... + If the following if found, it will be executed when the Editor boots: + "Editor\Scripts\bootstrap.py" + + This can be used to initialize code access, extend the editor (PySide2), etc. + +\Tools This is where the following is maintained: + +\Tools\DCC Integration for DCC tools: + configuration of tool (managed env, etc.) + bootstrapping, such as providing the tool access to azpy api code + extensibility, such as adding new functionality or tool to the app + +\Tools\DCC\Maya An example of adding a integration for Autodesk Maya + +\Tools\Env\Windows This provides a .bat file managed env to configure and bootsrap windows apps +\Tools\Launchers\windows Provides .bat files based tool launchers for windows (accesses env) + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Atom/Scripts/Python/minspect.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Atom/Scripts/Python/minspect.py deleted file mode 100644 index 760807042c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Atom/Scripts/Python/minspect.py +++ /dev/null @@ -1,132 +0,0 @@ -""" -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 pymel.core as pmc -import sys -import types - - -def syspath(): - print 'sys.path:' - for p in sys.path: - print ' ' + p - - -def info(obj): - """Prints information about the object.""" - - lines = ['Info for %s' % obj.name(), - 'Attributes:'] - # Get the name of all attributes - for a in obj.listAttr(): - lines.append(' ' + a.name()) - lines.append('MEL type: %s' % obj.type()) - lines.append('MRO:') - lines.extend([' ' + t.__name__ for t in type(obj).__mro__]) - result = '\n'.join(lines) - print result - - -def _is_pymel(obj): - try: # (1) - module = obj.__module__ # (2) - except AttributeError: # (3) - try: - module = obj.__name__ # (4) - except AttributeError: - return None # (5) - return module.startswith('pymel') # (6) - - -def _py_to_helpstr(obj): - if isinstance(obj, basestring): - return 'search.html?q=%s' % (obj.replace(' ', '+')) - if not _is_pymel(obj): - return None - if isinstance(obj, types.ModuleType): - return ('generated/%(module)s.html#module-%(module)s' % - dict(module=obj.__name__)) - if isinstance(obj, types.MethodType): - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s.%(methname)s' % dict( - module=obj.__module__, - typename=obj.im_class.__name__, - methname=obj.__name__)) - if isinstance(obj, types.FunctionType): - return ('generated/functions/%(module)s/' - '%(module)s.%(funcname)s.html' - '#%(module)s.%(funcname)s' % dict( - module=obj.__module__, - funcname=obj.__name__)) - if not isinstance(obj, type): - obj = type(obj) - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s' % dict( - module=obj.__module__, - typename=obj.__name__)) - - -def test_py_to_helpstr(): - def dotest(obj, ideal): - result = _py_to_helpstr(obj) - assert result == ideal, '%s != %s' % (result, ideal) - dotest('maya rocks', 'search.html?q=maya+rocks') - dotest(pmc.nodetypes, - 'generated/pymel.core.nodetypes.html' - '#module-pymel.core.nodetypes') - dotest(pmc.nodetypes.Joint, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint(), - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint().getTranslation, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint.getTranslation') - dotest(pmc.joint, - 'generated/functions/pymel.core.animation/' - 'pymel.core.animation.joint.html' - '#pymel.core.animation.joint') - dotest(object(), None) - dotest(10, None) - dotest([], None) - dotest(sys, None) - - -def test_py_to_helpstrFAIL(): - assert 1 == 2, '1 != 2' - - -import webbrowser # (1) -HELP_ROOT_URL = ('http://help.autodesk.com/cloudhelp/2018/ENU/Maya-Tech-Docs/PyMel/')# (2) - - -def pmhelp(obj): # (3) - """Gives help for a pymel or python object. - - If obj is not a PyMEL object, use Python's built-in - `help` function. - If obj is a string, open a web browser to a search in the - PyMEL help for the string. - Otherwise, open a web browser to the page for the object. - """ - tail = _py_to_helpstr(obj) - if tail is None: - help(obj) # (4) - else: - webbrowser.open(HELP_ROOT_URL + tail) # (5) - - -if __name__ == '__main__': - test_py_to_helpstr() - print 'Tests ran successfully.' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/launcher.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/launcher.bat deleted file mode 100644 index fb07178344..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/launcher.bat +++ /dev/null @@ -1,85 +0,0 @@ -@echo off - -REM -REM Copyright (c) Contributors to the Open 3D Engine Project. -REM For complete copyright and license terms please see the LICENSE at the root of this distribution. -REM -REM SPDX-License-Identifier: Apache-2.0 OR MIT -REM -REM - -:: Set up and run LY Python CMD prompt -:: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the dev environment - -:: Set up window -TITLE Lumberyard DCC Scripting Interface Cmd -:: Use obvious color to prevent confusion (Grey with Yellow Text) -COLOR 8E - -%~d0 -cd %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -:: This maps up to the \Dev folder -IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..\..\..\..\..\..) - -:: Change to root Lumberyard dev dir -:: Don't use the LY_DEV so we can test that ENVAR!!! -CD /d %DEV_REL_PATH% -set Rel_Dev=%CD% -echo Rel_Dev = %Rel_Dev% -:: Restore original directory -popd - -set DCCSI_PYTHON_INSTALL=%Rel_Dev%\Tools\Python\3.7.5\windows - -:: add to the PATH -SET PATH=%DCCSI_PYTHON_INSTALL%;%PATH% - -:: dcc scripting interface gem path -set DCCSIG_PATH=%Rel_Dev%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface -echo DCCSIG_PATH = %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -:: Constant Vars (Global) -:: global debug (propogates) -IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=false) -echo DCCSI_GDEBUG = %DCCSI_GDEBUG% -:: initiates debugger connection -IF "%DCCSI_DEV_MODE%"=="" (set DCCSI_DEV_MODE=false) -echo DCCSI_DEV_MODE = %DCCSI_DEV_MODE% -:: sets debugger, options: WING, PYCHARM -IF "%DCCSI_GDEBUGGER%"=="" (set DCCSI_GDEBUGGER=WING) -echo DCCSI_GDEBUGGER = %DCCSI_GDEBUGGER% - -echo. -echo _____________________________________________________________________ -echo. -echo ~ LY DCCsi, DCC Material Converter -echo _____________________________________________________________________ -echo. - -:: Change to root dir -CD /D %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -set PYTHONPATH=%DCCSIG_PATH%;%PYTHONPATH% - -CALL %DCCSI_PYTHON_INSTALL%\python.exe "%DCCSIG_PATH%\SDK\Maya\Scripts\Python\kitbash_converter\standalone.py" - - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE - -exit /b 0 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py index e554b1ca68..d0e9759208 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/kitbash_converter/main.py @@ -51,8 +51,8 @@ module_name = 'kitbash_converter.main' log_file_path = os.path.join(settings.DCCSI_LOG_PATH, f'{module_name}.log') _log_level = int(20) -_G_DEBUG = True -if _G_DEBUG: +_DCCSI_GDEBUG = True +if _DCCSI_GDEBUG: _log_level = int(10) from azpy.constants import FRMT_LOG_LONG diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Cmd.bat deleted file mode 100644 index ce118d7710..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Cmd.bat +++ /dev/null @@ -1,45 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Set up and run LY Python CMD prompt -:: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the dev environment - -:: Set up window -TITLE Lumberyard DCC Scripting Interface Cmd -:: Use obvious color to prevent confusion (Grey with Yellow Text) -COLOR 8E - -%~d0 -cd %~dp0 -PUSHD %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -CALL %~dp0\Project_Env.bat - -echo. -echo _____________________________________________________________________ -echo. -echo ~ LY DCC Scripting Interface CMD ... -echo _____________________________________________________________________ -echo. - -:: Create command prompt with environment -CALL %windir%\system32\cmd.exe - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Maya_2020.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Maya_2020.bat deleted file mode 100644 index 32e910790e..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_Maya_2020.bat +++ /dev/null @@ -1,68 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Launches maya with a bunch of local hooks for Lumberyard -:: ToDo: move all of this to a .json data driven boostrapping system - -%~d0 -cd %~dp0 -PUSHD %~dp0 - -echo ________________________________ -echo ~ calling PROJ_Env.bat - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -:: PY version Major -set DCCSI_PY_VERSION_MAJOR=2 -echo DCCSI_PY_VERSION_MAJOR = %DCCSI_PY_VERSION_MAJOR% - -:: PY version Major -set DCCSI_PY_VERSION_MINOR=7 -echo DCCSI_PY_VERSION_MINOR = %DCCSI_PY_VERSION_MINOR% - -:: Maya Version -set MAYA_VERSION=2020 -echo MAYA_VERSION = %MAYA_VERSION% - -:: if a local customEnv.bat exists, run it -IF EXIST "%~dp0Project_Env.bat" CALL %~dp0Project_Env.bat - -echo ________________________________ -echo Launching Maya %MAYA_VERSION% for Lumberyard... - -:::: Set Maya native project acess to this project -::set MAYA_PROJECT=%LY_PROJECT% -::echo MAYA_PROJECT = %MAYA_PROJECT% - -:: DX11 Viewport -Set MAYA_VP2_DEVICE_OVERRIDE = VirtualDeviceDx11 - -:: Default to the right version of Maya if we can detect it... and launch -IF EXIST "%MAYA_LOCATION%\bin\Maya.exe" ( - start "" "%MAYA_LOCATION%\bin\Maya.exe" %* -) ELSE ( - Where maya.exe 2> NUL - IF ERRORLEVEL 1 ( - echo Maya.exe could not be found - pause - ) ELSE ( - start "" Maya.exe %* - ) -) - -:: Return to starting directory -POPD - -:END_OF_FILE - -exit /b 0 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_WingIDE-7-1.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_WingIDE-7-1.bat deleted file mode 100644 index 4d82bd3e16..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Launch_WingIDE-7-1.bat +++ /dev/null @@ -1,81 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Launches Wing IDE and the DccScriptingInterface Project Files - -echo. -echo _____________________________________________________________________ -echo. -echo ~ Setting up LY DCCsi WingIDE Dev Env... -echo _____________________________________________________________________ -echo. - -:: Store current dir -%~d0 -cd %~dp0 -PUSHD %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -SET ABS_PATH=%~dp0 -echo Current Dir, %ABS_PATH% - -:: WingIDE version Major -SET WING_VERSION_MAJOR=7 -echo WING_VERSION_MAJOR = %WING_VERSION_MAJOR% - -:: WingIDE version Major -SET WING_VERSION_MINOR=1 -echo WING_VERSION_MINOR = %WING_VERSION_MINOR% - -:: note the changed path from IDE to Pro -set WINGHOME=%PROGRAMFILES(X86)%\Wing Pro %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% -echo WINGHOME = %WINGHOME% - -CALL %~dp0\Project_Env.bat - -echo. -echo _____________________________________________________________________ -echo. -echo ~ WingIDE Version %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% -echo _____________________________________________________________________ -echo. - -SET WING_PROJ=%DCCSIG_PATH%\Solutions\.wing\DCCsi_%WING_VERSION_MAJOR%x.wpr -echo WING_PROJ = %WING_PROJ% - -echo. -echo _____________________________________________________________________ -echo. -echo ~ Launching %LY_PROJECT% project in WingIDE %WING_VERSION_MAJOR%.%WING_VERSION_MINOR% ... -echo _____________________________________________________________________ -echo. - - -IF EXIST "%WINGHOME%\bin\wing.exe" ( - start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%" -) ELSE ( - Where wing.exe 2> NUL - IF ERRORLEVEL 1 ( - echo wing.exe could not be found - pause - ) ELSE ( - start "" wing.exe "%WING_PROJ%" - ) -) - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Project_Env.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Project_Env.bat deleted file mode 100644 index f958d98616..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/Project_Env.bat +++ /dev/null @@ -1,72 +0,0 @@ -:: coding:utf-8 -:: !/usr/bin/python -:: -:: 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 -:: -:: - -@echo off -:: Sets up environment for Lumberyard DCC tools and code access - -:: Store current dir -%~d0 -cd %~dp0 -PUSHD %~dp0 - -for %%a in (.) do set LY_PROJECT=%%~na - -echo. -echo _____________________________________________________________________ -echo. -echo ~ Setting up LY DSI PROJECT Environment ... -echo _____________________________________________________________________ -echo. - -echo LY_PROJECT = %LY_PROJECT% - -:: Put you project env vars and overrides here - -:: chanhe the relative path up to dev -set DEV_REL_PATH=../../.. -set ABS_PATH=%~dp0 - -:: Override the default maya version -set MAYA_VERSION=2020 -echo MAYA_VERSION = %MAYA_VERSION% - -set LY_PROJECT_PATH=%ABS_PATH% -echo LY_PROJECT_PATH = %LY_PROJECT_PATH% - -:: Change to root Lumberyard dev dir -CD /d %LY_PROJECT_PATH%\%DEV_REL_PATH% -set LY_DEV=%CD% -echo LY_DEV = %LY_DEV% - -CALL %LY_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\Launchers\Windows\Env.bat - -rem :: Constant Vars (Global) -rem SET LYPY_GDEBUG=0 -rem echo LYPY_GDEBUG = %LYPY_GDEBUG% -rem SET LYPY_DEV_MODE=0 -rem echo LYPY_DEV_MODE = %LYPY_DEV_MODE% -rem SET LYPY_DEBUGGER=WING -rem echo LYPY_DEBUGGER = %LYPY_DEBUGGER% - -:: Restore original directory -popd - -:: Change to root dir -CD /D %ABS_PATH% - -:: if the user has set up a custom env call it -IF EXIST "%~dp0User_Env.bat" CALL %~dp0User_Env.bat - -GOTO END_OF_FILE - -:: Return to starting directory -POPD - -:END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py index 3b985199bb..09caef836f 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/legacy_asset_converter/main.py @@ -80,8 +80,8 @@ module_name = 'legacy_asset_converter.main' log_file_path = os.path.join(settings.DCCSI_LOG_PATH, f'{module_name}.log') _log_level = int(20) -_G_DEBUG = True -if _G_DEBUG: +_DCCSI_GDEBUG = True +if _DCCSI_GDEBUG: _log_level = int(10) from azpy.constants import FRMT_LOG_LONG diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/maya_dcc_materials/minspect.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/maya_dcc_materials/minspect.py deleted file mode 100644 index 668382480c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/maya_dcc_materials/minspect.py +++ /dev/null @@ -1,135 +0,0 @@ -# -*- coding: utf-8 -*- -# !/usr/bin/python -# -# 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 pymel.core as pmc -import sys -import types - - -def syspath(): - print 'sys.path:' - for p in sys.path: - print ' ' + p - - -def info(obj): - """Prints information about the object.""" - - lines = ['Info for %s' % obj.name(), - 'Attributes:'] - # Get the name of all attributes - for a in obj.listAttr(): - lines.append(' ' + a.name()) - lines.append('MEL type: %s' % obj.type()) - lines.append('MRO:') - lines.extend([' ' + t.__name__ for t in type(obj).__mro__]) - result = '\n'.join(lines) - print result - - -def _is_pymel(obj): - try: # (1) - module = obj.__module__ # (2) - except AttributeError: # (3) - try: - module = obj.__name__ # (4) - except AttributeError: - return None # (5) - return module.startswith('pymel') # (6) - - -def _py_to_helpstr(obj): - if isinstance(obj, basestring): - return 'search.html?q=%s' % (obj.replace(' ', '+')) - if not _is_pymel(obj): - return None - if isinstance(obj, types.ModuleType): - return ('generated/%(module)s.html#module-%(module)s' % - dict(module=obj.__name__)) - if isinstance(obj, types.MethodType): - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s.%(methname)s' % dict( - module=obj.__module__, - typename=obj.im_class.__name__, - methname=obj.__name__)) - if isinstance(obj, types.FunctionType): - return ('generated/functions/%(module)s/' - '%(module)s.%(funcname)s.html' - '#%(module)s.%(funcname)s' % dict( - module=obj.__module__, - funcname=obj.__name__)) - if not isinstance(obj, type): - obj = type(obj) - return ('generated/classes/%(module)s/' - '%(module)s.%(typename)s.html' - '#%(module)s.%(typename)s' % dict( - module=obj.__module__, - typename=obj.__name__)) - - -def test_py_to_helpstr(): - def dotest(obj, ideal): - result = _py_to_helpstr(obj) - assert result == ideal, '%s != %s' % (result, ideal) - dotest('maya rocks', 'search.html?q=maya+rocks') - dotest(pmc.nodetypes, - 'generated/pymel.core.nodetypes.html' - '#module-pymel.core.nodetypes') - dotest(pmc.nodetypes.Joint, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint(), - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint') - dotest(pmc.nodetypes.Joint().getTranslation, - 'generated/classes/pymel.core.nodetypes/' - 'pymel.core.nodetypes.Joint.html' - '#pymel.core.nodetypes.Joint.getTranslation') - dotest(pmc.joint, - 'generated/functions/pymel.core.animation/' - 'pymel.core.animation.joint.html' - '#pymel.core.animation.joint') - dotest(object(), None) - dotest(10, None) - dotest([], None) - dotest(sys, None) - - -def test_py_to_helpstrFAIL(): - assert 1 == 2, '1 != 2' - - -import webbrowser # (1) -HELP_ROOT_URL = ('http://help.autodesk.com/cloudhelp/2018/ENU/Maya-Tech-Docs/PyMel/')# (2) - - -def pmhelp(obj): # (3) - """Gives help for a pymel or python object. - - If obj is not a PyMEL object, use Python's built-in - `help` function. - If obj is a string, open a web browser to a search in the - PyMEL help for the string. - Otherwise, open a web browser to the page for the object. - """ - tail = _py_to_helpstr(obj) - if tail is None: - help(obj) # (4) - else: - webbrowser.open(HELP_ROOT_URL + tail) # (5) - - -if __name__ == '__main__': - test_py_to_helpstr() - print 'Tests ran successfully.' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py index 10cdac813a..4c24a61b59 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter.py @@ -23,7 +23,7 @@ def returnStubDir(stub): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: print('~ Debug Message: I was not able to find the ' 'path to that file (stub) in a walk-up from currnet path') break diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py index 3ea7b1e5e7..3a7ec49e74 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/Python/stingraypbs_converter/stingrayPBS_converter_maya.py @@ -24,7 +24,7 @@ def returnStubDir(stub, start_path): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: print('~ Debug Message: I was not able to find the ' 'path to that file (stub) in a walk-up from currnet path') break diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py index 4dda18c403..8c3b0f6a64 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_callbacks.py @@ -42,7 +42,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, True) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, True) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, True) _MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_callbacks' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py index d6db5e42f4..639b03c297 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_defaults.py @@ -37,7 +37,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_defaults' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py index 4e85a448c1..e417324d0b 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/set_menu.py @@ -37,7 +37,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_menu' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py index aee5563c3d..04eb027142 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Maya/Scripts/userSetup.py @@ -56,7 +56,7 @@ import maya.mel as mel # ------------------------------------------------------------------------- # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) #_DCCSI_DEV_MODE = True # force true for debugger testing @@ -69,7 +69,7 @@ _MODULENAME = str('{0}.{1}'.format(_APP_TAG, _TOOL_TAG)) _LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) _LOGGER.info('Initializing: {0}.'.format({_MODULENAME})) -_LOGGER.info('DCCSI_GDEBUG: {0}.'.format({_G_DEBUG})) +_LOGGER.info('DCCSI_GDEBUG: {0}.'.format({_DCCSI_GDEBUG})) _LOGGER.info('DCCSI_DEV_MODE: {0}.'.format({_DCCSI_DEV_MODE})) # flag to turn off setting up callbacks, until they are fully implemented @@ -175,17 +175,17 @@ try: except Exception as e: _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_DCCSI_SDK_PATH])) -_LY_PROJECT_PATH = None +_O3DE_PROJECT_PATH = None try: - _LY_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH] + _O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] except Exception as e: - _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH])) + _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH])) # check some env var tags (fail if no, likely means no proper code access) -_LY_DEV = _BASE_ENVVAR_DICT[ENVAR_LY_DEV] -_LY_DCCSIG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] -_LY_DCCSI_LOG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] -_LY_AZPY_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] +_O3DE_DEV = _BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] +_O3DE_DCCSIG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] +_O3DE_DCCSI_LOG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] +_O3DE_AZPY_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] # ------------------------------------------------------------------------- @@ -270,18 +270,18 @@ def post_startup(): install_fix_paths() # set the project workspace - #_LY_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH] - _project_workspace = os.path.join(_LY_PROJECT_PATH, TAG_MAYA_WORKSPACE) + #_O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] + _project_workspace = os.path.join(_O3DE_PROJECT_PATH, TAG_MAYA_WORKSPACE) if os.path.isfile(_project_workspace): try: # load workspace - maya.cmds.workspace(_LY_PROJECT_PATH, openWorkspace=True) + maya.cmds.workspace(_O3DE_PROJECT_PATH, openWorkspace=True) _LOGGER.info('Loaded workspace file: {0}'.format(_project_workspace)) - maya.cmds.workspace(_LY_PROJECT_PATH, update=True) + maya.cmds.workspace(_O3DE_PROJECT_PATH, update=True) except Exception as e: _LOGGER.error(e) else: - _LOGGER.warning('Workspace file not found: {1}'.format(_LY_PROJECT_PATH)) + _LOGGER.warning('Workspace file not found: {1}'.format(_O3DE_PROJECT_PATH)) # Set up Lumberyard, maya default setting from set_defaults import set_defaults @@ -292,7 +292,7 @@ def post_startup(): _LOGGER.info('Add UI dependent tools') # wrap in a try, because we haven't implmented it yet try: - mel.eval(str(r'source "{}"'.format(TAG_LY_DCC_MAYA_MEL))) + mel.eval(str(r'source "{}"'.format(TAG_O3DE_DCC_MAYA_MEL))) except Exception as e: _LOGGER.error(e) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/blender_materials.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/blender_materials.py deleted file mode 100755 index 6d7bd10f89..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/blender_materials.py +++ /dev/null @@ -1,85 +0,0 @@ -# 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 bpy -import collections -import json - - -def get_shader_information(): - """ - Queries all materials and corresponding material attributes and file textures in the Blender scene. - - :return: - """ - # TODO - link file texture location to PBR material plugs- finding it difficult to track down how this is achieved - # in the Blender Python API documentation and/or in forums - - materials_count = 1 - shader_types = get_blender_shader_types() - materials_dictionary = {} - for target_mesh in [o for o in bpy.data.objects if type(o.data) is bpy.types.Mesh]: - material_information = collections.OrderedDict(DccApplication='Blender', AppliedMesh=target_mesh, - SceneName=bpy.data.filepath, MaterialAttributes={}, - FileConnections={}) - for target_material in target_mesh.data.materials: - material_information['MaterialName'] = target_material.name - shader_attributes = {} - shader_file_connections = {} - - for node in target_material.node_tree.nodes: - socket = node.inputs[0] - print('NODE: {}'.format(node)) - print('Socket: {}'.format(socket)) - - for material_input in node.inputs: - attribute_name = material_input.name - try: - attribute_value = material_input.default_value - print('Name: [{}] [{}] ValueType ::::::> {}'.format(attribute_name, attribute_value, - type(attribute_value))) - material_information['MaterialAttributes'].update({attribute_name: str(attribute_value)}) - except Exception as e: - pass - print('\n') - if node.type == 'TEX_IMAGE': - material_information['FileConnections'].update({str(node): str(node.image.filepath)}) - if node.name in shader_types.keys(): - material_information['MaterialType'] = shader_types[node.name] - - -# material_information['MaterialAttributes'] = shader_attributes - materials_dictionary['Material_{}'.format(materials_count)] = material_information - materials_count += 1 - print('_________________________________________________________________\n') - - return materials_dictionary - - -def get_blender_shader_types(): - """ - This returns all the material types present in the Blender scene - :return: - """ - shader_types = {} - ddir = lambda data, filter_str: [i for i in dir(data) if i.startswith(filter_str)] - get_nodes = lambda cat: [i for i in getattr(bpy.types, cat).category.items(None)] - cycles_categories = ddir(bpy.types, "NODE_MT_category_SH_NEW") - for cat in cycles_categories: - if cat == 'NODE_MT_category_SH_NEW_SHADER': - for node in get_nodes(cat): - shader_types[node.label] = node.nodetype - return shader_types - - -materials_dictionary = get_shader_information() -#print('Materials Dictionary:') -#print(materials_dictionary) -#parsed = json.loads(str(materials_dictionary)) -#print(json.dumps(parsed, indent=4, sort_keys=True)) - - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/cli_control.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/cli_control.py deleted file mode 100755 index 1d91275545..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/cli_control.py +++ /dev/null @@ -1,52 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- - -import click -import os -import main as app_main - - -@click.version_option('1.0.0') -@click.option('--output', default='PBR', help='Lumberyard material type. Current options: [pbr_basic]') -@click.argument('operands', type=click.STRING, nargs=-1) -@click.command(context_settings=dict(ignore_unknown_options=True)) -def main(output, operands): - target_files = [] - for index, operand in enumerate(operands): - entry_path = os.path.abspath(str(operand)) - if os.path.isdir(entry_path): - for directory_path, directory_names, file_names in os.walk(entry_path): - for file_name in file_names: - if is_valid_file(file_name): - target_files.append(os.path.join(entry_path, file_name)) - else: - if is_valid_file(operand): - target_files.append(operand) - - if len(target_files): - app_main.launch_material_converter('standalone', output, target_files) - - -def is_valid_file(file_name): - """ - Allows only supported DCC application files by extensions - :param file_name: The name of the file. - :return: - """ - target_extensions = 'ma mb fbx blend max'.split(' ') - if file_name.split('.')[-1] in target_extensions: - return True - return False - - -if __name__ == '__main__': - main() - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/dcc_material_mapping.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/dcc_material_mapping.py deleted file mode 100755 index 6369cb5658..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/dcc_material_mapping.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- - -import logging - -logging.basicConfig(level=logging.DEBUG) - -def get_maya_material_mapping(name, material_type, file_connections): - """ - Helps map found material DCC attribute values/file connections with Lumberyard materials. - - :param name: Material name from within Maya - :param material_type: Maya Material type to match values to (i.e. Stingray PBS, aiStandardSurface(Arnold) - :param file_connections: List of all connected texture files from Maya - :return: Key value pairs for attributes/file textures assigned as Lumberyard material values - """ - material_properties = {} - if material_type == 'StingrayPBS': - logging.debug('Mapping StingrayPBS') - maps = 'color, metallic, roughness, normal, emissive, ao, opacity'.split(', ') - naming_exceptions = {'color': 'baseColor', 'ao': 'ambientOcclusion'} - for m in maps: - texture_attribute = 'TEX_{}_map'.format(m) - for tex in file_connections.keys(): - if tex.find(texture_attribute) != -1: - key = m if m not in naming_exceptions else naming_exceptions.get(m) - logging.debug('Key, Value: {} {}.{}'.format(key, name, texture_attribute)) - material_properties[key] = {'useTexture': 'true', - 'textureMap': file_connections.get( - '{}.{}'.format(name, texture_attribute))} - elif material_type == 'aiStandardSurface': - logging.debug('Mapping AiStandardSurface') - # TODO- Occlusion is based on a more difficult setup- there is no standard channel. Set this up as time permits - maps = 'baseColor, metalness, specularRoughness, normal, emissionColor, opacity'.split(', ') - naming_exceptions = {'metalness': 'metallic', 'specularRoughness': 'roughness', 'emissionColor': 'emissive'} - for m in maps: - key = m if m not in naming_exceptions.keys() else naming_exceptions.get(m) - texture_attribute = m - for tex in file_connections.keys(): - if tex.find(texture_attribute) != -1: - logging.debug('Key, Value: {} {}.{}'.format(key, name, texture_attribute)) - material_properties[key] = {'useTexture': 'true', - 'textureMap': file_connections.get( - '{}.{}'.format(name, texture_attribute))} - else: - pass - - return material_properties - - -def get_blender_material_mapping(name, material_type, file_connections): - pass - - -def get_max_material_mapping(name, material_type, file_connections): - pass - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/drag_and_drop.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/drag_and_drop.py deleted file mode 100755 index d02104b12c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/drag_and_drop.py +++ /dev/null @@ -1,65 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- - -from PySide2 import QtWidgets, QtCore -from PySide2.QtCore import Signal - -class DragAndDrop(QtWidgets.QWidget): - drop_update = QtCore.Signal(list) - drop_over = QtCore.Signal(bool) - - def __init__(self, frame_color=None, highlight=None, parent=None): - super(DragAndDrop, self).__init__(parent) - - self.urls = [] - self.frame_color = frame_color - self.frame_highlight = highlight - self.setContentsMargins(0, 0, 0, 0) - self.setAcceptDrops(True) - - self.drag_and_drop_frame = QtWidgets.QFrame(self) - self.drag_and_drop_frame.setGeometry(0, 0, 5000, 5000) - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color)) - - def dragEnterEvent(self, e): - if e.mimeData().hasUrls: - e.accept() - self.drop_over.emit(True) - if self.frame_highlight: - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_highlight)) - else: - e.ignore() - - def dragLeaveEvent(self, e): - self.drop_over.emit(False) - - if self.frame_highlight: - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color)) - - def dragMoveEvent(self, e): - if e.mimeData().hasUrls: - e.accept() - else: - e.ignore() - - def dropEvent(self, e): - if e.mimeData().hasUrls: - e.setDropAction(QtCore.Qt.CopyAction) - e.accept() - - for url in e.mimeData().urls(): - file_name = str(url.toLocalFile()) - self.urls.append(file_name) - self.drop_update.emit(self.urls) - if self.frame_highlight: - self.drag_and_drop_frame.setStyleSheet('background-color:rgb({});'.format(self.frame_color)) - else: - e.ignore() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/launcher.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/launcher.bat deleted file mode 100644 index d609c26897..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/launcher.bat +++ /dev/null @@ -1,85 +0,0 @@ -@echo off - -REM -REM Copyright (c) Contributors to the Open 3D Engine Project. -REM For complete copyright and license terms please see the LICENSE at the root of this distribution. -REM -REM SPDX-License-Identifier: Apache-2.0 OR MIT -REM -REM - -:: Set up and run LY Python CMD prompt -:: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the dev environment - -:: Set up window -TITLE Lumberyard DCC Scripting Interface Cmd -:: Use obvious color to prevent confusion (Grey with Yellow Text) -COLOR 8E - -%~d0 -cd %~dp0 - -:: Keep changes local -SETLOCAL enableDelayedExpansion - -:: This maps up to the \Dev folder -IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..\..\..\..) - -:: Change to root Lumberyard dev dir -:: Don't use the LY_DEV so we can test that ENVAR!!! -CD /d %DEV_REL_PATH% -set Rel_Dev=%CD% -echo Rel_Dev = %Rel_Dev% -:: Restore original directory -popd - -set DCCSI_PYTHON_INSTALL=%Rel_Dev%\Tools\Python\3.7.5\windows - -:: add to the PATH -SET PATH=%DCCSI_PYTHON_INSTALL%;%PATH% - -:: dcc scripting interface gem path -set DCCSIG_PATH=%Rel_Dev%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface -echo DCCSIG_PATH = %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -:: Constant Vars (Global) -:: global debug (propogates) -IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=false) -echo DCCSI_GDEBUG = %DCCSI_GDEBUG% -:: initiates debugger connection -IF "%DCCSI_DEV_MODE%"=="" (set DCCSI_DEV_MODE=false) -echo DCCSI_DEV_MODE = %DCCSI_DEV_MODE% -:: sets debugger, options: WING, PYCHARM -IF "%DCCSI_GDEBUGGER%"=="" (set DCCSI_GDEBUGGER=WING) -echo DCCSI_GDEBUGGER = %DCCSI_GDEBUGGER% - -echo. -echo _____________________________________________________________________ -echo. -echo ~ LY DCCsi, DCC Material Converter -echo _____________________________________________________________________ -echo. - -:: Change to root dir -CD /D %DCCSIG_PATH% - -:: add to the PATH -SET PATH=%DCCSIG_PATH%;%PATH% - -set PYTHONPATH=%DCCSIG_PATH%;%PYTHONPATH% - -CALL %DCCSI_PYTHON_INSTALL%\python.exe "%DCCSIG_PATH%\SDK\PythonTools\DCC_Material_Converter\standalone.py" - - -ENDLOCAL - -:: Return to starting directory -POPD - -:END_OF_FILE - -exit /b 0 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/main.py deleted file mode 100755 index f9359d7d82..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/main.py +++ /dev/null @@ -1,1026 +0,0 @@ -# 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 -# -# - -""" -Usage -===== -Put usage instructions here. - -Output -====== -Put output information here. - -Notes: -In order to run this, you'll need to verify that the "mayapy_path" class attribute corresponds to the location on -your machine. Currently I've just included mapping instructions for Maya StingrayPBS materials, although most of -the needed elements are in place to carry out additional materials inside of Maya pretty quickly moving forward. -I've marked areas that still need refinement (or to be added altogether) with TODO comments - -TODO- Docstrings need work... wanted to get descriptions in but they need to be set for Sphinx -TODO- Add 3ds Max interoperability -Links: -https://blender.stackexchange.com/questions/100497/use-blenders-bpy-in-projects-outside-blender -https://knowledge.autodesk.com/support/3ds-max/learn-explore/caas/CloudHelp/cloudhelp/2019/ENU/3DSMax-Batch/files/ -GUID-0968FF0A-5ADD-454D-B8F6-1983E76A4AF9-htm.html - -TODO- Look at dynaconf and wire in a solid means for configuration settings -TODO- This hasn't been "designed"- might be worth it to consider the visual design to ensure the most effective and - attractive UI -TODO- Allow revisions to Model - -Reading FBX file information (might come in handy later) --- Materials information can be extracted from ASCII fbx pretty easily, binary is possible but more difficult --- FBX files could be exported as ASCII files and I could use regex there to extract material information --- I couldn't get pyfbx_i42 to work, but purportedly it can extract information from binary files. You may just have -to use the specified python versions -""" -# built-ins -import collections -import logging -import subprocess -import json -import sys -import os -import re - -# should give access to Lumberyard Qt dlls and PySide2 -from PySide2 import QtWidgets, QtCore, QtGui -from PySide2.QtCore import Slot -from PySide2.QtWidgets import QApplication -import shiboken2 -from shiboken2 import wrapInstance - -# local imports -from model import MaterialsModel -from drag_and_drop import DragAndDrop -import dcc_material_mapping as dcc_map - -# global space -main_window_pointer = None -main_app_window = None - - -class MaterialsToLumberyard(QtWidgets.QWidget): - def __init__(self, output_material_type='PBR', cli_values=None, parent=None): - super(MaterialsToLumberyard, self).__init__(parent) - - self.app = QtWidgets.QApplication.instance() - self.setWindowFlags(QtCore.Qt.Window) - self.setGeometry(50, 50, 800, 520) - self.setObjectName('MaterialsToLumberyard') - self.setWindowTitle(' ') - self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowMinMaxButtonsHint) - self.isTopLevel() - - self.cli_enabled = cli_values - self.output_material_type = output_material_type - self.desktop_location = os.path.join(os.path.expanduser('~'), 'Desktop') - self.directory_path = os.path.dirname(os.path.abspath(__file__)) - self.mayapy_path = os.path.abspath("C:/Program Files/Autodesk/Maya2020/bin/mayapy.exe") - self.blender_path = self.get_blender_path() - self.bold_font_large = QtGui.QFont('Helvetica', 7, QtGui.QFont.Bold) - self.medium_font = QtGui.QFont('Helvetica', 7, QtGui.QFont.Normal) - self.blessed_file_extensions = 'ma mb fbx max blend'.split(' ') - - self.dcc_materials_dictionary = {} - self.lumberyard_materials_dictionary = {} - self.lumberyard_material_nodes = [] - self.target_file_list = [] - self.current_scene = None - self.model = None - self.total_materials = 0 - - self.main_container = QtWidgets.QVBoxLayout(self) - self.main_container.setContentsMargins(0, 0, 0, 0) - self.main_container.setAlignment(QtCore.Qt.AlignTop) - self.setLayout(self.main_container) - self.content_layout = QtWidgets.QVBoxLayout() - self.content_layout.setAlignment(QtCore.Qt.AlignTop) - self.content_layout.setContentsMargins(10, 3, 10, 5) - self.main_container.addLayout(self.content_layout) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Header Bar - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.header_bar_layout = QtWidgets.QHBoxLayout() - self.lumberyard_logo_layout = QtWidgets.QHBoxLayout() - self.lumberyard_logo_layout.setAlignment(QtCore.Qt.AlignLeft) - logo_path = os.path.join(self.directory_path, 'resources', 'lumberyard_logo.png') - logo_pixmap = QtGui.QPixmap(logo_path) - self.lumberyard_logo = QtWidgets.QLabel() - self.lumberyard_logo.setPixmap(logo_pixmap) - self.lumberyard_logo_layout.addWidget(self.lumberyard_logo) - self.header_bar_layout.addLayout(self.lumberyard_logo_layout) - - self.switch_combobox_layout = QtWidgets.QHBoxLayout() - self.switch_combobox_layout.setAlignment(QtCore.Qt.AlignRight) - self.switch_layout_combobox = QtWidgets.QComboBox() - self.set_combobox_items_accessibility() - self.switch_layout_combobox.setFixedSize(250, 30) - self.combobox_items = ['Add Source Files', 'Source File List', 'DCC Material Values', 'Export Materials'] - self.switch_layout_combobox.setStyleSheet('QComboBox {padding-left:6px;}') - self.switch_layout_combobox.addItems(self.combobox_items) - self.switch_combobox_layout.addWidget(self.switch_layout_combobox) - self.header_bar_layout.addLayout(self.switch_combobox_layout) - - self.content_layout.addSpacing(5) - self.content_layout.addLayout(self.header_bar_layout) - - # ++++++++++++++++++++++++++++++++++++++++++++++++# - # File Source Table / Attributes (Stacked Layout) # - # ++++++++++++++++++++++++++++++++++++++++++++++++# - - self.content_stacked_layout = QtWidgets.QStackedLayout() - self.content_layout.addLayout(self.content_stacked_layout) - self.switch_layout_combobox.currentIndexChanged.connect(self.layout_combobox_changed) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Add Source Files - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - frame_color_value = '75,75,75' - highlight_color_value = '20,106,30' - self.drag_and_drop_widget = DragAndDrop(frame_color_value, highlight_color_value) - self.drag_and_drop_widget.drop_update.connect(self.drag_and_drop_file_update) - self.drag_and_drop_widget.drop_over.connect(self.drag_and_drop_over) - self.drag_and_drop_layout = QtWidgets.QVBoxLayout() - self.drag_and_drop_layout.setContentsMargins(0, 0, 0, 0) - self.drag_and_drop_layout.setAlignment(QtCore.Qt.AlignCenter) - self.drag_and_drop_widget.setLayout(self.drag_and_drop_layout) - - start_message = 'Drag source files here, or use file browser button below to get started.' - self.drag_and_drop_label = QtWidgets.QLabel(start_message) - self.drag_and_drop_label.setStyleSheet('color: white;') - self.drag_and_drop_layout.addWidget(self.drag_and_drop_label) - self.drag_and_drop_layout.addSpacing(10) - - self.select_files_button_layout = QtWidgets.QHBoxLayout() - self.select_files_button_layout.setAlignment(QtCore.Qt.AlignCenter) - self.select_files_button = QtWidgets.QPushButton('Select Files') - self.select_files_button_layout.addWidget(self.select_files_button) - self.select_files_button.clicked.connect(self.select_files_button_clicked) - self.select_files_button.setFixedSize(80, 35) - self.drag_and_drop_layout.addLayout(self.select_files_button_layout) - self.content_stacked_layout.addWidget(self.drag_and_drop_widget) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Files Table - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.target_files_table = QtWidgets.QTableWidget() - self.target_files_table.setFocusPolicy(QtCore.Qt.NoFocus) - self.target_files_table.setColumnCount(2) - self.target_files_table.setAlternatingRowColors(True) - self.target_files_table.setHorizontalHeaderLabels(['File List', '']) - self.target_files_table.horizontalHeader().setStyleSheet('QHeaderView::section ' - '{background-color: rgb(220, 220, 220); ' - 'padding-top:7px; padding-left:5px;}') - self.target_files_table.verticalHeader().hide() - files_header = self.target_files_table.horizontalHeader() - files_header.setFixedHeight(30) - files_header.setDefaultAlignment(QtCore.Qt.AlignLeft) - files_header.setContentsMargins(10, 10, 0, 0) - files_header.setDefaultSectionSize(60) - files_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) - files_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Fixed) - self.target_files_table.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection) - self.content_stacked_layout.addWidget(self.target_files_table) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Scene Information Table - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.material_tree_view = QtWidgets.QTreeView() - self.headers = ['Key', 'Value'] - self.material_tree_view.setStyleSheet('QTreeView::item {height:25px;} QHeaderView::section ' - '{background-color: rgb(220, 220, 220); height:30px; padding-left:10px}') - self.material_tree_view.setFocusPolicy(QtCore.Qt.NoFocus) - self.material_tree_view.setAlternatingRowColors(True) - self.material_tree_view.setUniformRowHeights(True) - self.content_stacked_layout.addWidget(self.material_tree_view) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> LY Material Definitions - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.lumberyard_material_definitions_widget = QtWidgets.QWidget() - self.lumberyard_material_definitions_layout = QtWidgets.QHBoxLayout(self.lumberyard_material_definitions_widget) - self.lumberyard_material_definitions_layout.setSpacing(0) - self.lumberyard_material_definitions_layout.setContentsMargins(0, 0, 0, 0) - self.lumberyard_material_definitions_frame = QtWidgets.QFrame(self.lumberyard_material_definitions_widget) - self.lumberyard_material_definitions_frame.setGeometry(0, 0, 5000, 5000) - self.lumberyard_material_definitions_frame.setStyleSheet('background-color:rgb(75,75,75);') - self.lumberyard_material_definitions_scroller = QtWidgets.QScrollArea() - self.scroller_widget = QtWidgets.QWidget() - self.scroller_layout = QtWidgets.QVBoxLayout() - self.scroller_widget.setLayout(self.scroller_layout) - self.lumberyard_material_definitions_scroller.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOn) - self.lumberyard_material_definitions_scroller.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.lumberyard_material_definitions_scroller.setWidgetResizable(True) - self.lumberyard_material_definitions_scroller.setWidget(self.scroller_widget) - self.lumberyard_material_definitions_layout.addWidget(self.lumberyard_material_definitions_scroller) - self.content_stacked_layout.addWidget(self.lumberyard_material_definitions_widget) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> File processing buttons - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - self.process_files_layout = QtWidgets.QHBoxLayout() - self.content_layout.addLayout(self.process_files_layout) - self.process_files_button = QtWidgets.QPushButton('Process Added Files') - self.process_files_button.setFixedHeight(50) - self.process_files_button.clicked.connect(self.process_listed_files_clicked) - self.process_files_layout.addWidget(self.process_files_button) - - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - # ---->> Status bar / Loader - # >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> - - # TODO- Move all processing of files to another thread and display progress with loader - - self.status_bar = QtWidgets.QStatusBar() - self.status_bar.setStyleSheet('background-color: rgb(220, 220, 220);') - self.status_bar.setContentsMargins(0, 0, 0, 0) - self.status_bar.setSizeGripEnabled(False) - self.message_readout_label = QtWidgets.QLabel('Ready.') - self.message_readout_label.setStyleSheet('padding-left: 10px') - self.status_bar.addWidget(self.message_readout_label) - - self.progress_bar = QtWidgets.QProgressBar() - self.progress_bar_widget = QtWidgets.QWidget() - self.progress_bar_widget_layout = QtWidgets.QHBoxLayout() - self.progress_bar_widget_layout.setContentsMargins(0, 0, 0, 0) - self.progress_bar_widget_layout.setAlignment(QtCore.Qt.AlignRight) - self.progress_bar_widget.setLayout(self.progress_bar_widget_layout) - self.status_bar.addPermanentWidget(self.progress_bar_widget) - self.progress_bar_widget_layout.addWidget(self.progress_bar) - self.progress_bar.setFixedSize(180, 20) - self.main_container.addWidget(self.status_bar) - self.initialize() - - ############################ - # UI Display Layers ######## - ############################ - - def initialize(self): - if self.cli_enabled: - print('CLI ACCESS:::::::::::\nValues passed: {}'.format(self.cli_enabled)) - self.target_file_list = self.cli_enabled - self.process_file_list() - self.export_selected_materials() - - def populate_source_files_table(self): - """ - Adds selected files from the 'Source Files' section of the UI. This creates each item listing in the table - as well as adds a 'Remove' button that will clear corresponding item from the table. Processed files will - get color coded, based on whether or not the materials in the file could be successfully processed. Subsequent - searches will not clear items from the table currently, as each item acts as a register of materials that have - and have not yet been processed. - :return: - """ - self.target_files_table.setRowCount(0) - for index, entry in enumerate(self.target_file_list): - entry = entry[1] if type(entry) == list else entry - self.target_files_table.insertRow(index) - item = QtWidgets.QTableWidgetItem(' {}'.format(entry)) - self.target_files_table.setRowHeight(index, 45) - remove_button = QtWidgets.QPushButton('Remove') - remove_button.setFixedWidth(60) - remove_button.clicked.connect(self.remove_source_file_clicked) - self.target_files_table.setItem(index, 0, item) - self.target_files_table.setCellWidget(index, 1, remove_button) - - def populate_dcc_material_values_tree(self): - """ - Sets the materials model class to the file attribute tree. - :return: - """ - # TODO- Create mechanism for collapsing previously gathered materials, and or pushing them further down the list - self.material_tree_view.setModel(self.model) - self.material_tree_view.expandAll() - self.material_tree_view.resizeColumnToContents(0) - - def populate_export_materials_list(self): - """ - Once all materials have been analyzed inside of DCC applications, the 'Export Materials' view lists all - materials presented as their Lumberyard counterparts. Each listing displays a representation of the material - file based on its corresponding DCC material values and file connections. - :return: - """ - self.reset_export_materials_description() - for count, value in enumerate(self.lumberyard_materials_dictionary): - material_definition_node = MaterialNode([value, self.lumberyard_materials_dictionary[value]], count) - self.lumberyard_material_nodes.append(material_definition_node) - self.scroller_layout.addWidget(material_definition_node) - self.scroller_layout.addLayout(self.create_separator_line()) - - ############################ - # TBD ######## - ############################ - - def process_file_list(self): - """ - The entry point for reading DCC files and extracting values. Files are filtered and separated - by DCC app (based on file extensions) before processing is done. - - Supported DCC applications: - Maya (.ma, .mb, .fbx), 3dsMax(.max), Blender(.blend) - :return: - """ - files_dict = {'maya': [], 'max': [], 'blender': [], 'na': []} - for file_location in self.target_file_list: - file_name = os.path.basename(str(file_location)) - file_extension = os.path.splitext(file_name)[1] - target_application = self.get_target_application(file_extension) - if target_application in files_dict.keys(): - files_dict[target_application].append(file_location) - - for key, values in files_dict.items(): - try: - if key == 'maya' and len(values): - self.get_maya_material_values(values) - elif key == 'max' and len(values): - self.get_max_material_values(values) - elif key == 'blender' and len(values): - self.get_blender_material_values(values) - else: - pass - except Exception as e: - # TODO- Allow corrective actions or some display of errors if this fails? - logging.warning('Could not process files. Error: {}'.format(e)) - - if self.dcc_materials_dictionary: - self.set_transfer_status(self.dcc_materials_dictionary) - # Create Model with extracted values from file list - self.set_material_model() - # Setup Lumberyard Material File Values - self.set_export_materials_description() - # Update UI Layout - self.populate_export_materials_list() - self.switch_layout_combobox.setCurrentIndex(3) - self.set_ui_buttons() - self.message_readout_label.setText('Ready.') - - def reset_export_materials_description(self): - pass - - def reset_all_values(self): - pass - - def create_separator_line(self): - """ Convenience function for adding separation line to the UI. """ - layout = QtWidgets.QHBoxLayout() - line = QtWidgets.QLabel() - line.setFrameStyle(QtWidgets.QFrame.HLine | QtWidgets.QFrame.Sunken) - line.setLineWidth(1) - line.setFixedHeight(10) - layout.addWidget(line) - layout.setContentsMargins(8, 0, 8, 0) - return layout - - def export_selected_materials(self): - """ - This will eventually be revised to save material definitions in the proper place in the user's project folder, - but for now material definitions will be saved to the desktop. - :return: - """ - for node in self.lumberyard_material_nodes: - if node.material_name_checkbox.isChecked(): - output_path = os.path.dirname(node.material_info['sourceFile']) - node.material_info.pop('sourceFile') - output = os.path.join(output_path, '{}.material'.format(node.material_name)) - with open(output, 'w', encoding='utf-8') as material_file: - json.dump(node.material_info, material_file, ensure_ascii=False, indent=4) - - ############################ - # Getters/Setters ########## - ############################ - - @staticmethod - def get_target_application(file_extension): - """ - Searches compatible file extensions and returns one of three Application names- Maya, 3dsMax, or Blender. - :param file_extension: Passed file extension used to determine DCC Application it originated from. - :return: Returns the application corresponding to the extension if found- otherwise returns a Boolean None - """ - app_extensions = {'maya': ['.ma', '.mb', '.fbx'], 'max': ['.max'], 'blender': ['.blend']} - target_dcc_application = [key for key, values in app_extensions.items() if file_extension in values] - if target_dcc_application: - return target_dcc_application[0] - return None - - @staticmethod - def get_lumberyard_material_template(shader_type): - """ - Loads material descriptions from the Lumberyard installation, providing a template to compare and convert DCC - shaders to Lumberyard material definitions. This is the first step in the comparison. The second step is to - compare these values with specific mapping instructions for DCC Application and DCC material type to arrive at - a converted material. - :param shader_type: The type of Lumberyard shader to pair material attributes to (i.e. PBR Shader) - :return: File dictionary of the available boilerplate Lumberyard shader settings. - """ - definitions = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'resources', - '{}.template.material'.format(shader_type)) - if os.path.exists(definitions): - with open(definitions) as f: - return json.load(f) - - @staticmethod - def get_lumberyard_material_properties(name, dcc_app, material_type, file_connections): - """ - This system will probably need rethinking if DCCs and compatible materials grow. I've tried to keep this - flexible so that it can be expanded with more apps and materials. - - :param name: Material name from within the DCC application - :param dcc_app: The application that the material was sourced from - :param material_type: DCC material type - :param file_connections: Texture files found attached to the materials - """ - - material_properties = {} - if dcc_app == 'Maya': - material_properties = dcc_map.get_maya_material_mapping(name, material_type, file_connections) - elif dcc_app == 'Blender': - material_properties = dcc_map.get_blender_material_mapping(name, material_type, file_connections) - elif dcc_app == '3dsMax': - material_properties = dcc_map.get_max_material_mapping(name, material_type, file_connections) - else: - pass - return material_properties - - @staticmethod - def get_filename_increment(name): - """ - Convenience function that assists in ensuring that if any materials are encountered with the same name, an - underscore and number is appended to it to prevent overwrites. - :param name: The name of the material. The function searches the string for increment numbers, and either adds - one to any encountered, or adds an "_1" if passed name is the first duplicate encountered. - :return: The adjusted name with a unique incremental value. - """ - last_number = re.compile(r'(?:[^\d]*(\d+)[^\d]*)+') - number_found = last_number.search(name) - if number_found: - next_number = str(int(number_found.group(1)) + 1) - start, end = number_found.span(1) - name = name[:max(end - len(next_number), start)] + next_number + name[end:] - return name - - def get_maya_material_values(self, target_files): - """ - Launches Maya Standalone and processes list of materials for each scene passed to the 'target_files' argument. - Also sets the environment paths needed for an instance of Maya's Python distribution. After files are processed - a single dictionary of scene materials is returned, and added to the "materials_dictionary" scene attribute. - :param target_files: List of files filtered from total list of files requested for processing that have a - Maya file extension - :return: - """ - - # TODO- Set load process to a separate thread and wire load progress bar up - - try: - script_path = str(os.path.join(self.directory_path, 'maya_materials.py')) - target_files.append(self.total_materials) - runtime_env = os.environ.copy() - runtime_env['MAYA_LOCATION'] = os.path.dirname(self.mayapy_path) - runtime_env['PYTHONPATH'] = os.path.dirname(self.mayapy_path) - command = f'{self.mayapy_path} "{script_path}"' - for file in target_files: - command += f' "{file}"' - p = subprocess.Popen(command, shell=False, env=runtime_env, stdout=subprocess.PIPE) - output = p.communicate()[0] - self.set_material_dictionary(json.loads(output)) - except Exception as e: - logging.warning('maya error: {}'.format(e)) - - def get_max_material_values(self, target_files): - """ - This has not been implemented yet. - - :param target_files: List of files filtered from total list of files requested for processing that have a - .max file extension - :return: - """ - logging.debug('Max Target file: {}'.format(target_files)) - - def get_blender_material_values(self, target_files): - """ - This has not been implemented yet. - - :param target_files: List of files filtered from total list of files requested for processing that have a - .blend file extension - :return: - """ - logging.debug('Blender Target file: {}'.format(target_files)) - script_path = str(os.path.join(self.directory_path, 'blender_materials.py')) - target_files.append(self.total_materials) - p = subprocess.Popen([self.blender_path, '--background', '--python', script_path, '--', target_files]) - output = p.communicate()[0] - self.set_material_dictionary(json.loads(output)) - - def get_blender_path(self): - """ - Finds latest Blender version installed on the user machine for command line file processing. - - :return: Most current version available (or none) - """ - blender_base_directory = os.path.join(os.path.join('C:\\', 'Program Files', 'Blender Foundation')) - blender_versions_found = [] - for (directory_path, directory_name, filenames) in os.walk(blender_base_directory): - for filename in filenames: - if filename == 'blender.exe': - blender_versions_found.append(os.path.join(directory_path, filename)) - - if blender_versions_found: - return max(blender_versions_found, key=os.path.getctime) - else: - return None - - def set_combobox_items_accessibility(self): - """ - Locks items from within the combobox until the sections they connect to have content - :return: - """ - # TODO- Add this functionality - pass - - def set_transfer_status(self, transfer_info): - """ - Colorizes listings in the 'Source Files' view of the UI after processing to green or red, indicating whether or - not scene analysis successfully returned compatible materials and their values. - :param transfer_info: Each file the scripts attempt to process return a receipt of the success or failure of - the analysis. - :return: - """ - - # TODO- Include some way to get error information if analysis fails, and potentially offer the means to - # repair values as they map to intended Lumberyard shader type - - for row in range(self.target_files_table.rowCount()): - for key, values in transfer_info.items(): - row_path = self.target_files_table.item(row, 0).text().strip() - scene_processed = {x for x in transfer_info if values['SceneName'].replace('\\', '/') == row_path} - if scene_processed: - self.target_files_table.item(row, 0).setBackground(QtGui.QColor(192, 255, 171)) - break - else: - self.target_files_table.item(row, 0).setBackground(QtGui.QColor(255, 177, 171)) - - def set_export_materials_description(self): - root = self.model.rootItem - for row in range(self.model.rowCount()): - source_file = self.model.get_attribute_value('SceneName', root.child(row)) - name = self.model.get_attribute_value('MaterialName', root.child(row)) - material_type = self.model.get_attribute_value('MaterialType', root.child(row)) - dcc_app = self.model.get_attribute_value('DccApplication', root.child(row)) - file_connections = {} - shader_attributes = {} - - for childIndex in range(root.child(row).childCount()): - child_item = root.child(row).child(childIndex) - child_value = child_item.itemData - if child_item.childCount(): - target_dict = file_connections if child_value[0] == 'FileConnections' else shader_attributes - for subChildIndex in range(child_item.childCount()): - sub_child_data = child_item.child(subChildIndex).itemData - target_dict[sub_child_data[0]] = sub_child_data[1] - self.set_material_description(source_file, name, dcc_app, material_type, file_connections) - - def set_material_dictionary(self, dcc_dictionary): - """ - Adds all material descriptions pulled from each DCC file analyzed to the "materials_dictionary" class attribute. - This function runs each time a subprocess is launched to gather DCC application material values. - :param dcc_dictionary: The dictionary of values for each material analyzed by each specific DCC file list - return analyzed values - :return: - """ - logging.debug('DCC Dictionary: {}'.format(json.dumps(dcc_dictionary, indent=4))) - self.total_materials += len(dcc_dictionary) - self.dcc_materials_dictionary.update(dcc_dictionary) - - def set_material_model(self, initialize=True): - """ - Once all materials have been gathered across a selected file set query, this organizes the values into a - QT Model Class - :param initialize: Default is set to boolean True. If a model has already been established in the current - session, the initialize parameter would be set to false, and the values added to the Model. All changes to - the model would then be redistributed to other informational views in the UI. - :return: - """ - if initialize: - self.model = MaterialsModel(self.headers, self.dcc_materials_dictionary) - else: - self.model.update() - self.dcc_materials_dictionary.clear() - self.populate_dcc_material_values_tree() - - def set_ui_buttons(self): - """ - Handles UI buttons for each of the three stacked layout views (Source Files, DCC Material Values, - Export Materials) - :return: - """ - display_index = self.content_stacked_layout.currentIndex() - self.switch_layout_combobox.setEnabled(True) - self.process_files_button.setText('Process Listed Files') - # Add Source Files Layout ------------------------------->> - if display_index == 0: - self.process_files_button.setEnabled(True) - - # Source File List -------------------------------------->> - elif display_index == 1: - self.process_files_button.setEnabled(True) - - # DCC Material Values Layout ---------------------------->> - elif display_index == 2: - self.process_files_button.setEnabled(False) - - # Export Materials Layout ------------------------------->> - else: - self.process_files_button.setText('Export Selected Materials') - if self.lumberyard_materials_dictionary: - self.process_files_button.setEnabled(True) - - def set_material_description(self, source_file, name, dcc_app, material_type, file_connections): - """ - Build dictionary for material description based on extracted values - - :param source_file: The file that the material was extracted from - :param name: Name of material - :param dcc_app: Source file type of material (Maya, Blender or 3ds Max) - :param material_type: Material type within app (i.e. Stingray PBS) - :param file_connections: Texture files found connected to the shader - :return: - """ - - default_settings = self.get_lumberyard_material_template('standardPBR') - material = collections.OrderedDict(sourceFile=source_file, description=name, - materialType=default_settings.get('materialType'), - parentMaterial=default_settings.get('parentMaterial'), - propertyLayoutVersion=default_settings.get('propertyLayoutVersion'), - properties=self.get_lumberyard_material_properties(name, dcc_app, - material_type, - file_connections)) - name += self.output_material_type - self.lumberyard_materials_dictionary[name if name not in self.lumberyard_materials_dictionary.keys() else - self.get_filename_increment(name)] = material - - ############################ - # Button Actions ########### - ############################ - - def remove_source_file_clicked(self): - """ - In the Source File view of the UI layout, this will remove the listed file in its respective row. If files - have not been processed yet, it prevents that file from being analyzed. If the files have already been - analyzed, this will remove the materials from stored values. - :return: - """ - file_index = self.target_files_table.indexAt(self.sender().pos()) - del self.target_file_list[file_index.row()] - self.populate_files_table() - - def process_listed_files_clicked(self): - """ - The button serves a dual purpose, depending on the current layout of the window. 'Process listed files' - initiates the DCC file analysis that extracts material information. In the "Export Materials" layout, this - button (for now) will export material files corresponding to each analyzed material. Exported material files - are routed to the directories of the respective files processed. - :return: - """ - - if self.sender().text() == 'Process Added Files': - self.message_readout_label.setText('Gathering Material Information...') - self.app.processEvents() - self.process_file_list() - else: - self.export_selected_materials() - - def select_files_button_clicked(self): - """ - This dialog allows user to select DCC files to be processed for the materials present for conversion. - :return: - """ - - # TODO- Eventually it might be worth it to allow files from multiple locations to be selected. Currently - # this only allows single/multiple files from a single directory to be selected, although drag and drop - # allows multiple locations - - dialog = QtWidgets.QFileDialog(self, 'Shift-Select Target Files', self.desktop_location) - dialog.setFileMode(QtWidgets.QFileDialog.ExistingFile) - dialog.setNameFilter('Compatible Files (*.ma *.mb *.fbx *.max *.blend)') - dialog.setOption(QtWidgets.QFileDialog.DontUseNativeDialog, True) - file_view = dialog.findChild(QtWidgets.QListView, 'listView') - - # Workaround for selecting multiple files with File Dialog - if file_view: - file_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - f_tree_view = dialog.findChild(QtWidgets.QTreeView) - if f_tree_view: - f_tree_view.setSelectionMode(QtWidgets.QAbstractItemView.MultiSelection) - - if dialog.exec_() == QtWidgets.QDialog.Accepted: - self.target_file_list += dialog.selectedFiles() - if self.target_file_list: - self.populate_source_files_table() - self.message_readout_label.setText('Source files added: {}'.format(len(self.target_file_list))) - self.process_files_button.setEnabled(True) - - def layout_combobox_changed(self): - """ - Handles main window layout combobox index change. - :return: - """ - self.content_stacked_layout.setCurrentIndex(self.switch_layout_combobox.currentIndex()) - self.set_ui_buttons() - - def reset_clicked(self): - """ - Brings the application and all variables back to their initial state. - :return: - """ - self.reset_all_values() - - ############################ - # Slots #################### - ############################ - - @Slot(list) - def drag_and_drop_file_update(self, file_list): - for file in file_list: - if os.path.basename(file).split('.')[-1] in self.blessed_file_extensions: - self.target_file_list.append(file) - self.drag_and_drop_widget.urls.clear() - self.populate_source_files_table() - self.message_readout_label.setText('Source files added: {}'.format(len(self.target_file_list))) - self.drag_and_drop_label.setStyleSheet('color: white;') - - @Slot(bool) - def drag_and_drop_over(self, is_over): - if is_over: - self.drag_and_drop_label.setStyleSheet('color: rgb(0, 255, 0);') - else: - self.drag_and_drop_label.setStyleSheet('color: white;') - - -class MaterialNode(QtWidgets.QWidget): - def __init__(self, material_info, current_position, parent=None): - super(MaterialNode, self).__init__(parent) - - self.material_name = material_info[0] - self.material_info = material_info[1] - self.current_position = current_position - self.property_settings = {} - - self.small_font = QtGui.QFont("Helvetica", 7, QtGui.QFont.Bold) - self.bold_font = QtGui.QFont("Helvetica", 8, QtGui.QFont.Bold) - self.main_layout = QtWidgets.QVBoxLayout() - self.main_layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(self.main_layout) - - self.background_frame = QtWidgets.QFrame(self) - self.background_frame.setGeometry(0, 0, 5000, 5000) - self.background_frame.setStyleSheet('background-color:rgb(220, 220, 220);') - - # ######################## - # Title Bar - # ######################## - - self.title_bar_widget = QtWidgets.QWidget() - self.title_bar_layout = QtWidgets.QHBoxLayout(self.title_bar_widget) - self.title_bar_layout.setContentsMargins(10, 0, 10, 0) - self.title_bar_layout.setAlignment(QtCore.Qt.AlignTop) - self.title_bar_frame = QtWidgets.QFrame(self.title_bar_widget) - self.title_bar_frame.setGeometry(0, 0, 5000, 40) - self.title_bar_frame.setStyleSheet('background-color:rgb(193,154,255);') - self.main_layout.addWidget(self.title_bar_widget) - self.material_name_checkbox = QtWidgets.QCheckBox(self.material_name) - self.material_name_checkbox.setFixedHeight(35) - self.material_name_checkbox.setStyleSheet('spacing:10px; color:white') - self.material_name_checkbox.setFont(self.bold_font) - self.material_name_checkbox.setChecked(True) - self.title_bar_layout.addWidget(self.material_name_checkbox) - - self.material_file_layout = QtWidgets.QHBoxLayout() - self.material_file_layout.setAlignment(QtCore.Qt.AlignRight) - self.source_file = QtWidgets.QLabel(os.path.basename(self.material_info['sourceFile'])) - self.source_file.setStyleSheet('color:white;') - self.source_file.setFont(self.small_font) - self.material_file_layout.addWidget(self.source_file) - self.material_file_layout.addSpacing(10) - - self.edit_button = QtWidgets.QPushButton('Edit') - self.edit_button.clicked.connect(self.edit_button_clicked) - self.edit_button.setFixedWidth(55) - self.material_file_layout.addWidget(self.edit_button) - self.title_bar_layout.addLayout(self.material_file_layout) - - self.information_layout = QtWidgets.QHBoxLayout() - self.information_layout.setContentsMargins(10, 0, 10, 10) - self.main_layout.addLayout(self.information_layout) - - # ######################## - # Details layout - # ######################## - - self.details_layout = QtWidgets.QVBoxLayout() - self.details_layout.setAlignment(QtCore.Qt.AlignTop) - self.details_groupbox = QtWidgets.QGroupBox("Details") - self.details_groupbox.setFixedWidth(200) - self.details_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; " - "margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); " - "subcontrol-position: top left;}") - self.details_layout.addSpacing(15) - self.material_type_label = QtWidgets.QLabel('Material Type') - self.material_type_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);') - self.material_type_label.setFixedHeight(25) - self.material_type_label.setFont(self.bold_font) - self.details_layout.addWidget(self.material_type_label) - - self.material_type_combobox = QtWidgets.QComboBox() - self.material_type_combobox.setFixedHeight(30) - self.material_type_combobox.setStyleSheet('QCombobox QAbstractItemView { padding-left: 15px; }') - material_type_items = [' Standard PBR'] - self.material_type_combobox.addItems(material_type_items) - self.details_layout.addWidget(self.material_type_combobox) - self.details_layout.addSpacing(10) - - self.description_label = QtWidgets.QLabel('Description') - self.description_label.setStyleSheet('padding-left: 6px; color: white; background-color:rgb(175, 175, 175);') - self.description_label.setFixedHeight(25) - self.description_label.setFont(self.bold_font) - self.details_layout.addWidget(self.description_label) - - self.description_box = QtWidgets.QTextEdit('This space is reserved for additional information.') - self.details_layout.addWidget(self.description_box) - self.information_layout.addWidget(self.details_groupbox) - self.details_groupbox.setLayout(self.details_layout) - - # ######################## - # Properties layout - # ######################## - - self.properties_layout = QtWidgets.QVBoxLayout() - self.properties_layout.setAlignment(QtCore.Qt.AlignTop) - self.properties_groupbox = QtWidgets.QGroupBox("Properties") - self.properties_groupbox.setFixedWidth(150) - self.properties_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; " - "margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); " - "subcontrol-position: top left;}") - self.properties_list_widget = QtWidgets.QListWidget() - self.material_properties = ['ambientOcclusion', 'baseColor', 'emissive', 'metallic', 'roughness', 'specularF0', - 'normal', 'opacity'] - self.properties_list_widget.addItems(self.material_properties) - self.properties_list_widget.itemSelectionChanged.connect(self.property_selection_changed) - self.properties_layout.addSpacing(15) - self.properties_layout.addWidget(self.properties_list_widget) - self.information_layout.addWidget(self.properties_groupbox) - self.properties_groupbox.setLayout(self.properties_layout) - - # ######################## - # Attributes layout - # ######################## - - self.attributes_layout = QtWidgets.QVBoxLayout() - self.attributes_layout.setAlignment(QtCore.Qt.AlignTop) - self.attributes_groupbox = QtWidgets.QGroupBox("Attributes") - self.attributes_groupbox.setStyleSheet("QGroupBox {font:bold; border: 1px solid silver; " - "margin-top: 6px;} QGroupBox::title { color: rgb(150, 150, 150); " - "subcontrol-position: top left;}") - self.information_layout.addWidget(self.attributes_groupbox) - self.attributes_layout.addSpacing(15) - self.attributes_table = QtWidgets.QTableWidget() - self.attributes_table.setFocusPolicy(QtCore.Qt.NoFocus) - self.attributes_table.setColumnCount(2) - self.attributes_table.setAlternatingRowColors(True) - self.attributes_table.setHorizontalHeaderLabels(['Attribute', 'Value']) - self.attributes_table.verticalHeader().hide() - attributes_table_header = self.attributes_table.horizontalHeader() - attributes_table_header.setStyleSheet('QHeaderView::section {background-color: rgb(220, 220, 220);}') - attributes_table_header.setDefaultAlignment(QtCore.Qt.AlignLeft) - attributes_table_header.setContentsMargins(10, 10, 0, 0) - attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Stretch) - attributes_table_header.setSectionResizeMode(1, QtWidgets.QHeaderView.Stretch) - attributes_table_header.setSectionResizeMode(0, QtWidgets.QHeaderView.Interactive) - self.attributes_layout.addWidget(self.attributes_table) - self.attributes_groupbox.setLayout(self.attributes_layout) - self.initialize_display_values() - - def initialize_display_values(self): - """ - Initializes all of the widget item information for material based on the DCC application info the class has - been passed. - :return: - """ - for material_property in self.material_properties: - if material_property in self.material_info.get('properties'): - self.property_settings[material_property] = self.material_info['properties'].get(material_property) - current_row = self.material_properties.index(material_property) - current_item = self.properties_list_widget.takeItem(current_row) - self.properties_list_widget.insertItem(0, current_item) - else: - self.property_settings[material_property] = 'inactive' - current_row = self.material_properties.index(material_property) - item = self.properties_list_widget.item(current_row) - item.setFlags(item.flags() & ~QtCore.Qt.ItemIsEnabled) - item.setFlags(item.flags() & ~QtCore.Qt.ItemIsSelectable) - - self.properties_list_widget.setCurrentRow(0) - self.set_attributes_table(self.get_selected_property()) - - def set_attributes_table(self, selected_property): - """ - Displays the key, value pairs for the item selected in the Properties list widget - :param selected_property: The item in the Properties list widget that is currently selected. Only active - values are displayed. - :return: - """ - self.attributes_table.setRowCount(0) - row_count = 0 - for key, value in self.property_settings[selected_property].items(): - self.attributes_table.insertRow(row_count) - key_item = QtWidgets.QTableWidgetItem(key) - self.attributes_table.setItem(row_count, 0, key_item) - value_item = QtWidgets.QTableWidgetItem(value) - self.attributes_table.setItem(row_count, 1, value_item) - row_count += 1 - - def get_selected_property(self): - """ - Convenience function to get current value selected in the Properties list widget. - :return: - """ - return self.properties_list_widget.currentItem().text() - - def update_model(self): - """ - Not sure if this will go away, but if desired, I could make attribute values able to be revised after - materials have been scraped from the DCC materials - :return: - """ - pass - - def edit_button_clicked(self): - """ - This is in place in the event that we want to allow material revisions for properties to be made after - DCC processing step has already been executed. The idea would basically be to surface an editable - table where values can be added, removed or changed within the final material definition. - :return: - """ - logging.debug('Edit button clicked') - - def property_selection_changed(self): - """ - Fired when index of list view selected property selection has changed. - :return: - """ - self.set_attributes_table(self.get_selected_property()) - - -def is_valid_file(file_name): - """ - The acts as a clearinghouse for DCC file types supported by the script - :param file_name: Reads the extension of the filename for filtering - :return: - """ - target_extensions = 'ma mb fbx blend max'.split(' ') - if file_name.split('.')[-1] in target_extensions: - return True - return False - - -def launch_material_converter(window_type='standalone', material_type='PBR', target_files=None): - """ - The setup for this will be revised once this is fully integrated into the DCCsi system. Currently only the - standalone (default) and command line entry points work as intended. - :param window_type: The method of access for material conversion (standalone, command_line, maya_native, max_native) - :param material_type: Type of output material desired for import into Lumberyard. Currently only PBR is supported - :param target_files: DCC app files to process for converted Lumberyard materials - :return: - """ - if window_type == 'command_line': - MaterialsToLumberyard(material_type, target_files) - elif window_type == 'maya_native': - from maya import OpenMayaUI as omui - main_window_pointer = omui.MQtUtil.mainWindow() - main_app_window = wrapInstance(long(main_window_pointer), QtWidgets.QWidget) - MaterialsToLumberyard(material_type, None, main_app_window) - elif window_type == 'max_native': - from pymxs import runtime as rt - main_window_pointer = QtWidgets.QWidget.find(rt.windows.getMAXHWND()) - main_app_window = shiboken2.wrapInstance(shiboken2.getCppPointer(main_window_pointer)[0], QtWidgets.QMainWindow) - MaterialsToLumberyard(material_type, None, main_app_window) - else: - app = QApplication(sys.argv) - app_ui = MaterialsToLumberyard() - app_ui.show() - sys.exit(app.exec_()) - - -if __name__ == '__main__': - launch_material_converter() - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/maya_materials.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/maya_materials.py deleted file mode 100755 index 4e66f83661..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/maya_materials.py +++ /dev/null @@ -1,169 +0,0 @@ -# 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 -# - -from PySide2 import QtCore -import maya.standalone -maya.standalone.initialize(name='python') -import maya.cmds as mc -import collections -import logging -import json -import sys -import os - - -for handler in logging.root.handlers[:]: - logging.root.removeHandler(handler) - -logging.basicConfig(level=logging.INFO, - format='%(name)s - %(levelname)s - %(message)s', - datefmt='%m-%d %H:%M', - filename='output.log', - filemode='w') - - -class MayaMaterials(QtCore.QObject): - def __init__(self, files_list, materials_count, parent=None): - super(MayaMaterials, self).__init__(parent) - - self.files_list = files_list - self.current_scene = None - self.materials_dictionary = {} - self.materials_count = int(materials_count) - self.get_material_information() - - def get_material_information(self): - """ - Main entry point for the material information extraction. Because this class is run - in Standalone mode as a subprocess, the list is passed as a string- some parsing/measures - need to be taken in order to separate values that originated as a list before passed. - - :return: A dictionary of all of the materials gathered. Sent back to main UI through stdout - """ - for target_file in file_list: - self.current_scene = os.path.abspath(target_file.replace('\'', '')) - mc.file(self.current_scene, open=True, force=True) - self.set_material_descriptions() - json.dump(self.materials_dictionary, sys.stdout) - - @staticmethod - def get_materials(target_mesh): - """ - Gathers a list of all materials attached to each mesh's shader - - :param target_mesh: The target mesh to pull attached material information from. - :return: List of unique material values attached to the mesh passed as an argument. - """ - shading_group = mc.listConnections(target_mesh, type='shadingEngine') - materials = mc.ls(mc.listConnections(shading_group), materials=1) - return list(set(materials)) - - @staticmethod - def get_shader(material_name): - """ - Convenience function for obtaining the shader that the specified material (as an argument) - is attached to. - - :param material_name: Takes the material name as an argument to get associated shader object - :return: - """ - connections = mc.listConnections(material_name, type='shadingEngine')[0] - shader_name = '{}.surfaceShader'.format(connections) - shader = mc.listConnections(shader_name)[0] - return shader - - def get_shader_information(self, shader, material_mesh): - """ - Helper function for extracting shader/material attributes used to form the DCC specific dictionary - of found material values for conversion. - - :param shader: The target shader object to analyze - :param material_mesh: The material mesh needs to be passed to search for textures attached to it. - :return: Complete set (in the form of two dictionaries) of file connections and material attribute values - """ - shader_file_connections = {} - materials = self.get_materials(material_mesh) - for material in materials: - material_files = [x for x in mc.listConnections(material, plugs=1, source=1) if x.startswith('file')] - for file_name in material_files: - file_texture = mc.getAttr('{}.fileTextureName'.format(file_name.split('.')[0])) - if os.path.basename(file_texture).split('.')[-1] != 'dds': - key_name = mc.listConnections(file_name, plugs=1, source=1)[0] - shader_file_connections[key_name] = file_texture - - shader_attributes = {} - for shader_attribute in mc.listAttr(shader, s=True, iu=True): - try: - shader_attributes[str(shader_attribute)] = str(mc.getAttr('{}.{}'.format(shader, shader_attribute))) - except Exception as e: - logging.error('MayaAttributeError: {}'.format(e)) - - return shader_file_connections, shader_attributes - - def set_material_dictionary(self, material_name, material_type, material_mesh): - """ - When a unique material has been found, this creates a dictionary entry with all relevant material values. This - includes material attributes as well as attached file textures. Later in the process this information is - leveraged when creating the Lumberyard material definition. - - :param material_name: The name attached to the material - :param material_type: Specific type of material (Arnold, Stingray, etc.) - :param material_mesh: Mesh that the material is applied to - :return: - """ - self.materials_count += 1 - shader = self.get_shader(material_name) - shader_file_connections, shader_attributes = self.get_shader_information(shader, material_mesh) - material_dictionary = collections.OrderedDict(MaterialName=material_name, MaterialType=material_type, - DccApplication='Maya', AppliedMesh=material_mesh, - FileConnections=shader_file_connections, - SceneName=str(self.current_scene), - MaterialAttributes=shader_attributes) - material_name = 'Material_{}'.format(self.materials_count) - self.materials_dictionary[material_name] = material_dictionary - logging.info('\n\n:::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n' - 'MATERIAL DEFINITION: {} \n' - ':::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::\n{}'.format( - self.materials_dictionary[material_name]['MaterialType'], - json.dumps(self.materials_dictionary[material_name], indent=4))) - - def set_material_descriptions(self): - """ - This function serves as the clearinghouse for all analyzed materials passing through the system. - It will determine whether or not the found material has already been processed, or if it needs to - be added to the final material dictionary. In the event that an encountered material has already - been processed, this function creates a register of all meshes it is applied to in the 'AppliedMesh' - attribute. - :return: - """ - scene_geo = mc.ls(v=True, geometry=True) - for target_mesh in scene_geo: - material_list = self.get_materials(target_mesh) - for material_name in material_list: - material_type = mc.nodeType(material_name) - - if material_type != 'lambert': - material_listed = [x for x in self.materials_dictionary - if self.materials_dictionary[x]['MaterialName'] == material_name] - - if not material_listed: - self.set_material_dictionary(str(material_name), str(material_type), str(target_mesh)) - else: - mesh_list = self.materials_dictionary[material_name].get('AppliedMesh') - if not isinstance(mesh_list, list): - self.materials_dictionary[str(material_name)]['AppliedMesh'] = [mesh_list, target_mesh] - else: - mesh_list.append(target_mesh) - - -# ++++++++++++++++++++++++++++++++++++++++++++++++# -# Maya Specific Shader Mapping # -# ++++++++++++++++++++++++++++++++++++++++++++++++# - -file_list = sys.argv[1:-1] -count = sys.argv[-1] -instance = MayaMaterials(file_list, count) - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/model.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/model.py deleted file mode 100755 index 3c23635438..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/model.py +++ /dev/null @@ -1,154 +0,0 @@ -# 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 -# -# - -from PySide2.QtCore import QAbstractItemModel, QModelIndex, Qt - - -class MaterialsModel(QAbstractItemModel): - def __init__(self, headers, data, parent=None): - super(MaterialsModel, self).__init__(parent) - - self.rootItem = TreeNode(headers) - self.parents = [self.rootItem] - self.indentations = [0] - self.create_data(data) - - def create_data(self, data, indent=-1): - """ - Recursive loop that structures Model data into tree form. - :param data: Row information. - :param indent: Column information. This helps to facilitate the creation of nested rows. - :return: - """ - if type(data) == dict: - indent += 1 - position = 4 * indent - for key, value in data.items(): - if position > self.indentations[-1]: - if self.parents[-1].childCount() > 0: - self.parents.append(self.parents[-1].child(self.parents[-1].childCount() - 1)) - self.indentations.append(position) - else: - while position < self.indentations[-1] and len(self.parents) > 0: - self.parents.pop() - self.indentations.pop() - parent = self.parents[-1] - parent.insertChildren(parent.childCount(), 1, parent.columnCount()) - parent.child(parent.childCount() - 1).setData(0, key) - value_string = str(value) if type(value) != dict else str('') - parent.child(parent.childCount() - 1).setData(1, value_string) - try: - self.create_data(value, indent) - except RuntimeError: - pass - - @staticmethod - def get_attribute_value(search_string, search_column): - """ Convenience function for quickly accessing row information based on attribute keys. """ - for childIndex in range(search_column.childCount()): - child_item = search_column.child(childIndex) - child_value = child_item.itemData - if child_value[0] == search_string: - return child_value[1] - return None - - def index(self, row, column, index=QModelIndex()): - """ Returns the index of the item in the model specified by the given row, column and parent index """ - if not self.hasIndex(row, column, index): - return QModelIndex() - if not index.isValid(): - item = self.rootItem - else: - item = index.internalPointer() - - child = item.child(row) - if child: - return self.createIndex(row, column, child) - return QModelIndex() - - def parent(self, index): - """ - Returns the parent of the model item with the given index If the item has no parent, - an invalid QModelIndex is returned - """ - if not index.isValid(): - return QModelIndex() - item = index.internalPointer() - if not item: - return QModelIndex() - - parent = item.parentItem - if parent == self.rootItem: - return QModelIndex() - else: - return self.createIndex(parent.childNumber(), 0, parent) - - def rowCount(self, index=QModelIndex()): - """ - Returns the number of rows under the given parent. When the parent is valid it means that - rowCount is returning the number of children of parent - """ - if index.isValid(): - parent = index.internalPointer() - else: - parent = self.rootItem - return parent.childCount() - - def columnCount(self, index=QModelIndex()): - """ Returns the number of columns for the children of the given parent """ - return self.rootItem.columnCount() - - def data(self, index, role=Qt.DisplayRole): - """ Returns the data stored under the given role for the item referred to by the index """ - if index.isValid() and role == Qt.DisplayRole: - return index.internalPointer().data(index.column()) - elif not index.isValid(): - return self.rootItem.data(index.column()) - - def headerData(self, section, orientation, role=Qt.DisplayRole): - """ Returns the data for the given role and section in the header with the specified orientation """ - if orientation == Qt.Horizontal and role == Qt.DisplayRole: - return self.rootItem.data(section) - - -class TreeNode(object): - def __init__(self, data, parent=None): - self.parentItem = parent - self.itemData = data - self.children = [] - - def child(self, row): - return self.children[row] - - def childCount(self): - return len(self.children) - - def childNumber(self): - if self.parentItem is not None: - return self.parentItem.children.index(self) - - def columnCount(self): - return len(self.itemData) - - def data(self, column): - return self.itemData[column] - - def insertChildren(self, position, count, columns): - if position < 0 or position > len(self.children): - return False - for row in range(count): - data = [v for v in range(columns)] - item = TreeNode(data, self) - self.children.insert(position, item) - - def parent(self): - return self.parentItem - - def setData(self, column, value): - if column < 0 or column >= len(self.itemData): - return False - self.itemData[column] = value diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/standalone.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/standalone.py deleted file mode 100644 index e7a8445953..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/standalone.py +++ /dev/null @@ -1,37 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 -# - -"""Boostraps and Starts Standalone DCC Material Converter utility""" - -# built in's -import os -import site - -# ------------------------------------------------------------------------- -# \dev\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\PythonTools\DCC_Material_Converter\standalone.py -_MODULE_PATH = os.path.abspath(__file__) - -_DCCSIG_REL_PATH = "../../../.." -_DCCSIG_PATH = os.path.join(_MODULE_PATH, _DCCSIG_REL_PATH) -_DCCSIG_PATH = os.path.normpath(_DCCSIG_PATH) - -_DCCSIG_PATH = os.getenv('DCCSIG_PATH', - os.path.abspath(_DCCSIG_PATH)) - -# we don't have access yet to the DCCsi Lib\site-packages -site.addsitedir(_DCCSIG_PATH) # PYTHONPATH - -# azpy bootstrapping and extensions -import azpy.config_utils -_config = azpy.config_utils.get_dccsi_config() -settings = _config.get_config_settings(setup_ly_pyside=True) - -from main import launch_material_converter - -launch_material_converter() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/Launcher/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/Launcher/main.py deleted file mode 100644 index 5812755035..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/Launcher/main.py +++ /dev/null @@ -1,74 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- -# built-ins -import os -import sys -import logging as _logging - -# azpy extensions -import azpy.config_utils -_config = azpy.config_utils.get_dccsi_config() -settings = _config.get_config_settings(setup_ly_pyside=True) - -# 3rd Party (we may or do provide) -from pathlib import Path -from pathlib import PurePath - -# Lumberyard extensions -from azpy.env_bool import env_bool -from azpy.constants import ENVAR_DCCSI_GDEBUG -from azpy.constants import ENVAR_DCCSI_DEV_MODE - -# ------------------------------------------------------------------------- -# set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, settings.DCCSI_GDEBUG) - -for handler in _logging.root.handlers[:]: - _logging.root.removeHandler(handler) - -_MODULENAME = 'DCCsi.SDK.pythontools.launcher.main' - -_log_level = _logging.INFO -if _G_DEBUG: - _log_level = _logging.DEBUG - -_LOGGER = azpy.initialize_logger(name=_MODULENAME, - log_to_file=True, - default_log_level=_log_level) - -_LOGGER.debug('Starting up: {0}.'.format({_MODULENAME})) -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -def main(): - from PySide2.QtWidgets import QApplication, QPushButton - - app = QApplication(sys.argv) -# ------------------------------------------------------------------------- - - -# -------------------------------------------------------------------------- -if __name__ == '__main__': - """Run this file as main""" - -app = QApplication([]) # Start an application. -window = QWidget() # Create a window. -layout = QVBoxLayout() # Create a layout. -button = QPushButton("I'm just a Button man") # Define a button -layout.addWidget(QLabel('Hello World!')) # Add a label -layout.addWidget(button) # Add the button man -window.setLayout(layout) # Pass the layout to the window -window.show() # Show window -app.exec_() # Execute the App diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py index 4c35861b9c..b7a6bfa3a1 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/atom_material.py @@ -29,7 +29,7 @@ from pathlib import Path # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py index 50a4d3fdda..f3e91b59ce 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/bootstrap.py @@ -34,20 +34,20 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # these are for module debugging, set to false on submit -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = 'DCCsi.SDK.substance.builder.bootstrap' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_PACKAGENAME, log_to_file=True, default_log_level=_log_level) _LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) _LOGGER.debug('_DCCSIG_PATH: {}'.format(_DCCSIG_PATH)) -_LOGGER.debug('_G_DEBUG: {}'.format(_G_DEBUG)) +_LOGGER.debug('_G_DEBUG: {}'.format(_DCCSI_GDEBUG)) _LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) if _DCCSI_DEV_MODE: @@ -69,7 +69,7 @@ from dynaconf import settings try: from PySide2.QtWidgets import QApplication except: - _dccsi_config.init_ly_pyside(settings.LY_DEV) # init for standalone + _dccsi_config.init_o3de_pyside(settings.O3DE_DEV) # init for standalone # running in the editor if the QtForPython Gem is enabled # you should already have access and shouldn't need to set up @@ -92,20 +92,20 @@ os.environ["PYSBS_DIR_PATH"] = str(_PYSBS_DIR_PATH) # standard paths we may use downstream # To Do: move these into a dynaconf config extension specific to this tool? -from azpy.constants import ENVAR_LY_DEV -_LY_DEV = Path(os.getenv(ENVAR_LY_DEV, - settings.LY_DEV)).resolve() +from azpy.constants import ENVAR_O3DE_DEV +_O3DE_DEV = Path(os.getenv(ENVAR_O3DE_DEV, + settings.O3DE_DEV)).resolve() -from azpy.constants import ENVAR_LY_PROJECT_PATH -_LY_PROJECT_PATH = Path(os.getenv(ENVAR_LY_PROJECT_PATH, - settings.LY_PROJECT_PATH)).resolve() +from azpy.constants import ENVAR_O3DE_PROJECT_PATH +_O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, + settings.O3DE_PROJECT_PATH)).resolve() from azpy.constants import ENVAR_DCCSI_SDK_PATH _DCCSI_SDK_PATH = Path(os.getenv(ENVAR_DCCSI_SDK_PATH, settings.DCCSIG_SDK_PATH)).resolve() # build some reuseable path parts for the substance builder -_PROJECT_ASSETS_PATH = Path(_LY_PROJECT_PATH, 'Assets').resolve() +_PROJECT_ASSETS_PATH = Path(_O3DE_PROJECT_PATH, 'Assets').resolve() _PROJECT_MATERIALS_PATH = Path(_PROJECT_ASSETS_PATH, 'Materials').resolve() # ------------------------------------------------------------------------- @@ -116,15 +116,15 @@ _PROJECT_MATERIALS_PATH = Path(_PROJECT_ASSETS_PATH, 'Materials').resolve() if __name__ == "__main__": """Run this file as main""" - _LOGGER.info('_LY_DEV: {}'.format(_LY_DEV)) - _LOGGER.info('_LY_PROJECT_PATH: {}'.format(_LY_PROJECT_PATH)) + _LOGGER.info('_O3DE_DEV: {}'.format(_O3DE_DEV)) + _LOGGER.info('_O3DE_PROJECT_PATH: {}'.format(_O3DE_PROJECT_PATH)) _LOGGER.info('_DCCSI_SDK_PATH: {}'.format(_DCCSI_SDK_PATH)) _LOGGER.info('_PYSBS_DIR_PATH: {}'.format(_PYSBS_DIR_PATH)) _LOGGER.info('_PROJECT_ASSETS_PATH: {}'.format(_PROJECT_ASSETS_PATH)) _LOGGER.info('_PROJECT_MATERIALS_PATH: {}'.format(_PROJECT_MATERIALS_PATH)) - if _G_DEBUG: + if _DCCSI_GDEBUG: _dccsi_config.test_pyside2() # runs a small PySdie2 test # remove the logger diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py index cc501fe872..9408b94919 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sb_gui_main.py @@ -35,7 +35,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, settings.DCCSI_GDEBUG) for handler in _logging.root.handlers[:]: @@ -44,7 +44,7 @@ for handler in _logging.root.handlers[:]: _MODULENAME = 'DCCsi.SDK.substance.builder.sb_gui_main' _log_level = _logging.INFO -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = _logging.DEBUG _LOGGER = azpy.initialize_logger(name=_MODULENAME, @@ -71,12 +71,12 @@ import config _LOGGER.debug('config.py is: {}'.format(config)) # initialize the Lumberyard Qt / PySide2 -config.init_ly_pyside(settings.LY_DEV) # for standalone +config.init_o3de_pyside(settings.O3DE_DEV) # for standalone settings.setenv() # for standalone # log debug info about Qt/PySide2 _LOGGER.debug('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) -_LOGGER.debug('LY_BIN_PATH: {}'.format(settings.LY_BIN_PATH)) +_LOGGER.debug('O3DE_BIN_PATH: {}'.format(settings.O3DE_BIN_PATH)) _LOGGER.debug('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) _LOGGER.debug('QT_QPA_PLATFORM_PLUGIN_PATH: {}'.format(settings.QT_QPA_PLATFORM_PLUGIN_PATH)) # ------------------------------------------------------------------------- @@ -123,26 +123,26 @@ from atom_material import AtomMaterial # ------------------------------------------------------------------------- # To Do: still should manage via dynaconf (dynamic config and settings) -from azpy.constants import ENVAR_LY_DEV -_LY_DEV = Path(os.getenv(ENVAR_LY_DEV, None)).resolve() +from azpy.constants import ENVAR_O3DE_DEV +_O3DE_DEV = Path(os.getenv(ENVAR_O3DE_DEV, None)).resolve() -from azpy.constants import ENVAR_LY_PROJECT -_LY_PROJECT = os.getenv(ENVAR_LY_PROJECT, None) +from azpy.constants import ENVAR_O3DE_PROJECT +_O3DE_PROJECT = os.getenv(ENVAR_O3DE_PROJECT, None) -from azpy.constants import ENVAR_LY_PROJECT_PATH -_LY_PROJECT_PATH = Path(os.getenv(ENVAR_LY_PROJECT_PATH, None)).resolve() +from azpy.constants import ENVAR_O3DE_PROJECT_PATH +_O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, None)).resolve() from azpy.constants import ENVAR_DCCSI_SDK_PATH _DCCSI_SDK_PATH = Path(os.getenv(ENVAR_DCCSI_SDK_PATH, None)).resolve() # build some reuseable path parts -_PROJECT_ASSET_PATH = Path(_LY_PROJECT_PATH).resolve() -_PROJECT_ASSETS_PATH = Path(_LY_PROJECT_PATH, 'Materials').resolve() +_PROJECT_ASSET_PATH = Path(_O3DE_PROJECT_PATH).resolve() +_PROJECT_ASSETS_PATH = Path(_O3DE_PROJECT_PATH, 'Materials').resolve() # To Do: figure out a proper way to deal with Lumberyard game projects -_GEM_MATPLAY_PATH = Path(_LY_DEV, 'Gems', 'AtomContent', 'AtomMaterialPlayground').resolve() -_GEM_ROYALTYFREE = Path(_LY_DEV, 'Gems', 'AtomContent', 'RoyaltyFreeAssets').resolve() -_GEM_SUBSOURCELIBRARY = Path(_LY_DEV, 'Gems', 'AtomContent', 'SubstanceSourceLibrary').resolve() +_GEM_MATPLAY_PATH = Path(_O3DE_DEV, 'Gems', 'AtomContent', 'AtomMaterialPlayground').resolve() +_GEM_ROYALTYFREE = Path(_O3DE_DEV, 'Gems', 'AtomContent', 'RoyaltyFreeAssets').resolve() +_GEM_SUBSOURCELIBRARY = Path(_O3DE_DEV, 'Gems', 'AtomContent', 'SubstanceSourceLibrary').resolve() _SUB_LIBRARY_PATH = Path(_GEM_SUBSOURCELIBRARY, 'Assets', 'SubstanceSource', 'Library').resolve() # ^ This hard codes a bunch of known asset gems, again bad # To Do: figure out a proper way to scrap the gem registry from project @@ -150,9 +150,9 @@ _SUB_LIBRARY_PATH = Path(_GEM_SUBSOURCELIBRARY, 'Assets', 'SubstanceSource', 'Li # path to watcher script _WATCHER_SCRIPT_PATH = Path(_DCCSI_SDK_PATH, 'substance', 'builder', 'watchdog', '__init__.py').resolve() -_TEX_RNDR_PATH = Path(_LY_PROJECT_PATH, 'Materials', 'Substance').resolve() -_MAT_OUTPUT_PATH = Path(_LY_PROJECT_PATH, 'Materials', 'Substance').resolve() -_SBSAR_COOK_PATH = Path(_LY_PROJECT_PATH, 'Materials', 'Substance').resolve() +_TEX_RNDR_PATH = Path(_O3DE_PROJECT_PATH, 'Materials', 'Substance').resolve() +_MAT_OUTPUT_PATH = Path(_O3DE_PROJECT_PATH, 'Materials', 'Substance').resolve() +_SBSAR_COOK_PATH = Path(_O3DE_PROJECT_PATH, 'Materials', 'Substance').resolve() # ------------------------------------------------------------------------- @@ -171,7 +171,7 @@ class Window(QtWidgets.QDialog): # we should really init non-Qt stuff and set things up as properties if project_path is None: - self.project_path = str(_LY_PROJECT_PATH) + self.project_path = str(_O3DE_PROJECT_PATH) else: self.project_path = Path(project_path) @@ -213,7 +213,7 @@ class Window(QtWidgets.QDialog): self.matOutputPathComboBox = self.createComboBox(str(_MAT_OUTPUT_PATH)) # self.directoryComboBox = self.createComboBox(QtCore.QDir.currentPath()) - # I changed this to scan the _LY_PROJECT + # I changed this to scan the _O3DE_PROJECT # self.sbsarDirectory = self.return_1st_sbsar(Path(self.project_path, 'Assets')).resolve().parent self.sbsarDirectory = QtCore.QDir() self.sbsarDirectory.setCurrent(str(_PROJECT_ASSET_PATH)) @@ -672,13 +672,13 @@ class Window(QtWidgets.QDialog): # if you want relative paths here is a better way # first of all, assume we know the project we are in - #_LY_PROJECT_PATH + #_O3DE_PROJECT_PATH texture_output_path = Path(self.texRenderPathComboBox.currentText()).resolve() rel_tex_path = None for p in texture_output_path.parts: - if _LY_PROJECT == p: - index = texture_output_path.parts.index(_LY_PROJECT) + if _O3DE_PROJECT == p: + index = texture_output_path.parts.index(_O3DE_PROJECT) rel_tuple = texture_output_path.parts[index + 1:] rel_tex_path = Path(*list(rel_tuple)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py index 4763bf430c..441110b8d4 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbs_to_sbsar.py @@ -47,7 +47,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -68,10 +68,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py index 2a26f13be2..9652b8990a 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_info.py @@ -47,7 +47,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -62,7 +62,7 @@ _LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = os.getenv(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = os.getenv(ENVAR_DCCSI_GDEBUG, False) # global space debug flag _DCCSI_DEV_MODE = os.getenv(ENVAR_DCCSI_DEV_MODE, False) @@ -88,10 +88,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py index 31e2c81a57..6285ea0335 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_render.py @@ -45,7 +45,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -66,10 +66,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py index 74293a8d77..8ffdf30e35 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/sbsar_utils.py @@ -28,7 +28,7 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE from dynaconf import settings from pathlib import Path -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, settings.DCCSI_GDEBUG) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, settings.DCCSI_DEV_MODE) _MODULENAME = 'DCCsi.SDK.substance.builder.sbsar_utils' @@ -190,15 +190,15 @@ if __name__ == "__main__": _SYNTH_ENV_DICT = synthetic_env.stash_env() from azpy.constants import ENVAR_DCCSIG_PATH - from azpy.constants import ENVAR_LY_PROJECT_PATH + from azpy.constants import ENVAR_O3DE_PROJECT_PATH # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] # use DCCsi as the project path for this test - _LY_PROJECT_PATH = _PATH_DCCSI + _O3DE_PROJECT_PATH = _PATH_DCCSI - _PROJECT_ASSETS_PATH = Path(_LY_PROJECT_PATH, 'Assets').resolve() + _PROJECT_ASSETS_PATH = Path(_O3DE_PROJECT_PATH, 'Assets').resolve() _PROJECT_MATERIALS_PATH = Path(_PROJECT_ASSETS_PATH, 'Materials').resolve() # this will combine two parts into a single path (object) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py index af001902ed..0407a1db08 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/substance_tools.py @@ -46,7 +46,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -67,10 +67,10 @@ _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) # grab a specific path from the base_env _PATH_DCCSI = _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # build some reuseable path parts -_PATH_MOCK_ASSETS = Path(_LY_PROJECT_PATH, 'Assets').norm() +_PATH_MOCK_ASSETS = Path(_O3DE_PROJECT_PATH, 'Assets').norm() _PATH_MOCK_SUBLIB = Path(_PATH_MOCK_ASSETS, 'SubstanceSource').norm() _PATH_MOCK_SBS = Path(_PATH_MOCK_SUBLIB, 'sbs').norm() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py index d37a9cb5ec..35c970e711 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/Substance/builder/watchdog/__init__.py @@ -53,7 +53,7 @@ import pysbs.context as pysbs_context # ------------------------------------------------------------------------- # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ @@ -71,8 +71,8 @@ _LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) from collections import OrderedDict _SYNTH_ENV_DICT = OrderedDict() _SYNTH_ENV_DICT = azpy.synthetic_env.stash_env(_SYNTH_ENV_DICT) -_LY_DEV = _SYNTH_ENV_DICT[ENVAR_LY_DEV] -_LY_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] +_O3DE_DEV = _SYNTH_ENV_DICT[ENVAR_O3DE_DEV] +_O3DE_PROJECT_PATH = _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] # ------------------------------------------------------------------------- @@ -90,7 +90,7 @@ class MyHandler(PatternMatchingEventHandler): """ self.outputName = event.src_path.split(".sbsar")[0].split("/")[-1] self.outputCookPath = event.src_path.split(self.outputName) - self.outputRenderPath = Path(_LY_PROJECT_PATH, 'Assets', 'Textures', 'Substance').norm() + self.outputRenderPath = Path(_O3DE_PROJECT_PATH, 'Assets', 'Textures', 'Substance').norm() _LOGGER.debug(self.outputCookPath, self.outputName, self.outputRenderPath) pysbs_batch.sbsrender_info(input=event.src_path) diff --git a/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/3dsMax/stub similarity index 65% rename from AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/3dsMax/stub index 1f49f7111b..d365a5f2c9 100644 --- a/AutomatedTesting/Gem/PythonTests/editor_test_testing/conftest.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/3dsMax/stub @@ -1,8 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python """ 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 """ - -pytest_plugins = ["pytester"] +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/_init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/_init__.py new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/_init__.py @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/main.py new file mode 100644 index 0000000000..b08991f63e --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/AddOns/MaterialExporter/main.py @@ -0,0 +1,21 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\Blender\\AddOns\\MaterialExporter\\main.py + +""" A in Blender tool for exporting BRDF materials as O3DE Atom StandardPBR +""" + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + print('MaterialExporter.main() not implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py new file mode 100644 index 0000000000..b0f67b1034 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/config.py @@ -0,0 +1,11 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/settings.json similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/stub rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/settings.json diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py new file mode 100644 index 0000000000..b0f67b1034 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Blender/start.py @@ -0,0 +1,11 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Houdini/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Houdini/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Houdini/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Marmoset/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Marmoset/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Marmoset/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/Prefs/icons/MayaStartupImage.png b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/Prefs/icons/MayaStartupImage.png new file mode 100644 index 0000000000..8f5d8290a8 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/Prefs/icons/MayaStartupImage.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:083ab199e273431963fc5f80bb80b7d1b1d428f7b12a5180d7199a7431291982 +size 311865 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/plugins/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/plugins/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/scripts/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/scripts/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/siteDir/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/2020/siteDir/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Help/HelpStub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Help/HelpStub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Projects/default/workspace.mel b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Projects/default/workspace.mel new file mode 100644 index 0000000000..c6474c4414 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Projects/default/workspace.mel @@ -0,0 +1,36 @@ +//Maya 2016 Project Definition + +workspace -fr "fluidCache" "cache/nCache/fluid"; +workspace -fr "images" "images"; +workspace -fr "offlineEdit" "scenes/edits"; +workspace -fr "furShadowMap" "renderData/fur/furShadowMap"; +workspace -fr "iprImages" "renderData/iprImages"; +workspace -fr "renderData" "renderData"; +workspace -fr "scripts" "scripts"; +workspace -fr "fileCache" "cache/nCache"; +workspace -fr "eps" "data"; +workspace -fr "shaders" "renderData/shaders"; +workspace -fr "3dPaintTextures" "sourceimages/3dPaintTextures"; +workspace -fr "translatorData" "data"; +workspace -fr "mel" "scripts"; +workspace -fr "furFiles" "renderData/fur/furFiles"; +workspace -fr "OBJ" "data"; +workspace -fr "particles" "cache/particles"; +workspace -fr "scene" "scenes"; +workspace -fr "furEqualMap" "renderData/fur/furEqualMap"; +workspace -fr "sourceImages" "sourceimages"; +workspace -fr "furImages" "renderData/fur/furImages"; +workspace -fr "clips" "clips"; +workspace -fr "depth" "renderData/depth"; +workspace -fr "movie" "movies"; +workspace -fr "audio" "sound"; +workspace -fr "bifrostCache" "cache/bifrost"; +workspace -fr "autoSave" "autosave"; +workspace -fr "mayaAscii" "scenes"; +workspace -fr "move" "data"; +workspace -fr "sound" "sound"; +workspace -fr "diskCache" "data"; +workspace -fr "illustrator" "data"; +workspace -fr "mayaBinary" "scenes"; +workspace -fr "templates" "assets"; +workspace -fr "furAttrMap" "renderData/fur/furAttrMap"; diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/IBLbaker_brdf_lut.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/IBLbaker_brdf_lut.dds new file mode 100644 index 0000000000..81e62781b4 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/IBLbaker_brdf_lut.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:646b6d93b2c672bbbbcb46af1bfcaf26ca37c8a0c2b218989b145417bb6b7c93 +size 262272 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_diffuse.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_diffuse.dds new file mode 100644 index 0000000000..980477af4b --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_diffuse.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:86209db0389b152c709d529e9d5705219b44bfbf712a26788a5c2ab0adc0c373 +size 98452 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_specular.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_specular.dds new file mode 100644 index 0000000000..fd6e1f99fc --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_lighting_specular.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bd95898a04b81b80b095dbef34523f3b70a8c14bc9f82116f732ab648f25658b +size 2096788 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_skybox.dds b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_skybox.dds new file mode 100644 index 0000000000..304aaa0a1f --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/PBR/ly_cubempas/artist_workshop_4k_skybox.dds @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a85a15d5c60102f414b4ad03604dbd1c78e3d1c1fe445f60db158b2c069f792d +size 25165972 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/MayaStartupImage.psd b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/MayaStartupImage.psd new file mode 100644 index 0000000000..8d630620ca --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/MayaStartupImage.psd @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1318f73ca32ec56dfeb0233679504f6fc723081f0cafa8e7e2d0517b878defd1 +size 2000893 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/SourceImages/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/workspace.mel b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/workspace.mel new file mode 100644 index 0000000000..2cbf02f1aa --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Resources/workspace.mel @@ -0,0 +1,69 @@ +//Maya 2016 Project Definition + +workspace -fr "fluidCache" "mayaData/cache/nCache/fluid"; +workspace -fr "JT_DC" "mayaData/Trans/JT"; +workspace -fr "CATIAV4_DC" "mayaData/Trans/CATIAV4"; +workspace -fr "images" "mayaData/Images"; +workspace -fr "offlineEdit" "ArtSource/Maya"; +workspace -fr "STEP_DC" "mayaData/Trans/STEP"; +workspace -fr "furShadowMap" "mayaData/renderData/fur/furShadowMap"; +workspace -fr "SPF_DCE" "mayaData/Trans/SPF"; +workspace -fr "scripts" "mayaData/Scripts"; +workspace -fr "CATIAV5_DC" "mayaData/Trans/CATIAV5"; +workspace -fr "DAE_FBX" "mayaData/Trans/DAE_FBX"; +workspace -fr "shaders" "mayaData/renderData/shaders"; +workspace -fr "furFiles" "mayaData/renderData/fur/furFiles"; +workspace -fr "OBJ" "mayaData/OBJ"; +workspace -fr "FBX export" "mayaData/Trans/FBX_export"; +workspace -fr "furEqualMap" "mayaData/renderData/fur/furEqualMap"; +workspace -fr "Autodesk Packet File" "mayaData/Trans"; +workspace -fr "DAE_FBX export" "mayaData/Trans/DAE_FBX_export"; +workspace -fr "SPF_DC" "mayaData/Trans/SPF"; +workspace -fr "movie" "mayaData/movies"; +workspace -fr "DXF_DCE" "mayaData/Trans/DXF"; +workspace -fr "move" "mayaData/move"; +workspace -fr "mayaAscii" "ArtSource"; +workspace -fr "autoSave" "mayaData"; +workspace -fr "sound" "mayaData/Sounds"; +workspace -fr "mayaBinary" "ArtSource"; +workspace -fr "ZPR_DCE" "mayaData/Trans/ZPR"; +workspace -fr "STL_DCE" "mayaData/Trans/STL"; +workspace -fr "iprImages" "mayaData/renderData/iprImages"; +workspace -fr "PhysX" "mayaData/Trans/Physx"; +workspace -fr "DXF_DC" "mayaData/Trans/DXF"; +workspace -fr "FBX" "mayaData/Trans/FBX"; +workspace -fr "studioImport" "mayaData/Trans"; +workspace -fr "UG_DCE" "mayaData/Trans/UG"; +workspace -fr "renderData" "mayaData/renderData"; +workspace -fr "fileCache" "mayaData/cache/nCache"; +workspace -fr "eps" "mayaData/EPS"; +workspace -fr "Fbx" "Objects"; +workspace -fr "3dPaintTextures" "mayaData/images/3dPaintTextures"; +workspace -fr "translatorData" "mayaData"; +workspace -fr "mel" "mayaData/Scripts/Mel"; +workspace -fr "particles" "mayaData/cache/particles"; +workspace -fr "IV_DC" "mayaData/Trans/IV"; +workspace -fr "scene" "ArtSource"; +workspace -fr "DWG_DCE" "mayaData/Trans/DWG"; +workspace -fr "MayaCryExport" "Objects"; +workspace -fr "sourceImages" "ArtSource/Textures"; +workspace -fr "furImages" "mayaData/renderData/fur/furImages"; +workspace -fr "clips" "mayaData/clips"; +workspace -fr "PTC_DC" "mayaData/Trans/PTC"; +workspace -fr "STL_DC" "mayaData/Trans/STL"; +workspace -fr "IPT_DC" "mayaData/Trans/IPT"; +workspace -fr "CSB_DC" "mayaData/Trans/CSB"; +workspace -fr "SW_DC" "mayaData/Trans/SW"; +workspace -fr "depth" "mayaData/renderData/depth"; +workspace -fr "audio" "mayaData/Sounds"; +workspace -fr "DWG_DC" "mayaData/Trans/DWG"; +workspace -fr "bifrostCache" "mayaData/cache/bifrost"; +workspace -fr "IGES_DCE" "mayaData/Trans/IGES"; +workspace -fr "Alembic" "mayaData/Trans/Alembic"; +workspace -fr "illustrator" "mayaData/AI"; +workspace -fr "diskCache" "mayaData"; +workspace -fr "UG_DC" "mayaData/Trans/UG"; +workspace -fr "templates" "mayaData/assets"; +workspace -fr "OBJexport" "mayaData/Trans/Obj"; +workspace -fr "furAttrMap" "mayaData/renderData/fur/furAttrMap"; +workspace -fr "IGES_DC" "mayaData/Trans/IGES"; diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Mel/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Mel/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/max_materials.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub_util.py old mode 100755 new mode 100644 similarity index 51% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/max_materials.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub_util.py index a67c39910e..1dc43d8485 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/SDK/PythonTools/DCC_Material_Converter/max_materials.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/Python/stub_util.py @@ -1,18 +1,12 @@ +# coding:utf-8 +#!/usr/bin/python +# # 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 MaxPlus -import sys - - -def get_material_information(): - for mesh_object in MaxPlus.Core.GetRootNode().Children: - print('Object---> {}'.format(mesh_object)) - - -get_material_information() +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/constants.py new file mode 100644 index 0000000000..6ed8297ab9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/constants.py @@ -0,0 +1,32 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- + +""" +Module Documentation: + DccScriptingInterface:: Tools//maya//scripts//constants.py + +This module is mainly a bunch of commony used constants, and default strings +So we can make an update here once that is used elsewhere +""" +# ------------------------------------------------------------------------- +# built-ins +# none + +# -- External Python modules + +# -- DCCsi Extension Modules +#import azpy + +# -- maya imports +# none +# ------------------------------------------------------------------------- +OBJ_DCCSI_MAINMENU = 'O3deDCCsiMainMenu' +TAG_DCCSI_MAINMENU = 'DCCsi (O3DE:Atom)' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_callbacks.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_callbacks.py new file mode 100644 index 0000000000..4a2b756a5e --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_callbacks.py @@ -0,0 +1,201 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_callbacks.py + +This module manages a set of predefined callbacks for maya +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +import os +import sys +import logging as _logging +# -- External Python modules +from box import Box +# maya imports +import maya.cmds as mc +import maya.api.OpenMaya as om +# -- DCCsi Extension Modules +from azpy.constants import * +import azpy.dcc.maya +azpy.dcc.maya.init() # <-- should have already run? +import azpy.dcc.maya.callbacks.event_callback_handler as azEvCbH +import azpy.dcc.maya.callbacks.node_message_callback_handler as azNdMsH +# Node Message Callback Setup +import azpy.dcc.maya.callbacks.on_shader_rename as oSR +from set_defaults import set_defaults +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, True) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, True) + +_MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_callbacks' + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.debug('Invoking:: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global scope callbacks, set up set and initialize all to None +# To Do: should callback initialization use data-driven settings? +# To Do: should we move callback initialization to a sub-module? +# To Do: move the callback key like 'NewSceneOpened' here (instead of None) +# ^ this would provide ability to loop through and replace key with CB object + +_G_CALLBACKS = Box(box_dots=True) # global scope container +_G_PRIMEKEY = 'DCCsi_callbacks' +_G_CALLBACKS[_G_PRIMEKEY] = True # required prime key + + +# ------------------------------------------------------------------------- +def init_callbacks(_callbacks=_G_CALLBACKS): + # store as a dict (Box is a fancy dict) + _callbacks[_G_PRIMEKEY] = True # required prime key + + # signature dict['callback key'] = ('CallBack'(type), func, callbackObj) + _callbacks['on_new_file'] = ['NewSceneOpened', set_defaults, None] + _callbacks['new_scene_fix_paths'] = ['NewSceneOpened', install_fix_paths, None] + _callbacks['post_scene_fix_paths'] = ['PostSceneRead', install_fix_paths, None] + _callbacks['workspace_changed'] = ['workspaceChanged', update_workspace, None] + _callbacks['quit_app'] = ['quitApplication', uninstall_callbacks, None] + + # nodeMessage style callbacks + # fire a function + _func_00 = oSR.on_shader_rename_rename_shading_group + # using a nodeMessage callback trigger + _cb_00 = om.MNodeMessage.addNameChangedCallback + # all nodeMessage type callbacks can use 'nodeMessageType' key + _callbacks['shader_rename'] = ['nodeMessageType', (_func_00, _cb_00), None] + + return _callbacks +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def uninstall_callbacks(): + """Bulk uninstalls hte globally defined set of callbacks: + _G_callbacks""" + + global _G_CALLBACKS + + _LOGGER.debug('uninstall_callbacks() fired') + + for key, value in _G_CALLBACKS: + if value[2] is not None: # have a cb + value[2].uninstall() # so uninstall it + else: + _LOGGER.warning('No callback in: key {0}, value:{1}' + ''.format(key, value)) + _G_CALLBACKS = None + _LOGGER.info('DCCSI CALLBACKS UNINSTALLED ... EXITING') + return _G_CALLBACKS +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def install_callbacks(_callbacks=_G_CALLBACKS): + """Bulk installs the globally defined set of callbacks: + _G_callbacks""" + + _LOGGER.debug('install_callback_set() fired') + + _callbacks = init_callbacks(_callbacks) + + # we initialized the box with this so pop it + if 'box_dots' in _callbacks: + _callbacks.pop('box_dots') + + # don't pass anything but carefully considered dict + if _G_PRIMEKEY in _callbacks: + _primekey = _callbacks.pop(_G_PRIMEKEY) + else: + _LOGGER.error('No prime key, use a correct dictionary') + #To Do: implement error handling and return codes + return _callbacks[None] + + for key, value in _G_CALLBACKS.items(): + # we popped the prime key should the rest should be safe + if value[0] != 'nodeMessageType': + # set callback up + _cb = azEvCbH.EventCallbackHandler(value[0], + value[1]) + # ^ installs by default + # stash it back into managed dict + value[2] = _cb + # value[2].install() + else: + # set up callback, value[1] should be tupple(func, trigger) + _cb = azNdMsH.NodeMessageCallbackHandler(value[1][0], + value[1][1]) + # ^ installs by default + # stash it back into managed dict + value[2] = _cb + # value[2].install() + + return _callbacks +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def install_fix_paths(foo=None): + """Installs and triggers a fix paths module. + This can repair broken reference paths in shaders""" + global _fix_paths + _fix_paths = None + + _LOGGER.debug('install_fix_paths() fired') + + # if we don't have it already, this function is potentially triggered + # by a callback, so we don't need to keep importing it. + try: + _fix_paths + reload(_fix_paths) + except Exception as e: + try: + import fixPaths as _fix_paths + except Exception as e: + # To Do: not implemented yet + _LOGGER.warning('NOT IMPLEMENTED: {0}'.format(e)) + + # if we have it, use it + if _fix_paths: + return _fix_paths.main() + else: + # To Do: implement error handling and return codes + return 1 +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def update_workspace(foo=None): + """Forces and update of the workspace (workspace.mel)""" + _LOGGER.debug('update_workspace() fired') + result = mc.workspace(update=True) + return result +# ------------------------------------------------------------------------- + +# install and init callbacks on an import obj +_G_CALLBACKS = install_callbacks(_G_CALLBACKS) + +# ========================================================================== +# Module Tests +#========================================================================== +if __name__ == '__main__': + _G_CALLBACKS = install_callbacks(_G_CALLBACKS) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_defaults.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_defaults.py new file mode 100644 index 0000000000..639b03c297 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_defaults.py @@ -0,0 +1,93 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_pref_defaults.py + +This module manages a predefined set of prefs for maya +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +import os +import sys +# -- External Python modules + +# -- DCCsi Extension Modules +import azpy +from azpy.constants import * + +# -- maya imports +import maya.cmds as mc +import maya.mel as mm + +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_defaults' + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.debug('Invoking:: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def set_defaults(units='meter'): + """This method will make defined settings changes to Maya prefs, + to better configure maya to work with Lumberyard""" + # To Do: make this data-driven env/settings, game teams should be able + # to opt out and/or set their prefered configuration. + + _LOGGER.debug('set_defaults_lumberyard() fired') + + # set up default units ... this should be moved to bootstrap config + _LOGGER.info('Default, 1 Linear Game Unit in Lumberyard == 1 Meter' + ' in Maya content. Setting default linear units to Meters' + ' (user can change to other units in the preferences)') + + result = mc.currentUnit(linear=units) + + # set up grid defaults + _LOGGER.info('Setting Grid defaults, to match default unit scale.' + '(user can change grid config manually') + try: + mc.grid(size=32, spacing=1, divisions=10) + except Exception as e: + _LOGGER.warning('{0}'.format(e)) + + # viewFit + _LOGGER.info('Changing default mc.viewFit') + try: + mc.viewFit() + except Exception as e: + _LOGGER.warning('{0}'.format(e)) + + # some mel commands + _LOGGER.info('Changing sersp camera clipping planes') + try: + mm.eval(str(r'setAttr "perspShape.nearClipPlane" 0.01;')) + mm.eval(str(r'setAttr "perspShape.farClipPlane" 1000;')) + except Exception as e: + _LOGGER.warning('{0}'.format(e)) + + # set up fixPaths + _LOGGER.info('~ Setting up fixPaths in default scene') + + return 0 +# ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_menu.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_menu.py new file mode 100644 index 0000000000..e417324d0b --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_menu.py @@ -0,0 +1,88 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_menu.py + +This module creates and manages a DCCsi mainmenu +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +# none + +# -- External Python modules +# none + +# -- DCCsi Extension Modules +import azpy +from constants import OBJ_DCCSI_MAINMENU +from constants import TAG_DCCSI_MAINMENU + +# -- maya imports +import pymel.core as pm +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_MODULENAME = r'DCCsi.SDK.Maya.Scripts.set_menu' + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.debug('Invoking:: {0}.'.format({_MODULENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def menu_cmd_test(): + _LOGGER.info('test_func(), is TESTING main menu') + return +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +def set_main_menu(obj_name=OBJ_DCCSI_MAINMENU, label=TAG_DCCSI_MAINMENU): + _main_window = pm.language.melGlobals['gMainWindow'] + + _menu_obj = obj_name + _menu_label = label + + # check if it already exists and remove (so we don't duplicate) + if pm.menu(_menu_obj, label=_menu_label, exists=True, parent=_main_window): + pm.deleteUI(pm.menu(_menu_obj, e=True, deleteAllItems=True)) + + # create the main menu object + _custom_tools_menu = pm.menu(_menu_obj, + label=_menu_label, + parent=_main_window, + tearOff=True) + + # make a dummpy sub-menu + pm.menuItem(label='Menu Item Stub', + subMenu=True, + parent=_custom_tools_menu, + tearOff=True) + + # make a dummy menu item to test + pm.menuItem(label='Test', command=pm.Callback(menu_cmd_test)) + return _custom_tools_menu + +# ========================================================================== +# Run as LICENSE +#========================================================================== +if __name__ == '__main__': + + _custom_menu = set_main_menu() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_shelf.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_shelf.py new file mode 100644 index 0000000000..1bee05afe8 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/set_shelf.py @@ -0,0 +1,130 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +""" +Module Documentation: + DccScriptingInterface:: SDK//maya//scripts//set_shelf.py + +This module manages a custom shelf in maya for the DCCsi +Reference: https://gist.github.com/vshotarov/1c3176fe9e38dcaadd1e56c2f15c95d9 +""" +# ------------------------------------------------------------------------- +# -- Standard Python modules +# none + +# -- External Python modules +# none + +# -- DCCsi Extension Modules +# none + +# -- Maya Extension Modules +import maya.cmds as mc +# ------------------------------------------------------------------------- + +def _null(*args): + pass + +# ------------------------------------------------------------------------- +class customShelf(_Custom_Shelf): + '''This is an example shelf.''' + + def build(self): + self.add_button(label="button1") + self.add_button("button2") + self.add_button("popup") + p = mc.popupMenu(b=1) + self.add_menu_item(p, "popupMenuItem1") + self.add_menu_item(p, "popupMenuItem2") + sub = self.add_submenu(p, "subMenuLevel1") + self.add_menu_item(sub, "subMenuLevel1Item1") + sub2 = self.add_submenu(sub, "subMenuLevel2") + self.add_menu_item(sub2, "subMenuLevel2Item1") + self.add_menu_item(sub2, "subMenuLevel2Item2") + self.add_menu_item(sub, "subMenuLevel1Item2") + self.add_menu_item(p, "popupMenuItem3") + self.add_button("button3") +# ------------------------------------------------------------------------- + + + +class _Custom_Shelf(): + '''A simple class to build custom shelves in maya. + The build method is empty and an inheriting class should override''' + + def __init__(self, name="DCCsi", icon_path=""): + self._name = name + + self._icon_path = icon_path + + self._label_background_color = (0, 0, 0, 0) + self._label_colour = (.9, .9, .9) + + self._clean_old_shlef() + + mc.setParent(self._name) + + self.build() + + def build(self): + '''Override this method in custom class. + Otherwise, nothing is added to the shelf.''' + pass + + def add_button(self, + label='', + icon="commandButton.png", + command=_null, + doubleCommand=_null): + '''Adds a shelf button with the specified label, + command, double click command and image.''' + mc.setParent(self._name) + if icon: + icon = self._icon_path + icon + mc.shelfButton(width=37, height=37, + image=icon, + label=label, + command=command, + doubleClickCommand=doubleCommand, + imageOverlayLabel=label, + overlayLabelBackColor=self._label_background_color, + overlayLabelColor=self._label_colour) + + def add_menu_item(self, parent, label, command=_null, icon=""): + '''Adds a shelf button with the specified label, + command, double click command and image.''' + if icon: + icon = self._icon_path + icon + return mc.menuItem(p=parent, l=label, c=command, i="") + + def add_submenu(self, parent, label, icon=None): + '''Adds a sub menu item with the specified label and icon + to the specified parent popup menu.''' + if icon: + icon = self._icon_path + icon + return mc.menuItem(p=parent, l=label, i=icon, subMenu=1) + + def _clean_old_shlef(self): + '''Checks if the shelf exists and empties it if it does or + creates it if it does not.''' + if mc.shelfLayout(self._name, ex=1): + if mc.shelfLayout(self._name, q=1, ca=1): + for each in mc.shelfLayout(self._name, q=1, ca=1): + mc.deleteUI(each) + else: + mc.shelfLayout(self._name, p="ShelfLayout") + + +# ========================================================================== +# Module Tests +# ========================================================================== +if __name__ == '__main__': + customShelf() + pass diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py new file mode 100644 index 0000000000..c68eb8d256 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Scripts/userSetup.py @@ -0,0 +1,325 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +from __future__ import unicode_literals + +""" +This module fullfils the maya bootstrap pattern as described in their docs +https://tinyurl.com/y2aoz8es + +Pattern is similar to Lumberyard Editor\\Scripts\\bootstrap.py + +For now the proper way to initiate Maya boostrapping the DCCsi, is to use +the provided env and launcher bat files. + +If you are developing for the DCCsi you can use this launcher to start Maya: +DccScriptingInterface\\Launchers\\Windows\\Launch_Maya_2020.bat" + +To Do: ATOM-5861 +""" +__project__ = 'DccScriptingInterface' + +# it is really hard to debug userSetup bootstrapping +# this enables some rudimentary logging for debugging +_BOOT_INFO = True + +# ------------------------------------------------------------------------- +# built in's +import os +import sys +import site +import inspect +import traceback +import logging as _logging + +# -- DCCsi Extension Modules +import azpy +from azpy.constants import * +from azpy.env_bool import env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE + +# To Do: needs to be updated to use dynaconf and config.py +from azpy.env_base import _BASE_ENVVAR_DICT + +# -- maya imports +import maya.cmds as cmds +import maya.mel as mel +#from pymel.all import * +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global space +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_DEV_MODE = True # force true for debugger testing + +_ORG_TAG = r'Amazon::Lumberyard' +_APP_TAG = r'DCCsi' +_TOOL_TAG = r'SDK.Maya.Scripts.userSetup' +_TYPE_TAG = r'entrypoint' # bootstrap + +_MODULENAME = str('{0}.{1}'.format(_APP_TAG, _TOOL_TAG)) + +_LOGGER = azpy.initialize_logger(_MODULENAME, default_log_level=int(20)) +_LOGGER.info('Initializing: {0}.'.format({_MODULENAME})) +_LOGGER.info('DCCSI_GDEBUG: {0}.'.format({_DCCSI_GDEBUG})) +_LOGGER.info('DCCSI_DEV_MODE: {0}.'.format({_DCCSI_DEV_MODE})) + +# flag to turn off setting up callbacks, until they are fully implemented +# To Do: consider making it a settings option to define and enable/disable +_G_LOAD_CALLBACKS = True # couple bugs, couple NOT IMPLEMENTED +_LOGGER.info('DCCSI_MAYA_SET_CALLBACKS: {0}.'.format({_G_LOAD_CALLBACKS})) + +# early attach WingIDE debugger (can refactor to include other IDEs later) +if _DCCSI_DEV_MODE: + from azpy.test.entry_test import connect_wing + foo = connect_wing() +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# To Do REMOVE this block and replace with dev module +# debug prints, To Do: this should be moved to bootstrap config +#_G_DEBUGGER = os.getenv(ENVAR_DCCSI_GDEBUGGER, "WING") + +#if _DCCSI_DEV_MODE: + #if _G_DEBUGGER == "WING": + #_LOGGER.info('{0}'.format('-' * 74)) + #_LOGGER.info('Developer Debug Mode: {0}, Basic debugger: {1}'.format(_G_DEBUG, _G_DEBUGGER)) + #try: + #_LOGGER.info('Attempting to start basic WING debugger') + #import azpy.lmbr.test + + #_LOGGER.info('Package Imported: azpy.test') + #ouput = azpy.entry_test.main(verbose=False, + #connectDebugger=True, + #returnOuput=_G_DEBUG) + #_LOGGER.info(ouput) + #pass + #except Exception as e: + #_LOGGER.info("Error: azpy.test, entry_test (didn't perform)") + #_LOGGER.info("Exception: {0}".format(e)) + #pass + #elif _G_DEBUGGER == "PYCHARM": + ## https://github.com/juggernate/PyCharm-Maya-Debugging + #_LOGGER.info('{0}'.format('-' * 74)) + #_LOGGER.info('Developer Debug Mode: {0}, Basic debugger: {1}'.format(_G_DEBUG, _G_DEBUGGER)) + #sys.path.append('C:\Program Files\JetBrains\PyCharm 2019.1.3\debug-eggs\pydevd-pycharm.egg') + #try: + #_LOGGER.info('Attempting to start basic PYCHARM debugger') + ## Inside Maya Python Console (Tip: add to a shelf button for quick access) + #import pydevd + + #_LOGGER.info('Package Imported: pydevd') + #pydevd.settrace('localhost', port=7720, suspend=False) + #_LOGGER.info('PYCHARM Debugger Attach Success!!!') + ## To disconnect run: + ## pydevd.stoptrace() + #pass + #except Exception as e: + #_LOGGER.info("Error: pydevd.settrace (didn't perform)") + #_LOGGER.info("Exception: {0}".format(e)) + #pass + #else: + #pass +## ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# validate access to the DCCsi and it's Lib site-packages +# bootstrap site-packages by version +from azpy.constants import PATH_DCCSI_PYTHON_LIB_PATH + +try: + os.path.exists(PATH_DCCSI_PYTHON_LIB_PATH) + site.addsitedir(PATH_DCCSI_PYTHON_LIB_PATH) + _LOGGER.info('azpy 3rdPary site-packages: is: {0}'.format(PATH_DCCSI_PYTHON_LIB_PATH)) +except Exception as e: + _LOGGER.error('ERROR: {0}, {1}'.format(e, PATH_DCCSI_PYTHON_LIB_PATH)) + raise e + +# 3rdparty +from unipath import Path +from box import Box +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +# Maya is frozen +#_MODULE_PATH = Path(__file__) +# https://tinyurl.com/y49t3zzn +# module path when frozen +_MODULE_FILEPATH = os.path.abspath(inspect.getfile(inspect.currentframe())) +_MODULE_PATH = os.path.dirname(_MODULE_FILEPATH) +if _BOOT_INFO: + _LOGGER.debug('Boot: CWD: {}'.format(os.getcwd())) + _LOGGER.debug('Frozen: _MODULE_FILEPATH: {}'.format(_MODULE_FILEPATH)) + _LOGGER.debug('Frozen: _MODULE_PATH: {}'.format(_MODULE_PATH)) + _LOGGER.debug('Module __name__: {}'.format(__name__)) +# root: INFO: Module __name__: __main__ + +_LOGGER.info('_MODULENAME: {}'.format(_MODULENAME)) + +# ------------------------------------------------------------------------- +# check some env var tags (fail if no, likely means no proper code access) +_STR_ERROR_ENVAR = "Envar 'key' does not exist in base_env: {0}" +_DCCSI_TOOLS_PATH = None +# To Do: needs to be updated to use dynaconf and config.py +try: + _DCCSI_TOOLS_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_TOOLS_PATH] +except Exception as e: + _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_DCCSI_TOOLS_PATH])) + +_O3DE_PROJECT_PATH = None +try: + _O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] +except Exception as e: + _LOGGER.critical(_STR_ERROR_ENVAR.format(_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH])) + +# check some env var tags (fail if no, likely means no proper code access) +_O3DE_DEV = _BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] +_O3DE_DCCSIG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] +_O3DE_DCCSI_LOG_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] +_O3DE_AZPY_PATH = _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# To Do: implement data driven config +# Currently not used, but will be where we store the ordered dict +# which is parsed from the project bootstrapping config files. +_G_app_config = {} + +# global scope maya callbacks container +_G_callbacks = Box(box_dots=True) # global scope container + +# used to store fixPaths in the global scope +_fix_paths = None +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# add appropriate common tools paths to the maya environment variables +def startup(): + """Early starup execution before mayautils.executeDeferred(). + Some things like UI and plugins should be defered to avoid failure""" + _LOGGER.info('startup() fired') + + # get known paths + _KNOWN_PATHS = site._init_pathinfo() + + if os.path.isdir(_DCCSI_TOOLS_PATH): + site.addsitedir(_DCCSI_TOOLS_PATH, _KNOWN_PATHS) + try: + import azpy.test + _LOGGER.info('SUCCESS, import azpy.test') + except Exception as e: + _LOGGER.warning('startup(), could not import azpy.test') + + _LOGGER.info('startup(), COMPLETE') + return 0 +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# verify Shared\Python exists and add it as a site dir. Begin imports and config. +def post_startup(): + """Allows for a defered execution startup sequence""" + + _LOGGER.info('post_startup() fired') + + # plugins, To Do: these should be moved to bootstrapping config + try: + maya.cmds.loadPlugin("dx11Shader") + except Exception as e: + _LOGGER.error(e) # not a hard failure + + # Lumberyard DCCsi environment ready or error out. + try: + import azpy.dcc.maya + _LOGGER.info('Python module imported: azpy.dcc.maya') + except Exception as e: + _LOGGER.error(e) + _LOGGER.error(traceback.print_exc()) + return 1 + + # Dccsi azpy maya ready or error out. + try: + azpy.dcc.maya.init() + _LOGGER.info('SUCCESS, azpy.dcc.maya.init(), code accessible.') + except Exception as e: + _LOGGER.error(e) + _LOGGER.error(traceback.print_exc()) + return 1 + + # callbacks, To Do: these should also be moved to the bootstrapping config + # Defered startup after the Ui is running. + _G_CALLBACKS = Box(box_dots=True) # this just ensures a global scope container + if _G_LOAD_CALLBACKS: + from set_callbacks import _G_CALLBACKS + # ^ need to hold on to this as the install repopulate set + + # this ensures the fixPaths callback is loaded + # even when the other global callbacks are disabled + from set_callbacks import install_fix_paths + install_fix_paths() + + # set the project workspace + #_O3DE_PROJECT_PATH = _BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] + _project_workspace = os.path.join(_O3DE_PROJECT_PATH, TAG_MAYA_WORKSPACE) + if os.path.isfile(_project_workspace): + try: + # load workspace + maya.cmds.workspace(_O3DE_PROJECT_PATH, openWorkspace=True) + _LOGGER.info('Loaded workspace file: {0}'.format(_project_workspace)) + maya.cmds.workspace(_O3DE_PROJECT_PATH, update=True) + except Exception as e: + _LOGGER.error(e) + else: + _LOGGER.warning('Workspace file not found: {1}'.format(_O3DE_PROJECT_PATH)) + + # Set up Lumberyard, maya default setting + from set_defaults import set_defaults + set_defaults() + + # Setup UI tools + if not maya.cmds.about(batch=True): + _LOGGER.info('Add UI dependent tools') + # wrap in a try, because we haven't implmented it yet + try: + mel.eval(str(r'source "{}"'.format(TAG_O3DE_DCC_MAYA_MEL))) + except Exception as e: + _LOGGER.error(e) + + # manage custom menu in a sub-module + from set_menu import set_main_menu + set_main_menu() + + # To Do: manage custom shelf in a sub-module + + _LOGGER.info('post_startup(), COMPLETE') + _LOGGER.info('DCCsi Bootstrap, COMPLETE') + return 0 +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +if __name__ == '__main__': + try: + # Early startup config. + startup() + + # This allows defered action post boot (atfer UI is active) + from maya.utils import executeDeferred + post = executeDeferred(post_startup) + + except Exception as e: + traceback.print_exc() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Shaders/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Shaders/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Tools/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Tools/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/Tools/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/config.py new file mode 100644 index 0000000000..50d6de8de4 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/config.py @@ -0,0 +1,23 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\DCC\\Maya\\config.py + +"""DccScriptingInterface (DCCsi) +This is the dynamic config (dynaconf) for O3DE Maya DCCsi interface +""" + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + print('Maya.config() not implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/constants.py new file mode 100644 index 0000000000..6695b6185c --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/constants.py @@ -0,0 +1,16 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\DCC\\Maya\\constsants.py + +"""DccScriptingInterface (DCCsi) +This module contains constants for the O3DE Maya DCCsi interface +""" + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.txt new file mode 100644 index 0000000000..57c8317737 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/readme.txt @@ -0,0 +1,78 @@ +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 +------------------------------------------------------------------------------- + +"DccScriptingInterface" aka DCCsi is a Gem for O3DE to extend and interface with dcc tools +in the python ecosystem. Each dcc tool may have it's own specific version of python. +Most are some version of py3+. O3DE provides an install of py3+ and manages package +dependancies with requirements.txt files and the cmake build system. + +However Autodesk Maya still uses a version of py2.7 and so we need an alternate way +to deal with package management for this DCC tool. + +Maya ships with it's own python interpreter called mayapy.exe + +Generally it is located here: +C:\Program Files\Autodesk\Maya2020\bin\mayapy.exe + +The python install and site-packages are here: +C:\Program Files\Autodesk\Maya2020\Python\Lib\site-packages + +A general goal of the DCCsi is be self-maintained, and to not taint the users installed applications of environment. + +So we boostrap additional access to site-packages in our userSetup.py: +"C:\Depot\Lumberyard\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\Maya\Scripts\userSetup.py" + +We don't want users to have to install or use Python2.7 although with maya and possibly other dcc tools we don't have that control. Maya 2020 and earlier versions are still on Python2.7, so instead of forcing another install of python we can just use mayapy to manage extensions. + +Pip may already be installed, you can check like so (your maya install path may be different): + +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m pip --version + +If pip is not available yet for your mayapy. + +First find out where th site-packages is located + +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m site +sys.path = [ + 'C:\\Program Files\\Autodesk\\Maya2020\\bin', + 'C:\\Program Files\\Autodesk\\Maya2020\\bin\\python27.zip', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\DLLs', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\plat-win', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\lib-tk', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python', + 'C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages', +] +USER_BASE: 'C:\\Users\\gallowj\\AppData\\Roaming\\Python' (exists) +USER_SITE: 'C:\\Users\\gallowj\\AppData\\Roaming\\Python\\Python27\\site-packages' (doesn't exist) +ENABLE_USER_SITE: True + +This is the location we are looking for: +C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages + +download get-pip.py and put into the above ^ directory: +https://bootstrap.pypa.io/pip/2.7/get-pip.py + +Put that in the root of site-packages: +C:\\Program Files\\Autodesk\\Maya2020\\Python\\lib\\site-packages\\get-pip.py + +With get-pip module ready, we run it to install pip: +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m get-pip + +Now you should be able to run the following command and verify pip: +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m pip --version +pip 20.3.4 from C:\Users\< you >\AppData\Roaming\Python\Python27\site-packages\pip (python 2.7) + +Now your local maya install is all set up with pip so you can install additional python packages to use in maya. (note: not all packages are compatible with maya) + +Now you will want to run the following file to finish setup... +We have a requirements.txt file with the extension packages we use in the DCCsi. +You'll need the repo/branch path of your O3DE (aka Lumberyard) install. +And you'll need to know where the DCCsi is located, we will install package dependancies there. + +Note: you may need to update the paths below to match your local o3de engine install! + +C:\Program Files\Autodesk\Maya2020\bin>mayapy -m pip install -r C:\Depot\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\SDK\Maya\requirements.txt -t C:\Depot\o3de\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface\3rdParty\Python\Lib\2.x\2.7.x\site-packages diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/requirements.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/requirements.txt new file mode 100644 index 0000000000..ceb5be4dea --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/requirements.txt @@ -0,0 +1,84 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --generate-hashes requirements.txt +# +certifi==2020.6.20 \ + --hash=sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3 \ + --hash=sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41 + # via -r requirements.txt +cachetools==3.1.1 \ + --hash=sha256:428266a1c0d36dc5aca63a2d7c5942e88c2c898d72139fca0e97fdd2380517ae \ + --hash=sha256:8ea2d3ce97850f31e4a08b0e2b5e6c34997d7216a9d2c98e0f3978630d4da69a + # via -r requirements.txt +click==7.1.2 \ + --hash=sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a \ + --hash=sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc + # via + # -r requirements.txt + # pip-tools +dynaconf==3.1.4 \ + --hash=sha256:b2f472d83052f809c5925565b8a2ba76a103d5dc1dbb9748b693ed67212781b9 \ + --hash=sha256:e6f383b84150b70fc439c8b2757581a38a58d07962aa14517292dcce1a77e160 + # via -r requirements.txt +hashids==1.3.1 \ + --hash=sha256:6c3dc775e65efc2ce2c157a65acb776d634cb814598f406469abef00ae3f635c \ + --hash=sha256:8bddd1acba501bfc9306e7e5a99a1667f4f2cacdc20cbd70bcc5ddfa5147c94c + # via -r requirements.txt +pathlib2==2.3.5 \ + --hash=sha256:0ec8205a157c80d7acc301c0b18fbd5d44fe655968f5d947b6ecef5290fc35db \ + --hash=sha256:6cd9a47b597b37cc57de1c05e56fb1a1c9cc9fab04fe78c29acd090418529868 + # via -r requirements.txt +pathlib==1.0.1 \ + --hash=sha256:6940718dfc3eff4258203ad5021090933e5c04707d5ca8cc9e73c94a7894ea9f + # via -r requirements.txt +python-box==3.4.6 \ + --hash=sha256:694a7555e3ff9fbbce734bbaef3aad92b8e4ed0659d3ed04d56b6a0a0eff26a9 \ + --hash=sha256:a71d3dc9dbaa34c8597d3517c89a8041bd62fa875f23c0f3dad55e1958e3ce10 + # via -r requirements.txt +scandir==1.10.0 \ + --hash=sha256:2586c94e907d99617887daed6c1d102b5ca28f1085f90446554abf1faf73123e \ + --hash=sha256:2ae41f43797ca0c11591c0c35f2f5875fa99f8797cb1a1fd440497ec0ae4b022 \ + --hash=sha256:2b8e3888b11abb2217a32af0766bc06b65cc4a928d8727828ee68af5a967fa6f \ + --hash=sha256:2c712840c2e2ee8dfaf36034080108d30060d759c7b73a01a52251cc8989f11f \ + --hash=sha256:4d4631f6062e658e9007ab3149a9b914f3548cb38bfb021c64f39a025ce578ae \ + --hash=sha256:67f15b6f83e6507fdc6fca22fedf6ef8b334b399ca27c6b568cbfaa82a364173 \ + --hash=sha256:7d2d7a06a252764061a020407b997dd036f7bd6a175a5ba2b345f0a357f0b3f4 \ + --hash=sha256:8c5922863e44ffc00c5c693190648daa6d15e7c1207ed02d6f46a8dcc2869d32 \ + --hash=sha256:92c85ac42f41ffdc35b6da57ed991575bdbe69db895507af88b9f499b701c188 \ + --hash=sha256:b24086f2375c4a094a6b51e78b4cf7ca16c721dcee2eddd7aa6494b42d6d519d \ + --hash=sha256:cb925555f43060a1745d0a321cca94bcea927c50114b623d73179189a4e100ac + # via + # -r requirements.txt + # pathlib2 +six==1.15.0 \ + --hash=sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259 \ + --hash=sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced + # via + # -r requirements.txt + # pathlib2 +typing==3.7.4.3 \ + --hash=sha256:1187fb9c82fd670d10aa07bbb6cfcfe4bdda42d6fab8d5134f04e8c4d0b71cc9 \ + --hash=sha256:283d868f5071ab9ad873e5e52268d611e851c870a2ba354193026f2dfb29d8b5 + # via + # -r requirements.txt + # dynaconf +unipath==1.1 \ + --hash=sha256:09839adcc72e8a24d4f76d63656f30b5a1f721fc40c9bcd79d8c67bdd8b47dae \ + --hash=sha256:e6257e508d8abbfb6ddd8ec357e33589f1f48b1599127f23b017124d90b0fff7 + # via -r requirements.txt +qdarkstyle==3.0.2 \ + --hash=sha256:55d149cf5f40ee297397f1818e091118cefb855a4a9c5c38566c47acd2d8c7ae \ + --hash=sha256:7c791535cc20b3cc1e8e1bf6b88dabe53cb0615983df702be83597e73ada2558 + # via -r c:\temp\requirements.txt +qtpy==1.9.0 \ + --hash=sha256:2db72c44b55d0fe1407be8fba35c838ad0d6d3bb81f23007886dc1fc0f459c8d \ + --hash=sha256:fa0b8363b363e89b2a6f49eddc162a04c0699ae95e109a6be3bb145a913190ea + # via + # -r c:\temp\requirements.txt + # qdarkstyle +wincertstore==0.2 \ + --hash=sha256:22d5eebb52df88a8d4014d5cf6d1b6c3a5d469e6c3b2e2854f3a003e48872356 \ + --hash=sha256:780bd1557c9185c15d9f4221ea7f905cb20b93f7151ca8ccaed9714dce4b327a + # via -r requirements.txt diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/settings.json new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/start.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/start.py new file mode 100644 index 0000000000..4d965aec28 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Maya/start.py @@ -0,0 +1,24 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +# DCCsi\\Tools\\DCC\\Maya\\start.py + +"""DccScriptingInterface (DCCsi) +The DCCsi bootstraps tools like Maya with additional code access and extensions. +This module starts up maya in the DCCsi managed synthetic environment context. +""" + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + print('Maya.Start() not implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/custom_shaders/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/custom_shaders/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/ly_presets/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/ly_presets/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/templates/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/resources/templates/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/scripts/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/scripts/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/scripts/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/stub new file mode 100644 index 0000000000..d365a5f2c9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/DCC/Substance/stub @@ -0,0 +1,9 @@ +# coding:utf-8 +#!/usr/bin/python +""" +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 +""" +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Core.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Core.bat similarity index 67% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Core.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Core.bat index 5146d5f3e8..b37170d605 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Core.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Core.bat @@ -31,11 +31,11 @@ echo ~ O3DE DCC Scripting Interface Environment ... echo _____________________________________________________________________ echo. -IF "%DCCSI_LAUNCHERS_PATH%"=="" (set DCCSI_LAUNCHERS_PATH=%~dp0) -echo DCCSI_LAUNCHERS_PATH = %DCCSI_LAUNCHERS_PATH% +IF "%DCCSI_DEV_ENV%"=="" (set DCCSI_DEV_ENV=%~dp0) +echo DCCSI_DEV_ENV = %DCCSI_DEV_ENV% :: add to the PATH -SET PATH=%DCCSI_LAUNCHERS_PATH%;%PATH% +SET PATH=%DCCSI_DEV_ENV%;%PATH% :: Constant Vars (Global) :: global debug flag (propogates) @@ -61,24 +61,21 @@ IF "%DCCSI_LOGLEVEL%"=="" (set DCCSI_LOGLEVEL=20) echo DCCSI_LOGLEVEL = %DCCSI_LOGLEVEL% :: This maps up to the \Dev folder -IF "%DEV_REL_PATH%"=="" (set DEV_REL_PATH=..\..\..\..) -echo DEV_REL_PATH = %DEV_REL_PATH% +IF "%O3DE_REL_PATH%"=="" (set O3DE_REL_PATH=..\..\..\..) +echo O3DE_REL_PATH = %O3DE_REL_PATH% :: You can define the project name -IF "%LY_PROJECT_NAME%"=="" ( - for %%a in (%CD%..\..\..) do set LY_PROJECT_NAME=%%~na +IF "%O3DE_PROJECT%"=="" ( + for %%a in (%CD%..\..\..\..) do set O3DE_PROJECT=%%~na ) -echo LY_PROJECT_NAME = %LY_PROJECT_NAME% - -:: if not defined we just use the DCCsi path as stand-in -IF "%LY_PROJECT%"=="" (set LY_PROJECT=%CD%) -echo LY_PROJECT = %LY_PROJECT% +echo O3DE_PROJECT = %O3DE_PROJECT% :: set up the default project path (dccsi) :: if not set we also use the DCCsi path as stand-in -CD /D ..\..\ -IF "%LY_PROJECT_PATH%"=="" (set LY_PROJECT_PATH=%CD%) -echo LY_PROJECT_PATH = %LY_PROJECT_PATH% +CD /D ..\..\..\ +:: To Do: remove one of these +IF "%O3DE_PROJECT_PATH%"=="" (set O3DE_PROJECT_PATH=%CD%) +echo O3DE_PROJECT_PATH = %O3DE_PROJECT_PATH% IF "%ABS_PATH%"=="" (set ABS_PATH=%CD%) echo ABS_PATH = %ABS_PATH% @@ -87,37 +84,40 @@ echo ABS_PATH = %ABS_PATH% pushd %ABS_PATH% :: Change to root Lumberyard dev dir -CD /d %LY_PROJECT_PATH%\%DEV_REL_PATH% -IF "%LY_DEV%"=="" (set LY_DEV=%CD%) -echo LY_DEV = %LY_DEV% +CD /d %O3DE_PROJECT_PATH%\%O3DE_REL_PATH% +IF "%O3DE_DEV%"=="" (set O3DE_DEV=%CD%) +echo O3DE_DEV = %O3DE_DEV% :: Restore original directory popd :: dcc scripting interface gem path :: currently know relative path to this gem -set DCCSIG_PATH=%LY_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface +set DCCSIG_PATH=%O3DE_DEV%\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface echo DCCSIG_PATH = %DCCSIG_PATH% :: Change to DCCsi root dir CD /D %DCCSIG_PATH% :: per-dcc sdk path -set DCCSI_SDK_PATH=%DCCSIG_PATH%\SDK -echo DCCSI_SDK_PATH = %DCCSI_SDK_PATH% +set DCCSI_TOOLS_PATH=%DCCSIG_PATH%\Tools +echo DCCSI_TOOLS_PATH = %DCCSI_TOOLS_PATH% :: temp log location specific to this gem -set DCCSI_LOG_PATH=%DCCSIG_PATH%\.temp\logs +set DCCSI_LOG_PATH=%O3DE_PROJECT_PATH%\.temp\logs echo DCCSI_LOG_PATH = %DCCSI_LOG_PATH% :: O3DE build path -IF "%TAG_LY_BUILD_PATH%"=="" (set TAG_LY_BUILD_PATH=build) -echo TAG_LY_BUILD_PATH = %TAG_LY_BUILD_PATH% +IF "%O3DE_BUILD_FOLDER%"=="" (set O3DE_BUILD_FOLDER=build) +echo O3DE_BUILD_FOLDER = %O3DE_BUILD_FOLDER% -IF "%LY_BUILD_PATH%"=="" (set LY_BUILD_PATH=%LY_DEV%\%TAG_LY_BUILD_PATH%\bin\profile) -echo LY_BUILD_PATH = %LY_BUILD_PATH% +IF "%O3DE_BUILD_PATH%"=="" (set O3DE_BUILD_PATH=%O3DE_DEV%\%O3DE_BUILD_FOLDER%) +echo O3DE_BUILD_PATH = %O3DE_BUILD_PATH% + +IF "%O3DE_BIN_PATH%"=="" (set O3DE_BIN_PATH=%O3DE_BUILD_PATH%\bin\profile) +echo O3DE_BIN_PATH = %O3DE_BIN_PATH% :: add to the PATH -SET PATH=%LY_BUILD_PATH%;%DCCSIG_PATH%;%DCCSI_AZPY_PATH%;%PATH% +SET PATH=%O3DE_BIN_PATH%;%DCCSIG_PATH%;%PATH% ::ENDLOCAL diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Maya.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Maya.bat similarity index 87% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Maya.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Maya.bat index cceaa80b75..5e3c600124 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Maya.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Maya.bat @@ -27,14 +27,12 @@ IF "%DCCSI_PY_VERSION_MINOR%"=="" (set DCCSI_PY_VERSION_MINOR=7) IF "%DCCSI_PY_VERSION_RELEASE%"=="" (set DCCSI_PY_VERSION_RELEASE=11) :: Default Maya Version -IF "%DCCSI_MAYA_VERSION%"=="" (set DCCSI_MAYA_VERSION=%MAYA_VERSION%) +IF "%DCCSI_MAYA_VERSION%"=="" (set DCCSI_MAYA_VERSION=2020) :: Initialize env CALL %~dp0\Env_Core.bat CALL %~dp0\Env_Python.bat -::SETLOCAL ENABLEDELAYEDEXPANSION - echo. echo _____________________________________________________________________ echo. @@ -48,14 +46,14 @@ echo DCCSI_PY_VERSION_RELEASE = %DCCSI_PY_VERSION_RELEASE% echo DCCSI_MAYA_VERSION = %DCCSI_MAYA_VERSION% :::: Set Maya native project acess to this project -IF "%MAYA_PROJECT%"=="" (set MAYA_PROJECT=%LY_PROJECT%) +IF "%MAYA_PROJECT%"=="" (set MAYA_PROJECT=%O3DE_PROJECT%) echo MAYA_PROJECT = %MAYA_PROJECT% :: maya sdk path -set DCCSI_SDK_MAYA_PATH=%DCCSI_SDK_PATH%\Maya -echo DCCSI_SDK_MAYA_PATH = %DCCSI_SDK_MAYA_PATH% +set DCCSI_TOOLS_MAYA_PATH=%DCCSI_TOOLS_PATH%\DCC\Maya +echo DCCSI_TOOLS_MAYA_PATH = %DCCSI_TOOLS_MAYA_PATH% -set MAYA_MODULE_PATH=%DCCSI_SDK_MAYA_PATH%;%MAYA_MODULE_PATH% +set MAYA_MODULE_PATH=%DCCSI_TOOLS_MAYA_PATH%;%MAYA_MODULE_PATH% echo MAYA_MODULE_PATH = %MAYA_MODULE_PATH% :: Maya File Paths, etc @@ -93,36 +91,36 @@ echo DCCSI_PY_MAYA = %DCCSI_PY_MAYA% SET PATH=%MAYA_BIN_PATH%;%PATH% :: Local DCCsi Maya plugins access (ours) -set DCCSI_MAYA_PLUG_IN_PATH=%DCCSI_SDK_MAYA_PATH%\plugins +set DCCSI_MAYA_PLUG_IN_PATH=%DCCSI_TOOLS_MAYA_PATH%\plugins :: also attached to maya's built-it env var set MAYA_PLUG_IN_PATH=%DCCSI_MAYA_PLUG_IN_PATH%;MAYA_PLUG_IN_PATH echo DCCSI_MAYA_PLUG_IN_PATH = %DCCSI_MAYA_PLUG_IN_PATH% :: Local DCCsi Maya shelves (ours) -set DCCSI_MAYA_SHELF_PATH=%DCCSI_SDK_MAYA_PATH%\Prefs\Shelves +set DCCSI_MAYA_SHELF_PATH=%DCCSI_TOOLS_MAYA_PATH%\Prefs\Shelves set MAYA_SHELF_PATH=%DCCSI_MAYA_SHELF_PATH% echo DCCSI_MAYA_SHELF_PATH = %DCCSI_MAYA_SHELF_PATH% :: Local DCCsi Maya icons path (ours) -set DCCSI_MAYA_XBMLANGPATH=%DCCSI_SDK_MAYA_PATH%\Prefs\icons +set DCCSI_MAYA_XBMLANGPATH=%DCCSI_TOOLS_MAYA_PATH%\Prefs\icons :: also attached to maya's built-it env var set XBMLANGPATH=%DCCSI_MAYA_XBMLANGPATH%;%XBMLANGPATH% echo DCCSI_MAYA_XBMLANGPATH = %DCCSI_MAYA_XBMLANGPATH% :: Local DCCsi Maya Mel scripts (ours) -set DCCSI_MAYA_SCRIPT_MEL_PATH=%DCCSI_SDK_MAYA_PATH%\Scripts\Mel +set DCCSI_MAYA_SCRIPT_MEL_PATH=%DCCSI_TOOLS_MAYA_PATH%\Scripts\Mel :: also attached to maya's built-it env var set MAYA_SCRIPT_PATH=%DCCSI_MAYA_SCRIPT_MEL_PATH%;%MAYA_SCRIPT_PATH% echo DCCSI_MAYA_SCRIPT_MEL_PATH = %DCCSI_MAYA_SCRIPT_MEL_PATH% :: Local DCCsi Maya Python scripts (ours) -set DCCSI_MAYA_SCRIPT_PY_PATH=%DCCSI_SDK_MAYA_PATH%\Scripts\Python +set DCCSI_MAYA_SCRIPT_PY_PATH=%DCCSI_TOOLS_MAYA_PATH%\Scripts\Python :: also attached to maya's built-it env var set MAYA_SCRIPT_PATH=%DCCSI_MAYA_SCRIPT_PY_PATH%;%MAYA_SCRIPT_PATH% echo DCCSI_MAYA_SCRIPT_PY_PATH = %DCCSI_MAYA_SCRIPT_PY_PATH% :: DCCsi Maya boostrap, userSetup.py access (ours) -set DCCSI_MAYA_SCRIPT_PATH=%DCCSI_SDK_MAYA_PATH%\Scripts +set DCCSI_MAYA_SCRIPT_PATH=%DCCSI_TOOLS_MAYA_PATH%\Scripts :: also attached to maya's built-it env var set MAYA_SCRIPT_PATH=%DCCSI_MAYA_SCRIPT_PATH%;%MAYA_SCRIPT_PATH% echo DCCSI_MAYA_SCRIPT_PATH = %DCCSI_MAYA_SCRIPT_PATH% diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_PyCharm.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_PyCharm.bat similarity index 95% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_PyCharm.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_PyCharm.bat index 67f2c3d81e..7be9154e6d 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_PyCharm.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_PyCharm.bat @@ -34,7 +34,7 @@ CALL %~dp0\Env_Python.bat CALL %~dp0\Env_Qt.bat :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Python.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Python.bat similarity index 76% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Python.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Python.bat index 196d7b09bb..388ab531df 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Python.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Python.bat @@ -19,8 +19,6 @@ PUSHD %~dp0 CALL %~dp0\Env_Core.bat -::SETLOCAL ENABLEDELAYEDEXPANSION - echo. echo _____________________________________________________________________ echo. @@ -56,32 +54,29 @@ echo DCCSI_PYTHON_LIB_PATH = %DCCSI_PYTHON_LIB_PATH% SET PATH=%DCCSI_PYTHON_LIB_PATH%;%PATH% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: location for O3DE python 3.7 location -set DCCSI_PY_BASE=%DCCSI_PYTHON_INSTALL%\python.cmd +:: Note, many DCC tools (like Maya) include thier own python interpretter +:: Some DCC apps may not operate correctly if PYTHONHOME is set (this is definitely the case with Maya) +:: Be aware the python.cmd below does set PYTHONHOME +set DCCSI_PY_BASE=%O3DE_PYTHON_INSTALL%\python.cmd echo DCCSI_PY_BASE = %DCCSI_PY_BASE% -:: ide and debugger plug -set DCCSI_PY_DEFAULT=%DCCSI_PY_BASE% +CALL %O3DE_PYTHON_INSTALL%\get_python_path.bat -:: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE=%DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +:: Some IDEs like Wing, may in some cases need acess directly to the exe to operate correctly +IF "%DCCSI_PY_IDE%"=="" (set DCCSI_PY_IDE=%O3DE_PYTHONHOME%\python.exe) echo DCCSI_PY_IDE = %DCCSI_PY_IDE% -set DCCSI_PY_IDE_PACKAGES=%DCCSI_PY_IDE%\Lib\site-packages -echo DCCSI_PY_IDE_PACKAGES = %DCCSI_PY_IDE_PACKAGES% - :: add to the PATH -SET PATH=%DCCSI_PYTHON_INSTALL%;%DCCSI_PY_IDE%;%DCCSI_PY_IDE_PACKAGES%;%PATH% +SET PATH=%O3DE_PYTHON_INSTALL%;%O3DE_PYTHONHOME%;%DCCSI_PY_IDE%;%PATH% :: add all python related paths to PYTHONPATH for package imports -set PYTHONPATH=%DCCSIG_PATH%;%DCCSI_PYTHON_LIB_PATH%;%LY_BUILD_PATH%;%PYTHONPATH% +set PYTHONPATH=%DCCSIG_PATH%;%DCCSI_PYTHON_LIB_PATH%;%O3DE_BUILD_PATH%;%PYTHONPATH% echo PYTHONPATH = %PYTHONPATH% -::ENDLOCAL - :: Set flag so we don't initialize dccsi environment twice SET DCCSI_ENV_PY_INIT=1 GOTO END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Qt.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Qt.bat similarity index 85% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Qt.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Qt.bat index 2ad49b4416..6202ead2f2 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Qt.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Qt.bat @@ -34,23 +34,23 @@ echo. :: set up Qt/Pyside paths :: set up PySide2/Shiboken -set QTFORPYTHON_PATH=%LY_DEV%\Gems\QtForPython\3rdParty\pyside2\windows\release +set QTFORPYTHON_PATH=%O3DE_DEV%\Gems\QtForPython\3rdParty\pyside2\windows\release echo QTFORPYTHON_PATH = %QTFORPYTHON_PATH% :: add to the PATH SET PATH=%QTFORPYTHON_PATH%;%PATH% SET PYTHONPATH=%QTFORPYTHON_PATH%;%PYTHONPATH% -set QT_PLUGIN_PATH=%LY_BUILD_PATH%\bin\profile\EditorPlugins +set QT_PLUGIN_PATH=%O3DE_BUILD_PATH%\bin\profile\EditorPlugins echo QT_PLUGIN_PATH = %QT_PLUGIN_PATH% :: add to the PATH SET PATH=%QT_PLUGIN_PATH%;%PATH% SET PYTHONPATH=%QT_PLUGIN_PATH%;%PYTHONPATH% -set LY_BIN_PATH=%LY_BUILD_PATH%\bin\profile -echo LY_BIN_PATH = %LY_BIN_PATH% -SET PATH=%LY_BIN_PATH%;%PATH% +set O3DE_BIN_PATH=%O3DE_BUILD_PATH%\bin\profile +echo O3DE_BIN_PATH = %O3DE_BIN_PATH% +SET PATH=%O3DE_BIN_PATH%;%PATH% ::ENDLOCAL diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Substance.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Substance.bat similarity index 92% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Substance.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Substance.bat index 3c92b306ca..d44ed985da 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_Substance.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_Substance.bat @@ -31,7 +31,7 @@ echo. : Substance Designer :: maya sdk path -set DCCSI_SUBSTANCE_PATH=%DCCSI_SDK_PATH%\Substance +set DCCSI_SUBSTANCE_PATH=%DCCSI_TOOLS_PATH%\Substance echo DCCSI_SUBSTANCE_PATH = %DCCSI_SUBSTANCE_PATH% :: https://docs.substance3d.com/sddoc/project-preferences-107118596.html#ProjectPreferences-ConfigurationFile :: Path to .exe, "C:\Program Files\Allegorithmic\Substance Designer\Substance Designer.exe" @@ -39,7 +39,7 @@ set SUBSTANCE_PATH="%ProgramFiles%\Allegorithmic\Substance Designer" echo SUBSTANCE_PATH = %SUBSTANCE_PATH% :: default config -set SUBSTANCE_CFG_PATH=%LY_PROJECT_PATH%\DCCsi_default.sbscfg +set SUBSTANCE_CFG_PATH=%O3DE_PROJECT_PATH%\DCCsi_default.sbscfg echo SUBSTANCE_CFG_PATH = %SUBSTANCE_CFG_PATH% ::ENDLOCAL diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_VScode.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_VScode.bat similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_VScode.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_VScode.bat diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_WingIDE.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_WingIDE.bat similarity index 78% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_WingIDE.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_WingIDE.bat index 56209ca5aa..a8c3b3d073 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Env_WingIDE.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Env_WingIDE.bat @@ -27,18 +27,9 @@ CALL %~dp0\Env_Core.bat CALL %~dp0\Env_Python.bat CALL %~dp0\Env_Qt.bat -:: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python -echo DCCSI_PY_IDE = %DCCSI_PY_IDE% - -:: ide and debugger plug -set DCCSI_PY_DEFAULT=%DCCSI_PY_IDE%\python.exe - :: put project env variables/paths here set WINGHOME=%PROGRAMFILES(X86)%\Wing Pro %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% -SET WING_PROJ=%DCCSIG_PATH%\Solutions\.wing\DCCsi_%DCCSI_WING_VERSION_MAJOR%x.wpr - -::SETLOCAL ENABLEDELAYEDEXPANSION +SET WING_PROJ=%DCCSIG_PATH%\Tools\Dev\Windows\Solutions\.wing\DCCsi_%DCCSI_WING_VERSION_MAJOR%x.wpr echo. echo _____________________________________________________________________ @@ -55,8 +46,6 @@ echo WING_PROJ = %WING_PROJ% :: add to the PATH SET PATH=%WINGHOME%;%PATH% -::ENDLOCAL - :: Set flag so we don't initialize dccsi environment twice SET DCCSI_ENV_WINGIDE_INIT=1 GOTO END_OF_FILE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Env_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Env_Cmd.bat similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Env_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Env_Cmd.bat index a8476ccc22..5b76de1401 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Env_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Env_Cmd.bat @@ -40,7 +40,7 @@ CALL %~dp0\Env_WingIDE.bat IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_MayaPy_PyCharmPro.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_MayaPy_PyCharmPro.bat similarity index 91% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_MayaPy_PyCharmPro.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_MayaPy_PyCharmPro.bat index d98b8d81e7..e451050ab8 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_MayaPy_PyCharmPro.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_MayaPy_PyCharmPro.bat @@ -56,14 +56,14 @@ echo ~ MayaPy.exe (default python interpreter) echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug @@ -80,7 +80,7 @@ IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% IF EXIST "%PYCHARM_HOME%\bin\pycharm64.exe" ( start "" "%PYCHARM_HOME%\bin\pycharm64.exe" "%PYCHARM_PROJ%" diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Maya_2020.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Maya_2020.bat similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Maya_2020.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Maya_2020.bat index 2c4e1cc941..ee2ea5bf1f 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Maya_2020.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Maya_2020.bat @@ -50,7 +50,7 @@ echo MAYA_LOCATION = %MAYA_LOCATION% echo MAYA_BIN_PATH = %MAYA_BIN_PATH% :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Default to the right version of Maya if we can detect it... and launch IF EXIST "%MAYA_BIN_PATH%\maya.exe" ( diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyCharmPro.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyCharmPro.bat similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyCharmPro.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyCharmPro.bat diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyMin_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyMin_Cmd.bat similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyMin_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyMin_Cmd.bat index d001d8a75e..193cb40de8 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_PyMin_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_PyMin_Cmd.bat @@ -38,7 +38,7 @@ echo _____________________________________________________________________ echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Qt_PyMin_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Qt_PyMin_Cmd.bat similarity index 93% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Qt_PyMin_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Qt_PyMin_Cmd.bat index d50989004b..75d1ff8cc4 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_Qt_PyMin_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_Qt_PyMin_Cmd.bat @@ -10,7 +10,7 @@ REM :: Set up and run LY Python CMD prompt :: Sets up the DccScriptingInterface_Env, -:: Puts you in the CMD within the LY_DEV environment +:: Puts you in the CMD within the O3DE_DEV environment :: Set up window TITLE O3DE DCC Scripting Interface Py Qt Cmd @@ -39,7 +39,7 @@ echo _____________________________________________________________________ echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_VScode.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_VScode.bat similarity index 92% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_VScode.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_VScode.bat index 120584ed6a..2dc5b06032 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_VScode.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_VScode.bat @@ -61,14 +61,14 @@ echo ~ Launching DCCsi Project in VScode echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_WingIDE-7-1.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_WingIDE-7-1.bat similarity index 74% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_WingIDE-7-1.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_WingIDE-7-1.bat index c933670bab..56e278da08 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_WingIDE-7-1.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_WingIDE-7-1.bat @@ -20,6 +20,9 @@ COLOR 8E cd %~dp0 PUSHD %~dp0 +:: if the user has set up a custom env call it +IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat + :: Constant Vars (Global) :: global debug (propogates) IF "%DCCSI_GDEBUG%"=="" (set DCCSI_GDEBUG=True) @@ -51,35 +54,20 @@ echo. echo _____________________________________________________________________ echo. echo ~ WingIDE Version %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% -echo ~ Launching O3DE %LY_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... +echo ~ Launching O3DE %O3DE_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% - -:: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python -echo DCCSI_PY_IDE = %DCCSI_PY_IDE% - -:: ide and debugger plug -set DCCSI_PY_BASE=%DCCSI_PY_IDE%\python.exe -echo DCCSI_PY_BASE = %DCCSI_PY_BASE% - -:: ide and debugger plug -set DCCSI_PY_DEFAULT=%DCCSI_PY_BASE% -echo DCCSI_PY_DEFAULT = %DCCSI_PY_DEFAULT% - -:: if the user has set up a custom env call it -IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% IF EXIST "%WINGHOME%\bin\wing.exe" ( start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%" diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayaPy_2020.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayaPy_2020.bat similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayaPy_2020.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayaPy_2020.bat index f07ec093cb..33b8bc6392 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayaPy_2020.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayaPy_2020.bat @@ -48,7 +48,7 @@ echo MAYA_LOCATION = %MAYA_LOCATION% echo MAYA_BIN_PATH = %MAYA_BIN_PATH% :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% SETLOCAL ENABLEDELAYEDEXPANSION diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayapy_WingIDE-7-1.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayapy_WingIDE-7-1.bat similarity index 87% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayapy_WingIDE-7-1.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayapy_WingIDE-7-1.bat index 631250d06b..ca5b175c39 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_mayapy_WingIDE-7-1.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_mayapy_WingIDE-7-1.bat @@ -50,19 +50,19 @@ echo. echo _____________________________________________________________________ echo. echo ~ WingIDE Version %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% -echo ~ Launching O3DE %LY_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... +echo ~ Launching O3DE %O3DE_PROJECT% project in WingIDE %DCCSI_WING_VERSION_MAJOR%.%DCCSI_WING_VERSION_MINOR% ... echo ~ MayaPy.exe (default python interpreter) echo _____________________________________________________________________ echo. -echo LY_DEV = %LY_DEV% +echo O3DE_DEV = %O3DE_DEV% :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python -echo DCCSI_PYTHON_INSTALL = %DCCSI_PYTHON_INSTALL% +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python +echo O3DE_PYTHON_INSTALL = %O3DE_PYTHON_INSTALL% :: Wing and other IDEs probably prefer access directly to the python.exe -set DCCSI_PY_IDE = %DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python +set DCCSI_PY_IDE = %O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python echo DCCSI_PY_IDE = %DCCSI_PY_IDE% :: ide and debugger plug @@ -79,7 +79,7 @@ IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat echo. :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% IF EXIST "%WINGHOME%\bin\wing.exe" ( start "" "%WINGHOME%\bin\wing.exe" "%WING_PROJ%" diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE.bat similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE.bat diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE_Cmd.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE_Cmd.bat similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE_Cmd.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE_Cmd.bat index 9f8950f59f..e1b075e065 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Launch_pyBASE_Cmd.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Launch_pyBASE_Cmd.bat @@ -33,7 +33,7 @@ CALL %~dp0\Env_Maya.bat IF EXIST "%~dp0Env_Dev.bat" CALL %~dp0Env_Dev.bat :: Change to root dir -CD /D %LY_PROJECT_PATH% +CD /D %O3DE_PROJECT_PATH% :: Create command prompt with environment CALL %windir%\system32\cmd.exe diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/README.txt new file mode 100644 index 0000000000..e5b7946276 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/README.txt @@ -0,0 +1,78 @@ +""" +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 +""" +# ------------------------------------------------------------------------- + +DccScriptingInterface (DCCsi) is a framework for O3DE extensions, for example: +- Lightweight python integrations with DCC tools like Maya +- O3DE configuration, customization and extensions of tools +- Standalone PySide2/Qt tools +- ^ These might utilize a mix of O3DE and DDC python APIs + +The DccScriptingInterface\config.py, procedurally provides a synthetic env context. +This env is a data-driven approach to configuring layered and managed env settings. + +This env provides the hooks for DDC apps and/or standalone tools, +to configure acess to O3DE code (for boostrapping), safely retreive known paths, set/get developer flags, etc. + +DccScriptingInterface\Tools\Dev\Windows\ + +This is a .bat file based version of the default env context for development on windows. +This is what we use to boot the default env context such that it is available, when launching a development tool such as a IDE. + +This allows a developer to troubleshoot/debug code, like config.py + +Other tools, can use config.py to stand up the env context. + +What is in this folder ... + +Core env modules +--------------------- +Env_Core.bat : core access to O3DE and DCCsi +Env_Python.bat : access to O3DE python and general py configuration +Env_Qt.bat : access to O3DE Qt .dll files and PySide2 + +DCC add on envars +--------------------- +Env_Maya.bat : configures Maya with code O3DE/DCCsi access +Env_Substance.bat : Configures Substance Designer + +IDE env +--------------------- +Env_WingIDE.bat : configures WingIDE for DCCsi development +Env_VScode.bat : configures VScode for DCCsi development +Env_PyCharm.bat : configures PyCharm for DCCsi development + +Launchers +--------------------- +Launch_env_Cmd.bat : Starts a cmd with entire managed env context + : ^ allows use to validate env + : ^ display all default ENVAR plugs + : ^ allows user to test O3DE python + scripts from cmd +Launch_PyMin_Cmd.bat : Starts minimal cmd with O3DE python access only + : ^ for instance, test Dccsi\config.py like this: + : {DCCsi prommpt}>python config.py +Launch_Maya_2020.bat : Starts Maya2020 within managed env context +Launch_WindIDE-7-1.bat : Starts WingIDE within managed env context + +--------------------- +Instructions: How to test the synthetic environment and settings externally + +1. Run the cmd: Launch_PyMin_Cmd.bat + + C:\Depot\o3de-engine\Gems\AtomLyIntegration\TechnicalArt\DccScriptingInterface> + +2. Run command:>python config.py -dm=True -py=True -qt=True + +What this does? +- runs the O3DE python exe +- starts the config.py which begins to procedurally create synthetic/dynamic environment (hooks) +- ^ this starts with DCCsi hooks +- enters 'dev mode'(-dm) and attempts to attach debugger (Wing IDE only for now, others planned) +- enables additional O3DE python hooks and code access +- ^ great for standalone tools, but you don't want that functionality to interfer with other DCC tools python environments (like Maya!) +- enables access to O3DE Qt .dlls and PySide2 python package support(-qt) +- ^ great for standalone PySide2 which can operate outside of the O3DE editor \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Setuo_copy_oiio.bat b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Setuo_copy_oiio.bat similarity index 76% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Setuo_copy_oiio.bat rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Setuo_copy_oiio.bat index 590e634d54..18a58e1371 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Launchers/Windows/Setuo_copy_oiio.bat +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Setuo_copy_oiio.bat @@ -13,12 +13,12 @@ cd %~dp0 PUSHD %~dp0 :: This maps up to the \Dev folder -set LY_DEV=..\..\..\..\..\.. +set O3DE_DEV=..\..\..\..\..\.. :: shared location for default O3DE python location -set DCCSI_PYTHON_INSTALL=%LY_DEV%\Python +set O3DE_PYTHON_INSTALL=%O3DE_DEV%\Python -set PY_SITE=%DCCSI_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python\Lib\site-packages +set PY_SITE=%O3DE_PYTHON_INSTALL%\runtime\python-3.7.10-rev2-windows\python\Lib\site-packages set PACKAGE_LOC=C:\Depot\3rdParty\packages\openimageio-2.1.16.0-rev1-windows\OpenImageIO\2.1.16.0\win_x64\bin diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/readme.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/readme.txt similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.dev/readme.txt rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/readme.txt diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.dev/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.p4ignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.p4ignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/.p4ignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/.p4ignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/DccScriptingInterface.iml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/DccScriptingInterface.iml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/DccScriptingInterface.iml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/DccScriptingInterface.iml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/encodings.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/encodings.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/encodings.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/encodings.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/main.py similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/main.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/main.py diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/misc.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/misc.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/misc.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/misc.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/modules.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/modules.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/modules.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/modules.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/vcs.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/vcs.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/vcs.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/vcs.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/webResources.xml b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/webResources.xml similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.idea/webResources.xml rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.idea/webResources.xml diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/dccsi.code-workspace b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/dccsi.code-workspace similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/dccsi.code-workspace rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/dccsi.code-workspace diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/launch.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/launch.json similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/launch.json rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/launch.json diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/settings.json similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.vscode/settings.json rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.vscode/settings.json diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/.gitignore b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/.gitignore similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/.gitignore rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/.gitignore diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/DCCsi_7x.wpr similarity index 57% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/DCCsi_7x.wpr index aa7a8c4bd9..18af1298db 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/.wing/DCCsi_7x.wpr +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/.wing/DCCsi_7x.wpr @@ -5,7 +5,22 @@ ################################################################## [project attributes] debug.launch-configs = (2, - {'launch-GeaM41WYMGA1sEfm': ({'shared': True}, + {'launch-0L6se5pxC9AWvpY0': ({'shared': True}, + {'buildcmd': ('project', + None), + 'env': ('project', + [u'']), + 'name': 'DCCSI_PY_IDE', + 'pyexec': ('custom', + u'${DCCSI_PY_IDE}'), + 'pypath': ('project', + []), + 'pyrunargs': ('project', + '-u'), + 'runargs': u'', + 'rundir': ('project', + u'')}), + 'launch-GeaM41WYMGA1sEfm': ({'shared': True}, {'buildcmd': ('project', None), 'env': ('custom', @@ -33,52 +48,30 @@ debug.launch-configs = (2, 'pyrunargs': ('project', '-u'), 'runargs': u'', - 'rundir': ('project', - u'')}), - 'launch-oobMrvXFf1SwtYBg': ({'shared': True}, - {'buildcmd': ('project', - None), - 'env': ('custom', - [u'']), - 'name': u'DCCSI_PY_DEFAULT', - 'pyexec': ('custom', - u'${DCCSI_PY_DEFAULT}'), - 'pypath': ('project', - []), - 'pyrunargs': ('project', - '-u'), - 'runargs': u'', 'rundir': ('project', u'')})}) -proj.directory-list = [{'dirloc': loc('../..'), +proj.directory-list = [{'dirloc': loc('../../../../..'), 'excludes': (), 'filter': u'*', - 'include_hidden': True, + 'include_hidden': False, + 'recursive': True, + 'watch_for_changes': True}, + {'dirloc': loc('../../../../../../../../Atom/Feature/Common/Editor/Scripts/ColorGrading'), + 'excludes': (), + 'filter': u'*', + 'include_hidden': False, + 'recursive': True, + 'watch_for_changes': True}, + {'dirloc': loc('../../../../../../../../../python'), + 'excludes': (), + 'filter': u'*', + 'include_hidden': False, 'recursive': True, 'watch_for_changes': True}] proj.file-type = 'shared' -proj.home-dir = loc('../..') -proj.launch-config = {loc('../../SDK/Atom/Scripts/Python/DCC_Materials/maya_materials_export.py'): ('c'\ - 'ustom', +proj.launch-config = {loc('../../../../../azpy/core/py2/utils.py'): ('custom', (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../SDK/Maya/Scripts/Python/legacy_asset_converter/main.py'): ('c'\ - 'ustom', + 'launch-0L6se5pxC9AWvpY0')), + loc('../../../../../azpy/core/py3/utils.py'): ('custom', (u'', - 'launch-WUN9lgYK6qYU7qE9')), - loc('../../azpy/__init__.py'): ('custom', - (u'', - 'launch-oobMrvXFf1SwtYBg')), - loc('../../azpy/constants.py'): ('custom', - (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../azpy/env_base.py'): ('project', - (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../azpy/maya/callbacks/node_message_callback_handler.py'): ('c'\ - 'ustom', - (u'', - 'launch-GeaM41WYMGA1sEfm')), - loc('../../config.py'): ('custom', - (u'', - 'launch-GeaM41WYMGA1sEfm'))} + 'launch-0L6se5pxC9AWvpY0'))} diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/readme.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/readme.txt similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Solutions/readme.txt rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Dev/Windows/Solutions/readme.txt diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/README.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/README.txt new file mode 100644 index 0000000000..05aa948943 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/README.txt @@ -0,0 +1,9 @@ +""" +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 folder resprents a Mock Tool - currently just a scaffold (not implemented) \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/Resources/resource.txt b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/Resources/resource.txt new file mode 100644 index 0000000000..30df2e0eb9 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/Resources/resource.txt @@ -0,0 +1,9 @@ +""" +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 folder is for tool resources - this file is a resource stub. \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/config.py new file mode 100644 index 0000000000..1dc43d8485 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/config.py @@ -0,0 +1,12 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/main.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/main.py new file mode 100644 index 0000000000..1dc43d8485 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/main.py @@ -0,0 +1,12 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- + +print('Not Implemented') \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/settings.json new file mode 100644 index 0000000000..d58fc7e10c --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/MockTool/settings.json @@ -0,0 +1,17 @@ +{ + "default": { + "DCCSI_GDEBUG": true, + "SPDX-LICENSE-IDENTIFIER": "Apache-2.0 OR MIT", + "COPYWRITE":"Copyright (c) Contributors to the Open 3D Engine Project.", + "COPYWRITE_MSG":"For complete copyright and license terms please see the LICENSE at the root of this distribution." + }, + "development": { + "DCCSI_GDEBUG": true, + "TEST_RULE": "/dccsi_with_json", + "MESSAGE": "The O3DE DCCsi manages dynamic configuration and settings with dynaconf", + "DYNACONF_HELP": "https://dynaconf.readthedocs.io/" + }, + "production": { + "DCCSI_GDEBUG": false + } +} \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/AA_Gun_01_ddna.tif b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/AA_Gun_01_ddna.tif new file mode 100644 index 0000000000..0cefbe9c25 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/AA_Gun_01_ddna.tif @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9c34bbeafae5c5b62d3571ac9eee295fff12ac0744f81375f53a3faf2c5095e4 +size 286400 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/test_oiio.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/test_oiio.py new file mode 100644 index 0000000000..b33fa8ef56 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/Tests/OpenImageIO/test_oiio.py @@ -0,0 +1,66 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- + +import OpenImageIO as oiio + +def convert(src): + try: + rgba = oiio.ImageBuf(src) + spec = get_image_spec(rgba) + + # Normal output ------> + rgb = oiio.ImageBufAlgo.channels(rgba, (0, 1, 2)) + normal_output = src.replace('ddna', 'Normal') + + # Roughness output ------> + alpha = oiio.ImageBufAlgo.channels(rgba, (3,)) + roughness = oiio.ImageBufAlgo.invert(alpha) + roughness_output = normal_output.replace('Normal', 'Roughness') + + write_image(rgba, normal_output, spec['format']) + write_image(roughness, roughness_output, spec['format']) + # logging.info('Output Normal Map: {}'.format(normal_output)) + # logging.info('Output Roughness Map: {}'.format(roughness_output)) + + except Exception as e: + logging.info('OIIO error in image conversion: {}'.format(e)) + return None + +def get_image_spec(target_image): + spec = target_image.spec() + info = {'resolution': (spec.width, spec.height, spec.x, spec.y), 'channels': spec.channelnames, + 'format': str(spec.format)} + if spec.channelformats : + info['channelformats'] = str(spec.channelformats) + info['alpha channel'] = str(spec.alpha_channel) + info['z channel'] = str(spec.z_channel) + info['deep'] = str(spec.deep) + for i in range(len(spec.extra_attribs)): + if type(spec.extra_attribs[i].value) == str: + info[spec.extra_attribs[i].name] = spec.extra_attribs[i].value + else: + info[spec.extra_attribs[i].name] = spec.extra_attribs[i].value + return info + +def write_image(image, filename, image_format): + if not image.has_error: + image.set_write_format(image_format) + image.write(filename) + if image.has_error: + print("Error writing", filename, ":", image.geterror()) + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + # run a test + convert('AA_Gun_01_ddna.tif') + # validate() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/stub b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Python/stub new file mode 100644 index 0000000000..e69de29bb2 diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Resources/Atom/StandardPBR_AllProperties.material b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Resources/Atom/StandardPBR_AllProperties.material new file mode 100644 index 0000000000..7d38d1a1a3 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/Resources/Atom/StandardPBR_AllProperties.material @@ -0,0 +1,47 @@ +{ + "description": "", + "materialType": "Materials/Types/StandardPBR.materialtype", + "parentMaterial": "", + "propertyLayoutVersion": 3, + "properties": { + "general": { + "texcoord": 0 + }, + "baseColor": { + "colorLinear": [ 1.0, 1.0, 1.0 ], + "factor": 1.0, + "useTexture": true, + "textureMap": "EngineAssets/TextureMsg/DefaultNoUVs.tif" + }, + "metallic": { + "factor": 0.0, + "useTexture": false, + "textureMap": "" + }, + "roughness": { + "factor": 1.0, + "useTexture": true, + "textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_spec.tif" + }, + "specularF0": { + "factor": 0.5, + "useTexture": false, + "textureMap": "" + }, + "normal": { + "factor": 1.0, + "useTexture": true, + "textureMap": "EngineAssets/TextureMsg/DefaultNoUVs_ddn.tif" + }, + "opacity": { + "doubleSided": false, + "factor": 1.0, + "cutoutAlpha": false, + "cutoutThreshold": 0.5, + "useBaseColorTextureAlpha": false, + "useTexture": false, + "textureMap": "" + } + } +} + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/start_service.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/start_service.py new file mode 100644 index 0000000000..ac9d02cf5d --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/Tools/start_service.py @@ -0,0 +1,21 @@ +""" +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 +""" +# ------------------------------------------------------------------------- +"""DCCsi Tool and Application service launcher""" + +def start_service(): + """Not Implemented""" + print('DCCsi.Tools.DCC.start_service() not implemented') + return None + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """Run this file as main""" + + start_service() \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py index 0577c50474..958508c227 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/__init__.py @@ -26,26 +26,18 @@ import logging as _logging # ------------------------------------------------------------------------- -_ORG_TAG = 'Amazon_Lumberyard' -_APP_TAG = 'DCCsi' -_TOOL_TAG = 'azpy' -_TYPE_TAG = 'module' +# global scope +_PACKAGENAME = 'azpy' -_PACKAGENAME = _TOOL_TAG +__all__ = ['config_utils', + 'constants', + 'env_bool', + 'return_stub', + 'core', + 'dcc', + 'dev', + 'test'] -__all__ = ['config_utils', 'render', - 'constants', 'return_stub', 'synthetic_env', - 'env_base', 'env_bool', 'test', 'dev', - 'lumberyard', 'marmoset'] # 'blender', 'maya', 'substance', 'houdini'] -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -# _ROOT_LOGGER = _logging.getLogger() # only use this if debugging -# https://stackoverflow.com/questions/56733085/how-to-know-the-current-file-path-after-being-frozen-into-an-executable-using-cx/56748839 -#os.chdir(os.path.dirname(os.path.abspath(inspect.getfile(inspect.currentframe())))) -# ------------------------------------------------------------------------- -# global space # we need to set up basic access to the DCCsi _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? _DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../..')) @@ -58,8 +50,11 @@ import azpy.env_bool as env_bool import azpy.constants as constants import azpy.config_utils as config_utils -_G_DEBUG = env_bool.env_bool(constants.ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool.env_bool(constants.ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool.env_bool(constants.ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_LOGLEVEL = int(env_bool.env_bool(constants.ENVAR_DCCSI_LOGLEVEL, int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) # for py2.7 (Maya) we provide this, so we must assume some bootstrapping # has occured, see DccScriptingInterface\\config.py (_DCCSI_PYTHON_LIB_PATH) @@ -69,38 +64,44 @@ try: except: import pathlib2 as pathlib from pathlib import Path -if _G_DEBUG: - print('DCCsi debug breadcrumb, pathlib is: {}'.format(pathlib)) +if _DCCSI_GDEBUG: + print('[DCCsi][AZPY] DEBUG BREADCRUMB, pathlib is: {}'.format(pathlib)) +# ------------------------------------------------------------------------- -# to be continued... +# ------------------------------------------------------------------------- +# set up module logging +#for handler in _logging.root.handlers[:]: + #_logging.root.removeHandler(handler) +_logging.basicConfig(format=constants.FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- # get/set the project name -_LY_DEV = os.getenv(constants.ENVAR_LY_DEV, +_O3DE_DEV = Path(os.getenv(constants.ENVAR_O3DE_DEV, config_utils.get_stub_check_path(in_path=os.getcwd(), - check_stub='engine.json')) + check_stub='engine.json'))) +_LOGGER.debug('_O3DE_DEV" {}'.format(_O3DE_DEV.resolve())) + +_O3DE_PROJECT_PATH = Path(os.getenv(constants.ENVAR_O3DE_PROJECT_PATH, + config_utils.get_o3de_project_path())) +_LOGGER.debug('_O3DE_PROJECT_PATH" {}'.format(_O3DE_PROJECT_PATH.resolve())) # get/set the project name -_LY_PROJECT_NAME = os.getenv(constants.ENVAR_LY_PROJECT, - config_utils.get_current_project().name) +if _O3DE_PROJECT_PATH: + _O3DE_PROJECT = str(os.getenv(constants.ENVAR_O3DE_PROJECT, + _O3DE_PROJECT_PATH.name)) +else: + _O3DE_PROJECT='o3de' # project cache log dir path -_DCCSI_LOG_PATH = Path(os.getenv(constants.ENVAR_DCCSI_LOG_PATH, - Path(_LY_DEV, - _LY_PROJECT_NAME, - 'Cache', - 'pc', 'user', 'log', 'logs'))) - - -for handler in _logging.root.handlers[:]: - _logging.root.removeHandler(handler) - -# very basic root logger for early debugging, flip to while 1: -while 0: - _logging.basicConfig(level=_logging.DEBUG, - format=constants.FRMT_LOG_LONG, - datefmt='%m-%d %H:%M') - - _logging.debug('azpy.rootlogger> root logger set up for debugging') # root logger +from azpy.constants import TAG_DCCSI_NICKNAME +from azpy.constants import PATH_DCCSI_LOG_PATH +_DCCSI_LOG_PATH = Path(PATH_DCCSI_LOG_PATH.format(O3DE_PROJECT_PATH=_O3DE_PROJECT_PATH.resolve(), + TAG_DCCSI_NICKNAME=TAG_DCCSI_NICKNAME)) # ------------------------------------------------------------------------- @@ -145,14 +146,15 @@ if sys.version_info.major < 3: # ------------------------------------------------------------------------- def initialize_logger(name, log_to_file=False, - default_log_level=_logging.NOTSET): + default_log_level=_logging.NOTSET, + propogate=False): """Start a azpy logger""" _logger = _logging.getLogger(name) - _logger.propagate = False + _logger.propagate = propogate if not _logger.handlers: _log_level = int(os.getenv('DCCSI_LOGLEVEL', default_log_level)) - if _G_DEBUG: + if _DCCSI_GDEBUG: _log_level = int(10) # force when debugging print('_log_level: {}'.format(_log_level)) @@ -201,27 +203,21 @@ def initialize_logger(name, return _logger # ------------------------------------------------------------------------- + # ------------------------------------------------------------------------- -# set up logger with both console and file _logging -if _G_DEBUG: - _LOGGER = initialize_logger(_PACKAGENAME, log_to_file=True) -else: - _LOGGER = initialize_logger(_PACKAGENAME, log_to_file=False) - -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - # some simple logger tests # evoke the filehandlers and test writting to the log file -if _G_DEBUG: +if _DCCSI_GDEBUG: _LOGGER.info('Forced Info! for {0}.'.format({_PACKAGENAME})) _LOGGER.error('Forced ERROR! for {0}.'.format({_PACKAGENAME})) # debug breadcrumbs to check this module and used paths _LOGGER.debug('MODULE_PATH: {}'.format(_MODULE_PATH)) -_LOGGER.debug('LY_DEV_PATH: {}'.format(_LY_DEV)) +_LOGGER.debug('O3DE_DEV_PATH: {}'.format(_O3DE_DEV)) _LOGGER.debug('DCCSI_PATH: {}'.format(_DCCSIG_PATH)) -_LOGGER.debug('LY_PROJECT_TAG: {}'.format(_LY_PROJECT_NAME)) +_LOGGER.debug('O3DE_PROJECT_TAG: {}'.format(_O3DE_PROJECT)) _LOGGER.debug('DCCSI_LOG_PATH: {}'.format(_DCCSI_LOG_PATH)) +# ------------------------------------------------------------------------- # ------------------------------------------------------------------------- @@ -258,9 +254,9 @@ del _LOGGER # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True - if _G_DEBUG: + if _DCCSI_GDEBUG: print(_DCCSIG_PATH) test_imports() diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py index 2df85cbfaa..f6b71a7d97 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/config_utils.py @@ -18,14 +18,31 @@ import logging as _logging # -------------------------------------------------------------------------- -_PACKAGENAME = 'azpy.config_utils' +# note: this module is called in other root modules +# must avoid cyclical imports +# global scope +# normally would pull the constant envar string +# but avoiding cyclical imports here FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)" -_logging.basicConfig(level=_logging.INFO, - format=FRMT_LOG_LONG, - datefmt='%m-%d %H:%M') -_LOGGER = _logging.getLogger(_PACKAGENAME) -_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) +from azpy.env_bool import env_bool +_DCCSI_GDEBUG = env_bool('DCCSI_GDEBUG', False) +_DCCSI_LOGLEVEL = env_bool('DCCSI_LOGLEVEL', False) +_DCCSI_LOGLEVEL = int(env_bool('DCCSI_LOGLEVEL', int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) + +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.config_utils' + +# set up module logging +#for handler in _logging.root.handlers[:]: + #_logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +#_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER.propagate = False +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) __all__ = ['get_os', 'return_stub', 'get_stub_check_path', 'get_dccsi_config', 'get_current_project'] @@ -34,13 +51,15 @@ __all__ = ['get_os', 'return_stub', 'get_stub_check_path', # ------------------------------------------------------------------------- # just a quick check to ensure what paths have code access -_G_DEBUG = False # enable for debug prints -if _G_DEBUG: +if _DCCSI_GDEBUG: known_paths = list() for p in sys.path: known_paths.append(p) _LOGGER.debug(known_paths) +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- # this import can fail in Maya 2020 (and earlier) stuck on py2.7 # wrapped in a try, to trap and providing messaging to help user correct try: @@ -76,6 +95,15 @@ def get_os(): # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +from azpy.core import get_datadir +# there was a method here refactored out to add py2.7 support for Maya 2020 +#"DccScriptingInterface\azpy\core\py2\utils.py get_datadir()" +#"DccScriptingInterface\azpy\core\py3\utils.py get_datadir()" +# Warning: planning to deprecate py2 support +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- def return_stub_dir(stub_file='dccsi_stub'): _dir_to_last_file = None @@ -126,6 +154,30 @@ def get_stub_check_path(in_path=os.getcwd(), check_stub='engine.json'): # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +def get_o3de_engine_root(check_stub='engine.json'): + # get the O3DE engine root folder + # if we are running within O3DE we can ensure which engine is running + _O3DE_DEV = None + try: + import azlmbr # this file will fail outside of O3DE + except ImportError as e: + # if that fails, we can search up + # search up to get \dev + _O3DE_DEV = get_stub_check_path(check_stub='engine.json') + # To Do: What if engine.json doesn't exist? + else: + # execute if no exception + # allow for external ENVAR override + from azpy.constants import ENVAR_O3DE_DEV + _O3DE_DEV = Path(os.getenv(ENVAR_O3DE_DEV, azlmbr.paths.engroot)) + finally: + # note: can't use fstrings as this module gets called with py2.7 in maya + _LOGGER.info('O3DE engine root: {}'.format(_O3DE_DEV.resolve())) + return _O3DE_DEV +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- # settings.setenv() # doing this will add the additional DYNACONF_ envars def get_dccsi_config(dccsi_dirpath=return_stub_dir()): @@ -176,46 +228,75 @@ def get_current_project_cfg(dev_folder=get_stub_check_path()): # ------------------------------------------------------------------------- -def get_current_project(): +def get_check_global_project(): """Gets o3de project via .o3de data in user directory""" from azpy.constants import PATH_USER_O3DE_BOOTSTRAP from collections import OrderedDict from box import Box + from azpy.core import get_datadir bootstrap_box = None - - try: - bootstrap_box = Box.from_json(filename=str(Path(PATH_USER_O3DE_BOOTSTRAP).resolve()), - encoding="utf-8", - errors="strict", - object_pairs_hook=OrderedDict) - except Exception as e: - # this file runs in py2.7 for Maya 2020, FileExistsError is not defined - _LOGGER.error('FileExistsError: {}'.format(PATH_USER_O3DE_BOOTSTRAP)) - _LOGGER.error('exception is: {}'.format(e)) - + json_file_path = Path(PATH_USER_O3DE_BOOTSTRAP) + if json_file_path.exists(): + try: + bootstrap_box = Box.from_json(filename=str(json_file_path.resolve()), + encoding="utf-8", + errors="strict", + object_pairs_hook=OrderedDict) + except IOError as e: + # this file runs in py2.7 for Maya 2020, FileExistsError is not defined + _LOGGER.error('Bad file interaction: {}'.format(json_file_path.resolve())) + _LOGGER.error('Exception is: {}'.format(e)) + pass if bootstrap_box: # this seems fairly hard coded - what if the data changes? project_path=Path(bootstrap_box.Amazon.AzCore.Bootstrap.project_path) - return project_path.resolve() + return project_path else: return None # ------------------------------------------------------------------------- +# ------------------------------------------------------------------------- +def get_o3de_project_path(): + """figures out the o3de project path + if not found defaults to the engine folder""" + _O3DE_PROJECT_PATH = None + try: + import azlmbr # this file will fail outside of O3DE + except ImportError as e: + # (fallback 1) this checks if a global project is set + # This check user home for .o3de data + _O3DE_PROJECT_PATH = get_check_global_project() + else: + # execute if no exception, this would indicate we are in O3DE land + # allow for external ENVAR override + from azpy.constants import ENVAR_O3DE_PROJECT_PATH + _O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, azlmbr.paths.projectroot)) + finally: + # (fallback 2) if None, fallback to engine folder + if not _O3DE_PROJECT_PATH: + _O3DE_PROJECT_PATH = get_o3de_engine_root() + # note: can't use fstrings as this module gets called with py2.7 in maya + _LOGGER.info('O3DE project root: {}'.format(_O3DE_PROJECT_PATH.resolve())) + return _O3DE_PROJECT_PATH +# ------------------------------------------------------------------------- + + # ------------------------------------------------------------------------- def bootstrap_dccsi_py_libs(dccsi_dirpath=return_stub_dir()): """Builds and adds local site dir libs based on py version""" from azpy.constants import STR_DCCSI_PYTHON_LIB_PATH # a path string constructor - _DCCSI_PYTHON_LIB_PATH = STR_DCCSI_PYTHON_LIB_PATH.format(dccsi_dirpath, - sys.version_info[0], - sys.version_info[1]) + _DCCSI_PYTHON_LIB_PATH = Path(STR_DCCSI_PYTHON_LIB_PATH.format(dccsi_dirpath, + sys.version_info[0], + sys.version_info[1])) - if os.path.exists(_DCCSI_PYTHON_LIB_PATH): - _LOGGER.debug('Performed site.addsitedir({})'.format(_DCCSI_PYTHON_LIB_PATH)) - site.addsitedir(_DCCSI_PYTHON_LIB_PATH) # PYTHONPATH + if _DCCSI_PYTHON_LIB_PATH.exists(): + site.addsitedir(_DCCSI_PYTHON_LIB_PATH.resolve()) # PYTHONPATH + _LOGGER.debug('Performed site.addsitedir({})' + ''.format(_DCCSI_PYTHON_LIB_PATH.resolve())) return _DCCSI_PYTHON_LIB_PATH else: message = "Doesn't exist: {}".format(_DCCSI_PYTHON_LIB_PATH) @@ -243,13 +324,10 @@ if __name__ == '__main__': _config = get_dccsi_config() _LOGGER.info('DCCSI_CONFIG_PATH: {}'.format(_config)) - _LOGGER.info('LY_DEV: {}'.format(get_stub_check_path('engine.json'))) - - # this will be deprecated and shouldn't work soon (returns None) - _LOGGER.info('LY_PROJECT: {}'.format(get_current_project_cfg(get_stub_check_path('bootstrap.cfg')))) + _LOGGER.info('O3DE_DEV: {}'.format(get_o3de_engine_root(check_stub='engine.json'))) # new o3de version - _LOGGER.info('LY_PROJECT: {}'.format(get_current_project())) + _LOGGER.info('O3DE_PROJECT: {}'.format(get_check_global_project())) _LOGGER.info('DCCSI_PYTHON_LIB_PATH: {}'.format(bootstrap_dccsi_py_libs(return_stub_dir('dccsi_stub')))) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py index b9619fa584..d23601a33d 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/constants.py @@ -25,7 +25,16 @@ import sys import site from os.path import expanduser import logging as _logging +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +# global scope +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.constants' + +os.environ['PYTHONINSPECT'] = 'True' # for this module to perform standalone # we need to set up basic access to the DCCsi _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? @@ -33,10 +42,10 @@ _DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../..')) _DCCSIG_PATH = os.getenv('DCCSIG_PATH', _DCCSIG_PATH) site.addsitedir(_DCCSIG_PATH) -# azpy module -#import azpy.constants as cnst +# now we have azpy api access +import azpy +from azpy.env_bool import env_bool from azpy.config_utils import return_stub_dir -import azpy.env_bool as env_bool # ------------------------------------------------------------------------- @@ -49,24 +58,26 @@ ENVAR_DCCSI_LOGLEVEL = str('DCCSI_LOGLEVEL') # Log formating FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)" FRMT_LOG_SHRT = "[%(asctime)s][%(name)s][%(levelname)s] >> %(message)s" +# ------------------------------------------------------------------------- -# global space -_G_DEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) +# ------------------------------------------------------------------------- +# global debug stuff +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_LOGLEVEL = int(env_bool(ENVAR_DCCSI_LOGLEVEL, int(20))) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# set up module logging for handler in _logging.root.handlers[:]: _logging.root.removeHandler(handler) - -_PACKAGENAME = 'azpy.constants' - -_LOG_LEVEL = int(20) -if _G_DEBUG: - _LOG_LEVEL = int(10) -_logging.basicConfig(level=_LOG_LEVEL, - format=FRMT_LOG_LONG, - datefmt='%m-%d %H:%M') -_LOGGER = _logging.getLogger(_PACKAGENAME) -_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -80,24 +91,25 @@ STR_CROSSBAR_RL = str('{0}\r'.format(STR_CROSSBAR)) STR_CROSSBAR_NL = str('{0}\n'.format(STR_CROSSBAR)) # some common str tags -TAG_DEFAULT_COMPANY = str('Amazon.Lumberyard') +TAG_DEFAULT_COMPANY = str('Amazon.O3DE') TAG_DEFAULT_PROJECT = str('DccScriptingInterface') +TAG_DCCSI_NICKNAME = str('DCCsi') TAG_MOCK_PROJECT = str('MockProject') -TAG_DIR_LY_DEV = str('dev') +TAG_DIR_O3DE_DEV = str('dev') TAG_DIR_DCCSI_AZPY = str('azpy') -TAG_DIR_DCCSI_SDK = str('SDK') -TAG_DIR_LY_BUILD = str('build') +TAG_DIR_DCCSI_TOOLS = str('Tools') +TAG_DIR_O3DE_BUILD_FOLDER = str('build') TAG_QT_PLUGIN_PATH = str('QT_PLUGIN_PATH') TAG_O3DE_FOLDER = str('.o3de') TAG_O3DE_BOOTSTRAP = str('bootstrap.setreg') +TAG_DCCSI_CONFIG = str('dccsiconfiguration.setreg') # filesystem markers, stub file names. -STUB_LY_DEV = str('engine.json') -STUB_LY_ROOT_PROJECT = str('ly_project_stub') -STUB_LY_ROOT_DCCSI = str('dccsi_stub') -STUB_LY_DCCSI_AZPY = str('dccsi_azpy_stub') -STUB_LY_DCCSI_SDK = str('dccsi_sdk_stub') +STUB_O3DE_DEV = str('engine.json') +STUB_O3DE_ROOT_DCCSI = str('dccsi_stub') +STUB_O3DE_DCCSI_AZPY = str('dccsi_azpy_stub') +STUB_O3DE_DCCSI_TOOLS = str('dccsi_tools_stub') # config string consts, Meta Qualifiers QUALIFIER_COMMENT = str('_meta_COMMENT') @@ -135,18 +147,18 @@ PATH_PROGRAMFILES_X64 = str(os.environ['PROGRAMFILES']) # base env var key as str ENVAR_COMPANY = str('COMPANY') -ENVAR_LY_PROJECT = str('LY_PROJECT') -ENVAR_LY_PROJECT_PATH = str('LY_PROJECT_PATH') -ENVAR_LY_DEV = str('LY_DEV') +ENVAR_O3DE_PROJECT = str('O3DE_PROJECT') +ENVAR_O3DE_PROJECT_PATH = str('O3DE_PROJECT_PATH') +ENVAR_O3DE_DEV = str('O3DE_DEV') ENVAR_DCCSIG_PATH = str('DCCSIG_PATH') ENVAR_DCCSI_AZPY_PATH = str('DCCSI_AZPY_PATH') -ENVAR_DCCSI_SDK_PATH = str('DCCSI_SDK_PATH') -ENVAR_LY_BUILD_DIR_NAME = str('LY_BUILD_DIR_NAME') +ENVAR_DCCSI_TOOLS_PATH = str('DCCSI_TOOLS_PATH') +ENVAR_O3DE_BUILD_DIR_NAME = str('O3DE_BUILD_DIR_NAME') -ENVAR_LY_BUILD_PATH = str('LY_BUILD_PATH') +ENVAR_O3DE_BUILD_PATH = str('O3DE_BUILD_PATH') ENVAR_QT_PLUGIN_PATH = TAG_QT_PLUGIN_PATH ENVAR_QTFORPYTHON_PATH = str('QTFORPYTHON_PATH') -ENVAR_LY_BIN_PATH = str('LY_BIN_PATH') +ENVAR_O3DE_BIN_PATH = str('O3DE_BIN_PATH') ENVAR_DCCSI_LOG_PATH = str('DCCSI_LOG_PATH') ENVAR_DCCSI_LAUNCHERS_PATH = str('DCCSI_LAUNCHERS_PATH') @@ -155,7 +167,7 @@ ENVAR_DCCSI_PY_VERSION_MAJOR = str('DCCSI_PY_VERSION_MAJOR') ENVAR_DCCSI_PY_VERSION_MINOR = str('DCCSI_PY_VERSION_MINOR') ENVAR_DCCSI_PYTHON_PATH = str('DCCSI_PYTHON_PATH') ENVAR_DCCSI_PYTHON_LIB_PATH = str('DCCSI_PYTHON_LIB_PATH') -ENVAR_DCCSI_PYTHON_INSTALL = str('DCCSI_PYTHON_INSTALL') +ENVAR_O3DE_PYTHON_INSTALL = str('O3DE_PYTHON_INSTALL') ENVAR_WINGHOME = str('WINGHOME') ENVAR_DCCSI_WING_VERSION_MAJOR = str('DCCSI_WING_VERSION_MAJOR') @@ -169,7 +181,7 @@ ENVAR_DCCSI_PY_DEFAULT = str('DCCSI_PY_DEFAULT') ENVAR_DCCSI_MAYA_VERSION = str('DCCSI_MAYA_VERSION') ENVAR_MAYA_LOCATION = str('MAYA_LOCATION') -ENVAR_DCCSI_SDK_MAYA_PATH = str('DCCSI_SDK_MAYA_PATH') +ENVAR_DCCSI_TOOLS_MAYA_PATH = str('DCCSI_TOOLS_MAYA_PATH') ENVAR_MAYA_MODULE_PATH = str('MAYA_MODULE_PATH') ENVAR_MAYA_BIN_PATH = str('MAYA_BIN_PATH') @@ -188,33 +200,33 @@ ENVAR_MAYA_SCRIPT_PATH = str('MAYA_SCRIPT_PATH') ENVAR_DCCSI_MAYA_SET_CALLBACKS = str('DCCSI_MAYA_SET_CALLBACKS') -TAG_LY_DCC_MAYA_MEL = 'dccsi_setup.mel' +TAG_O3DE_DCC_MAYA_MEL = 'dccsi_setup.mel' TAG_MAYA_WORKSPACE = 'workspace.mel' # dcc scripting interface common and default paths -PATH_LY_DEV = str(return_stub_dir(STUB_LY_DEV)) -PATH_DCCSIG_PATH = str(return_stub_dir(STUB_LY_ROOT_DCCSI)) -PATH_DCCSI_AZPY_PATH = str(return_stub_dir(STUB_LY_DCCSI_AZPY)) -PATH_DCCSI_SDK_PATH = str('{0}\\{1}'.format(PATH_DCCSIG_PATH, TAG_DIR_DCCSI_SDK)) +PATH_O3DE_DEV = str(return_stub_dir(STUB_O3DE_DEV)) +PATH_DCCSIG_PATH = str(return_stub_dir(STUB_O3DE_ROOT_DCCSI)) +PATH_DCCSI_AZPY_PATH = str(return_stub_dir(STUB_O3DE_DCCSI_AZPY)) +PATH_DCCSI_TOOLS_PATH = str('{0}\\{1}'.format(PATH_DCCSIG_PATH, TAG_DIR_DCCSI_TOOLS)) # logging into the cache -PATH_DCCSI_LOG_PATH = str('{LY_DEV}\\Cache\\{LY_PROJECT}\\pc\\user\\log\\logs') +PATH_DCCSI_LOG_PATH = str('{O3DE_PROJECT_PATH}\\user\\log\{TAG_DCCSI_NICKNAME}') # dev \ \ -STR_CONSTRUCT_LY_BUILD_PATH = str('{0}\\{1}') -PATH_LY_BUILD_PATH = str(STR_CONSTRUCT_LY_BUILD_PATH.format(PATH_LY_DEV, - TAG_DIR_LY_BUILD)) +STR_CONSTRUCT_O3DE_BUILD_PATH = str('{0}\\{1}') +PATH_O3DE_BUILD_PATH = str(STR_CONSTRUCT_O3DE_BUILD_PATH.format(PATH_O3DE_DEV, + TAG_DIR_O3DE_BUILD_FOLDER)) # ENVAR_QT_PLUGIN_PATH = TAG_QT_PLUGIN_PATH STR_QTPLUGIN_DIR = str('{0}\\bin\\profile\\EditorPlugins') STR_QTFORPYTHON_PATH = str('{0}\\Gems\\QtForPython\\3rdParty\\pyside2\\windows\\release') -STR_LY_BIN_PATH = str('{0}\\bin\\profile') +STR_O3DE_BIN_PATH = str('{0}\\bin\\profile') -PATH_LY_BUILD_PATH = str('{0}\\{1}'.format(PATH_LY_DEV, TAG_DIR_LY_BUILD)) -PATH_QTFORPYTHON_PATH = str(STR_QTFORPYTHON_PATH.format(PATH_LY_DEV)) -PATH_QT_PLUGIN_PATH = str(STR_QTPLUGIN_DIR).format(PATH_LY_BUILD_PATH) -PATH_LY_BIN_PATH = str(STR_LY_BIN_PATH).format(PATH_LY_BUILD_PATH) +PATH_O3DE_BUILD_PATH = str('{0}\\{1}'.format(PATH_O3DE_DEV, TAG_DIR_O3DE_BUILD_FOLDER)) +PATH_QTFORPYTHON_PATH = str(STR_QTFORPYTHON_PATH.format(PATH_O3DE_DEV)) +PATH_QT_PLUGIN_PATH = str(STR_QTPLUGIN_DIR).format(PATH_O3DE_BUILD_PATH) +PATH_O3DE_BIN_PATH = str(STR_O3DE_BIN_PATH).format(PATH_O3DE_BUILD_PATH) # py path string, parts, etc. TAG_DEFAULT_PY = str('Launch_pyBASE.bat') @@ -233,12 +245,19 @@ parts = os.path.split(PATH_USER_HOME) if str(parts[1].lower()) == 'documents': PATH_USER_HOME = parts[0] _LOGGER.debug('user home CORRECTED: {}'.format(PATH_USER_HOME)) + +STR_USER_O3DE_PATH = str('{home}\\{o3de}') -PATH_USER_O3DE = str('{home}\\{o3de}').format(home=PATH_USER_HOME, +PATH_USER_O3DE = str(STR_USER_O3DE_PATH).format(home=PATH_USER_HOME, o3de=TAG_O3DE_FOLDER) -PATH_USER_O3DE_REGISTRY = str('{0}\\Registry').format(PATH_USER_O3DE) -PATH_USER_O3DE_BOOTSTRAP = str('{reg}\\{file}').format(reg=PATH_USER_O3DE_REGISTRY, - file=TAG_O3DE_BOOTSTRAP) + +TAG_DIR_REGISTRY = str('Registry') +STR_USER_O3DE_REGISTRY_PATH = str('{0}\\{1}') +PATH_USER_O3DE_REGISTRY = str(STR_USER_O3DE_REGISTRY_PATH).format(PATH_USER_O3DE, TAG_DIR_REGISTRY) + +STR_USER_O3DE_BOOTSTRAP_PATH = str('{reg}\\{file}') +PATH_USER_O3DE_BOOTSTRAP = str(STR_USER_O3DE_BOOTSTRAP_PATH).format(reg=PATH_USER_O3DE_REGISTRY, + file=TAG_O3DE_BOOTSTRAP) #python and site-dir TAG_DCCSI_PY_VERSION_MAJOR = str(3) @@ -247,8 +266,8 @@ TAG_DCCSI_PY_VERSION_RELEASE = str(10) TAG_PYTHON_EXE = str('python.exe') TAG_TOOLS_DIR = str('Tools\\Python') TAG_PLATFORM = str('windows') -STR_CONSTRUCT_DCCSI_PYTHON_INSTALL = str('{0}\\{1}\\{2}.{3}.{4}\\{5}') -PATH_DCCSI_PYTHON_PATH = str(STR_CONSTRUCT_DCCSI_PYTHON_INSTALL.format(PATH_LY_DEV, +STR_CONSTRUCT_O3DE_PYTHON_INSTALL = str('{0}\\{1}\\{2}.{3}.{4}\\{5}') +PATH_DCCSI_PYTHON_PATH = str(STR_CONSTRUCT_O3DE_PYTHON_INSTALL.format(PATH_O3DE_DEV, TAG_TOOLS_DIR, TAG_DCCSI_PY_VERSION_MAJOR, TAG_DCCSI_PY_VERSION_MINOR, @@ -290,19 +309,12 @@ PATH_SAT_INSTALL_PATH = str('{0}\\{1}\\{2}\\{3}\\{4}' # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - # there are not really tests to run here due to this being a list of - # constants for shared use. - _G_DEBUG = True - _DCCSI_DEV_MODE = True - _LOGGER.setLevel(_logging.DEBUG) # force debugging - - # this is a top level module and to reduce cyclical azpy imports - # it only has a basic logger configured, add log to console - _handler = _logging.StreamHandler(sys.stdout) - _handler.setLevel(_logging.DEBUG) - _formatter = _logging.Formatter(FRMT_LOG_LONG) - _handler.setFormatter(_formatter) - _LOGGER.addHandler(_handler) + """Run this file as a standalone script""" + + # overide logger for standalone to be more verbose and lof to file + _LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) # happy print _LOGGER.info(STR_CROSSBAR) @@ -321,15 +333,15 @@ if __name__ == '__main__': from pathlib import Path _stash_dict = {} - _stash_dict['LY_DEV'] = Path(PATH_LY_DEV) + _stash_dict['O3DE_DEV'] = Path(PATH_O3DE_DEV) _stash_dict['DCCSIG_PATH'] = Path(PATH_DCCSIG_PATH) _stash_dict['DCCSI_AZPY_PATH'] = Path(PATH_DCCSI_AZPY_PATH) - _stash_dict['DCCSI_SDK_PATH'] = Path(PATH_DCCSI_SDK_PATH) + _stash_dict['DCCSI_TOOLS_PATH'] = Path(PATH_DCCSI_TOOLS_PATH) _stash_dict['DCCSI_PYTHON_PATH'] = Path(PATH_DCCSI_PYTHON_PATH) _stash_dict['DCCSI_PY_BASE'] = Path(PATH_DCCSI_PY_BASE) _stash_dict['DCCSI_PYTHON_LIB_PATH'] = Path(PATH_DCCSI_PYTHON_LIB_PATH) - _stash_dict['LY_BUILD_PATH'] = Path(PATH_LY_BUILD_PATH) - _stash_dict['LY_BIN_PATH'] = Path(PATH_LY_BIN_PATH) + _stash_dict['O3DE_BUILD_PATH'] = Path(PATH_O3DE_BUILD_PATH) + _stash_dict['O3DE_BIN_PATH'] = Path(PATH_O3DE_BIN_PATH) _stash_dict['QTFORPYTHON_PATH'] = Path(PATH_QTFORPYTHON_PATH) _stash_dict['QT_PLUGIN_PATH'] = Path(PATH_QT_PLUGIN_PATH) _stash_dict['SAT_INSTALL_PATH'] = Path(PATH_SAT_INSTALL_PATH) @@ -339,7 +351,7 @@ if __name__ == '__main__': # py 2 and 3 compatible iter def get_items(dict_object): for key in dict_object: - yield key, dict_object[key] + yield key, dict_object[key] for key, value in get_items(_stash_dict): # check if path exists diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/__init__.py new file mode 100644 index 0000000000..9269739f96 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/__init__.py @@ -0,0 +1,42 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +__copyright__ = "Copyright 2021, Amazon" +# ------------------------------------------------------------------------- +import sys +import logging as _logging +# ------------------------------------------------------------------------- + +#pulling from azpy.constants causes cyclical imports :( +FRMT_LOG_LONG = "[%(name)s][%(levelname)s] >> %(message)s (%(asctime)s; %(filename)s:%(lineno)d)" +_DCCSI_GDEBUG = False +_DCCSI_LOGLEVEL = int(20) +if _DCCSI_GDEBUG: + _DCCSI_LOGLEVEL = int(10) + +_PACKAGENAME = 'azpy.core' +_logging.basicConfig(format=FRMT_LOG_LONG, level=_DCCSI_LOGLEVEL) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = [] + +if sys.version_info >= (3, 6): + from azpy.core.py3.utils import get_datadir +elif sys.version_info >= (2, 6) and sys.version_info < (3, 6): + _LOGGER.warning('Python vesion < 3 will be deprecated in the future') + from azpy.core.py2.utils import get_datadir +else: + _LOGGER.warning('Python vesion < 2.6 not recommended') + from azpy.core.py2.utils import get_datadir + +__all__ = ['get_datadir'] # you should hope it works + +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/__init__.py new file mode 100644 index 0000000000..35727ddda6 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/__init__.py @@ -0,0 +1,20 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +__copyright__ = "Copyright 2021, Amazon" +# ------------------------------------------------------------------------- +import logging as _logging +# ------------------------------------------------------------------------- +_PACKAGENAME = 'azpy.core.py2' +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = [] +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/utils.py new file mode 100644 index 0000000000..3a79b1bccf --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py2/utils.py @@ -0,0 +1,59 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +"""DCCsi.azpy.core.py2.utils +This module contain versions of utils speciufic to py3 snyntax""" +import sys +import logging as _logging + +try: + import pathlib +except: + import pathlib2 as pathlib + +__all__ = ['get_datadir'] + +_MODULENAME = 'azpy.core.py2.utils' +_LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) + +# ------------------------------------------------------------------------- +def get_datadir(): + """ + persistent application data. + # linux: ~/.local/share + # macOS: ~/Library/Application Support + # windows: C:/Users//AppData/Roaming + """ + + home = pathlib.Path.home() + + if sys.platform.startswith('win'): + datadir = pathlib.Path(home, "AppData/Roaming") + return datadir + elif sys.platform == "linux": + datadir = pathlib.Path(home, ".local/share") + return datadir + elif sys.platform == "darwin": + datadir = pathlib.Path(home, "Library/Application Support") + return datadir + else: # unknown + return None +# ------------------------------------------------------------------------- + + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """module testing""" + + user_data_dir = get_datadir() + _LOGGER.info(user_data_dir.resolve()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/__init__.py new file mode 100644 index 0000000000..f5d26c9ebe --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/__init__.py @@ -0,0 +1,20 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +__copyright__ = "Copyright 2021, Amazon" +# ------------------------------------------------------------------------- +import logging as _logging +# ------------------------------------------------------------------------- +_PACKAGENAME = 'azpy.core.py3' +_LOGGER = _logging.getLogger(_PACKAGENAME) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = [] +# ------------------------------------------------------------------------- \ No newline at end of file diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/utils.py new file mode 100644 index 0000000000..cca07eb7dc --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/core/py3/utils.py @@ -0,0 +1,52 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- +"""DCCsi.azpy.core.py3.utils +This module contain versions of utils speciufic to py3 snyntax""" +import sys +import pathlib +import logging as _logging + +__all__ = ['get_datadir'] + +_MODULENAME = 'azpy.core.py3.utils' +_LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) + +# ------------------------------------------------------------------------- +def get_datadir() -> pathlib.Path: + """ + persistent application data. + # linux: ~/.local/share + # macOS: ~/Library/Application Support + # windows: C:/Users//AppData/Roaming + """ + + home = pathlib.Path.home() + + if sys.platform.startswith('win'): + return home / "AppData/Roaming" + elif sys.platform == "linux": + return home / ".local/share" + elif sys.platform == "darwin": + return home / "Library/Application Support" + else: # unknown + return None +# ------------------------------------------------------------------------- + + +########################################################################### +# Main Code Block, runs this script as main (testing) +# ------------------------------------------------------------------------- +if __name__ == '__main__': + """module testing""" + + user_data_dir = get_datadir() + _LOGGER.info(user_data_dir.resolve()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/3dsmax/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/3dsmax/__init__.py old mode 100755 new mode 100644 similarity index 71% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/3dsmax/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/3dsmax/__init__.py index 017730d63c..a98de89ad7 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/3dsmax/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/3dsmax/__init__.py @@ -12,29 +12,53 @@ # importing all of the modules """azpy.3dsmax.__init__""" -import os -from azpy.env_bool import env_bool +import logging as _logging + +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.3dsmax' + _PACKAGENAME = 'azpy.dcc.3dsmax' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + # ------------------------------------------------------------------------- +def init(): + """If the 3dsmax api is required for a package/module to import, + then it should be initialized and added here so general imports + don't fail""" + + # Make sure we can import the native apis + import pymxs + import MaxPlus + + # extend all with submodules + #__all__.append('foo', 'bar') + + # Importing local packages/modules + pass +# ------------------------------------------------------------------------- + # ------------------------------------------------------------------------- if _DCCSI_DEV_MODE: @@ -46,21 +70,4 @@ if _DCCSI_DEV_MODE: _logger=_LOGGER) # ------------------------------------------------------------------------- - -# ------------------------------------------------------------------------- -def init(): - """If the 3dsmax api is required for a package/module to import, - then it should be initialized and added here so general imports - don't fail""" - - # __all__.append() - - # Make sure we can import the native apis - import pymxs - import MaxPlus - - # Importing local packages/modules - pass -# ------------------------------------------------------------------------- - del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/__init__.py new file mode 100644 index 0000000000..7f2b402c95 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/__init__.py @@ -0,0 +1,53 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 line is 75 characters ------------------------------------------- + +"""azpy.shared.__init__""" + +import logging as _logging + +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG + +# global space +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_PACKAGENAME = __name__ +if _PACKAGENAME is '__main__': + _PACKAGENAME = 'azpy.dcc' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +# ------------------------------------------------------------------------- +# These are explicit imports for now +__all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +if _DCCSI_DEV_MODE: + # If in dev mode this will test imports of __all__ + from azpy import test_imports + _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) + test_imports(__all__, + _pkg=_PACKAGENAME, + _logger=_LOGGER) +# ------------------------------------------------------------------------- + +del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/blender/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/blender/__init__.py old mode 100755 new mode 100644 similarity index 71% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/blender/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/blender/__init__.py index 903b76e3c4..b46c7146c2 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/blender/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/blender/__init__.py @@ -12,28 +12,49 @@ # importing all of the modules """azpy.blender.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.blender' - -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -#_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) + _PACKAGENAME = 'azpy.dcc.blender' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +def init(): + """If the blender bpy api is required for a package/module to import, + then it should be initialized and added here so general imports + don't fail""" + + # Make sure we can import the native apis + import bpy + + # extend all with submodules + #__all__.append('foo', 'bar') + + # Importing local packages/modules + pass # ------------------------------------------------------------------------- @@ -47,20 +68,4 @@ if _DCCSI_DEV_MODE: _logger=_LOGGER) # ------------------------------------------------------------------------- - -# ------------------------------------------------------------------------- -def init(): - """If the blender bpy api is required for a package/module to import, - then it should be initialized and added here so general imports - don't fail""" - - # __all__.append() - - # Make sure we can import the native apis - import bpy - - # Importing local packages/modules - pass -# ------------------------------------------------------------------------- - del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/houdini/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/houdini/__init__.py old mode 100755 new mode 100644 similarity index 70% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/houdini/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/houdini/__init__.py index c0b1ced437..0c16872073 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/houdini/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/houdini/__init__.py @@ -12,28 +12,32 @@ # importing all of the modules """azpy.houdini.__init__""" -import os +import logging as _logging -from azpy import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.houdini' + _PACKAGENAME = 'azpy.dcc.houdini' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -#_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,12 +57,13 @@ def init(): """If the houdini api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis import hou - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/marmoset/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/marmoset/__init__.py old mode 100755 new mode 100644 similarity index 70% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/marmoset/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/marmoset/__init__.py index 35a081dcbf..7e95b5f19e --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/marmoset/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/marmoset/__init__.py @@ -12,28 +12,32 @@ # importing all of the modules """azpy.houdini.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.marmoset' + _PACKAGENAME = 'azpy.dcc.marmoset' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -53,12 +57,13 @@ def init(): """If the marmoset api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis import mset - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/__init__.py old mode 100755 new mode 100644 similarity index 64% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/__init__.py index e66ee63739..870ace6166 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/__init__.py @@ -10,40 +10,32 @@ # -- This line is 75 characters ------------------------------------------- # The __init__.py files help guide import statements without automatically # importing all of the modules -"""azpy.maya.__init__""" +"""azpy.dcc.maya.__init__""" -from azpy.env_bool import env_bool +import logging as _logging + +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya' + _PACKAGENAME = 'azpy.dcc.maya' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - -# ------------------------------------------------------------------------- +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = [] -# ------------------------------------------------------------------------- - - -# ------------------------------------------------------------------------- -if _DCCSI_DEV_MODE: - # If in dev mode this will test imports of __all__ - from azpy import test_imports - _LOGGER.debug('Testing Imports from {0}'.format(_PACKAGENAME)) - test_imports(__all__, - _pkg=_PACKAGENAME, - _logger=_LOGGER) -# ------------------------------------------------------------------------- - - # ------------------------------------------------------------------------- def init(): """If the maya api is required for a package/module to import, diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/__init__.py old mode 100755 new mode 100644 similarity index 64% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/__init__.py index 3f36bb0fba..7de93fe2bb --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/__init__.py @@ -12,23 +12,26 @@ # importing all of the modules """azpy.maya.callbacks.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.maya.callbacks' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = ['event_callback_handler', 'node_message_callback_handler', diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/event_callback_handler.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/event_callback_handler.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/event_callback_handler.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/event_callback_handler.py index 7603e969e3..d2ca596d3c --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/event_callback_handler.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/event_callback_handler.py @@ -73,12 +73,12 @@ import maya.api.OpenMaya as openmaya #-------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks.event_callback_handler' + _PACKAGENAME = 'azpy.dcc.maya.callbacks.event_callback_handler' _LOGGER = azpy.initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/node_message_callback_handler.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/node_message_callback_handler.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/node_message_callback_handler.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/node_message_callback_handler.py index cfb883be18..e41c9dbbc7 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/node_message_callback_handler.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/node_message_callback_handler.py @@ -80,12 +80,12 @@ import maya.cmds as mc # ------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks.event_callback_handler' + _PACKAGENAME = 'azpy.dcc.maya.callbacks.event_callback_handler' _LOGGER = azpy.initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/on_shader_rename.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/on_shader_rename.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/on_shader_rename.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/on_shader_rename.py index 84e2426c2b..834505f076 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/callbacks/on_shader_rename.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/callbacks/on_shader_rename.py @@ -85,12 +85,12 @@ import maya.cmds as mc # -------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks.on_shader_rename' + _PACKAGENAME = 'azpy.dcc.maya.callbacks.on_shader_rename' _LOGGER = azpy.initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/__init__.py old mode 100755 new mode 100644 similarity index 55% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/__init__.py index 7dab357d22..ecb164c1f7 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/__init__.py @@ -10,25 +10,28 @@ # -- This line is 75 characters ------------------------------------------- # The __init__.py files help guide import statements without automatically # importing all of the modules -"""azpy.maya.helpers.__init__""" +"""azpy.dcc.maya.helpers.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.callbacks' + _PACKAGENAME = 'azpy.dcc.maya.callbacks' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = ['undo_context', 'utils'] diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/undo_context.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/undo_context.py old mode 100755 new mode 100644 similarity index 97% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/undo_context.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/undo_context.py index 90c12af2c9..7a6de2224b --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/undo_context.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/undo_context.py @@ -42,12 +42,12 @@ import maya.cmds as mc # ------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.helpers.undo_context' + _PACKAGENAME = 'azpy.dcc.maya.helpers.undo_context' _LOGGER = initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/utils.py old mode 100755 new mode 100644 similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/utils.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/utils.py index 5fe6cd5876..5f1522e6e1 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/helpers/utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/helpers/utils.py @@ -10,7 +10,7 @@ # -- This line is 75 characters ------------------------------------------- """ -azpy.maya utility module +azpy.dcc.maya utility module """ # ------------------------------------------------------------------------- # built in's @@ -32,12 +32,12 @@ import maya.cmds as cmds # ------------------------------------------------------------------------- # -- Misc Global Space Definitions -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.helpers.undo_context' + _PACKAGENAME = 'azpy.dcc.maya.helpers.undo_context' _LOGGER = initialize_logger(_PACKAGENAME, default_log_level=int(20)) _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) @@ -46,7 +46,7 @@ _LOGGER.debug('Invoking:: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # Initiate the Wing IDE debug connection. -if _G_DEBUG: +if _DCCSI_GDEBUG: #import azpy.dev.connectDebugger as lyDevConnnect # lyDevConnnect() pass diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/__init__.py new file mode 100644 index 0000000000..41f177bcf0 --- /dev/null +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/__init__.py @@ -0,0 +1,38 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# -------------------------------------------------------------------------- +"""azpy.dcc.maya.toolbits.__init__""" + +import logging as _logging + +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_DCCSI_GDEBUG +from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG + +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) + +_PACKAGENAME = __name__ +if _PACKAGENAME is '__main__': + _PACKAGENAME = 'azpy.dcc.maya.toolbits' + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + +__all__ = ['detach'] + +del _LOGGER +#-------------------------------------------------------------------------- + diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/detach.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/detach.py old mode 100755 new mode 100644 similarity index 94% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/detach.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/detach.py index 1afcbfa9f6..51d3bb9b66 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/detach.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/toolbits/detach.py @@ -44,12 +44,12 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.toolbits.detatch' + _PACKAGENAME = 'azpy.dcc.maya.toolbits.detatch' import azpy _LOGGER = azpy.initialize_logger(_PACKAGENAME) @@ -65,13 +65,13 @@ def clean_detach(detachType=0, args=None, name=None, or duplicating those faces without harming the orignal ''' - sel = azpy.maya.helpers.utils.Selection() + sel = azpy.dcc.maya.helpers.utils.Selection() for obj in sel.selection.keys(): print("~ cleanDetach:: Working on: {0}".format(obj)) # set up / open the maya undo context - with azpy.maya.helpers.UndoContext(): + with azpy.dcc.maya.helpers.UndoContext(): if deletHistoyIn: mc.delete( obj, constructionHistory = True) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/__init__.py similarity index 100% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/__init__.py diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/execute_wing_code.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/execute_wing_code.py similarity index 95% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/execute_wing_code.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/execute_wing_code.py index 34aaa2cbb7..3c8e7a75a3 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/execute_wing_code.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/execute_wing_code.py @@ -43,13 +43,13 @@ def get_stub_check_path(in_path=__file__, check_stub='engineroot.txt'): # ------------------------------------------------------------------------- # -- Global Definitions -- -_MODULENAME = 'azpy.maya.utils.execute_wing_code' +_MODULENAME = 'azpy.dcc.maya.utils.execute_wing_code' _LOGGER = _logging.getLogger(_MODULENAME) -_LY_DEV = get_stub_check_path() -_LOGGER.info('_LY_DEV: {}'.format(_LY_DEV)) +_O3DE_DEV = get_stub_check_path() +_LOGGER.info('_O3DE_DEV: {}'.format(_O3DE_DEV)) -_PROJ_CACHE = os.path.join(_LY_DEV, 'cache', 'DCCsi', 'wing') +_PROJ_CACHE = os.path.join(_O3DE_DEV, 'cache', 'DCCsi', 'wing') _LOGGER.info('_PROJ_CACHE: {}'.format(_PROJ_CACHE)) _LOCAL_HOST = socket.gethostbyname(socket.gethostname()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/simple_command_port.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/simple_command_port.py similarity index 98% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/simple_command_port.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/simple_command_port.py index 659cae985d..3a130cd85f 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/simple_command_port.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/simple_command_port.py @@ -24,7 +24,7 @@ import logging as _logging # -------------------------------------------------------------------------- # -- Global Definitions -- -_MODULENAME = 'azpy.maya.utils.simple_command_port' +_MODULENAME = 'azpy.dcc.maya.utils.simple_command_port' _LOGGER = _logging.getLogger(_MODULENAME) _LOCAL_HOST = socket.gethostbyname(socket.gethostname()) @@ -205,7 +205,7 @@ class SimpleCommandPort: # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/wing_to_maya.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/wing_to_maya.py similarity index 99% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/wing_to_maya.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/wing_to_maya.py index f170090823..0c06b912d9 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/utils/wing_to_maya.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/maya/utils/wing_to_maya.py @@ -24,7 +24,7 @@ from simple_command_port import SimpleCommandPort # -------------------------------------------------------------------------- # -- Global Definitions -- -_MODULENAME = 'azpy.maya.utils.wing_to_maya' +_MODULENAME = 'azpy.dcc.maya.utils.wing_to_maya' _LOGGER = _logging.getLogger(_MODULENAME) _LOCAL_HOST = socket.gethostbyname(socket.gethostname()) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/lumberyard/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/__init__.py old mode 100755 new mode 100644 similarity index 68% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/lumberyard/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/__init__.py index 9da4e8975a..95aa781557 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/lumberyard/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/__init__.py @@ -11,30 +11,50 @@ # The __init__.py files help guide import statements without automatically # importing all of the modules """azpy.lumberyard.__init__ -All Lumberyard render related packages/modules should live here.""" +All O3DE related extension packages/modules should live here.""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.lumberyard' + _PACKAGENAME = 'azpy.dcc.o3de' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +def init(): + """If the lumberyard azlmbr api is required for a package/module to + import, then it should be initialized and added here so general imports + don't fail""" + + import azlmbr + + # extend all with submodules + __all__.append('atom') + + # Importing local packages/modules + pass # ------------------------------------------------------------------------- @@ -49,19 +69,4 @@ if _DCCSI_DEV_MODE: # ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- -def init(): - """If the lumberyard azlmbr api is required for a package/module to - import, then it should be initialized and added here so general imports - don't fail""" - - # __all__.append() - - # Make sure we can import the native apis - #import - - # Importing local packages/modules - pass -# ------------------------------------------------------------------------- - del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/render/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/atom/__init__.py old mode 100755 new mode 100644 similarity index 72% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/render/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/atom/__init__.py index a6eb9ea373..ecd1db3ef0 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/render/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/o3de/atom/__init__.py @@ -14,28 +14,33 @@ This package generically uses 'render' to refer to Atom (which is a code name.) All Atom render related packages/modules should live here.""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) + _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.render' + _PACKAGENAME = 'azpy.dcc.o3de.atom' - -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] - +# To Do: procedurally discover dcc access and extend __all__ # ------------------------------------------------------------------------- @@ -55,12 +60,13 @@ def init(): """If the atom render api is required for a package/module to import, then it should be initialized and added here so general imports don't fail""" - - # __all__.append() # Make sure we can import the native apis # import - + + # extend all with submodules + #__all__.append('foo', 'bar') + # Importing local packages/modules pass # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/substance/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/substance/__init__.py old mode 100755 new mode 100644 similarity index 71% rename from Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/substance/__init__.py rename to Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/substance/__init__.py index 8f9d542906..1e5ee058cc --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/substance/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dcc/substance/__init__.py @@ -12,28 +12,49 @@ # importing all of the modules """azpy.substance.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.substance' + _PACKAGENAME = 'azpy.dcc.substance' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- - +# These are explicit imports for now __all__ = [] +# To Do: procedurally discover dcc access and extend __all__ +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +def init(): + """If the substance api is required for a package/module to import, + then it should be initialized and added here so general imports + don't fail""" + + # Make sure we can import the native apis + # import + + # extend all with submodules + #__all__.append('foo', 'bar') + + # Importing local packages/modules + pass # ------------------------------------------------------------------------- @@ -47,21 +68,4 @@ if _DCCSI_DEV_MODE: _logger=_LOGGER) # ------------------------------------------------------------------------- - -# ------------------------------------------------------------------------- -def init(): - """If the substance api is required for a package/module to import, - then it should be initialized and added here so general imports - don't fail""" - - # __all__.append() - - # Make sure we can import the native apis - # import - - # Importing local packages/modules - pass - -# ------------------------------------------------------------------------- - del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py index cbd58ba5e8..0db99ff838 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/__init__.py @@ -15,7 +15,7 @@ from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = 'azpy.dev.ide' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py index 2c91398b70..1e179003fe 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/hot_keys.py @@ -119,10 +119,10 @@ def get_stub_check_path(in_path=__file__, check_stub='engineroot.txt'): # ------------------------------------------------------------------------- # globals -_LY_DEV = get_stub_check_path() -_LOGGER.info('_LY_DEV: {}'.format(_LY_DEV)) +_O3DE_DEV = get_stub_check_path() +_LOGGER.info('_O3DE_DEV: {}'.format(_O3DE_DEV)) -_PROJ_CACHE = os.path.join(_LY_DEV, 'cache', 'DCCsi', 'wing') +_PROJ_CACHE = os.path.join(_O3DE_DEV, 'cache', 'DCCsi', 'wing') _LOGGER.info('_PROJ_CACHE: {}'.format(_PROJ_CACHE)) if not os.path.exists(_PROJ_CACHE): @@ -339,7 +339,7 @@ mel_selection_to_maya.contexts = [ if __name__ == '__main__': # there are not really tests to run here due to this being a list of # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py index dbf5025f88..838e890184 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/ide/wing/test.py @@ -73,7 +73,7 @@ dccsi_test_script.contexts = [ if __name__ == '__main__': # there are not really tests to run here due to this being a list of # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py index 76dc3d9b2a..3e4344aecc 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/maya_app.py @@ -25,10 +25,10 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE # -------------------------------------------------------------------------- # -- Global Definitions -- -_DCCSI_DCC_APP = None +_DCCSI_G_DCC_APP = None # set up global space, logging etc. -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = 'azpy.dev.utils.check.maya_app' @@ -43,14 +43,14 @@ _LOGGER = _logging.getLogger(_MODULENAME) def set_dcc_app(dcc_app='maya'): """ azpy.dev.utils.check.maya.set_dcc_app() - this will set global _DCCSI_DCC_APP = 'maya' - and os.environ["DCCSI_DCC_APP"] = 'maya' + this will set global _DCCSI_G_DCC_APP = 'maya' + and os.environ["DCCSI_G_DCC_APP"] = 'maya' """ - _DCCSI_DCC_APP = dcc_app + _DCCSI_G_DCC_APP = dcc_app - _LOGGER.info('Setting DCCSI_DCC_APP to: {0}'.format(dcc_app)) + _LOGGER.info('Setting DCCSI_G_DCC_APP to: {0}'.format(dcc_app)) - return _DCCSI_DCC_APP + return _DCCSI_G_DCC_APP # ------------------------------------------------------------------------- @@ -58,44 +58,44 @@ def set_dcc_app(dcc_app='maya'): def clear_dcc_app(dcc_app=False): """ azpy.dev.utils.check.maya.set_dcc_app() - this will set global _DCCSI_DCC_APP = False - and os.environ["DCCSI_DCC_APP"] = False + this will set global _DCCSI_G_DCC_APP = False + and os.environ["DCCSI_G_DCC_APP"] = False """ - _DCCSI_DCC_APP = dcc_app + _DCCSI_G_DCC_APP = dcc_app - _LOGGER.info('Setting DCCSI_DCC_APP to: {0}'.format(dcc_app)) + _LOGGER.info('Setting DCCSI_G_DCC_APP to: {0}'.format(dcc_app)) - return _DCCSI_DCC_APP + return _DCCSI_G_DCC_APP # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -def validate_state(DCCSI_DCC_APP=_DCCSI_DCC_APP): +def validate_state(DCCSI_G_DCC_APP=_DCCSI_G_DCC_APP): ''' This will detect if we are running in Maya or not, then will call either, set_dcc_app('maya') or clear_dcc_app(dcc_app=False) ''' - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER.debug(autolog()) try: import maya.cmds as cmds - DCCSI_DCC_APP = set_dcc_app('maya') + DCCSI_G_DCC_APP = set_dcc_app('maya') except ImportError as e: _LOGGER.warning('Can not perform: import maya.cmds as cmds') - DCCSI_DCC_APP = clear_dcc_app() + DCCSI_G_DCC_APP = clear_dcc_app() else: try: if cmds.about(batch=True): - DCCSI_DCC_APP = set_dcc_app('maya') + DCCSI_G_DCC_APP = set_dcc_app('maya') except AttributeError as e: _LOGGER.warning("maya.cmds module isn't fully loaded/populated, " "(cmds populates only in batch, maya.standalone, or maya GUI)") # NO Maya - DCCSI_DCC_APP=clear_dcc_app() + DCCSI_G_DCC_APP=clear_dcc_app() - return DCCSI_DCC_APP + return DCCSI_G_DCC_APP # ------------------------------------------------------------------------- @@ -120,7 +120,7 @@ def autolog(): # ------------------------------------------------------------------------- # run the check on import -_DCCSI_DCC_APP = validate_state() +_DCCSI_G_DCC_APP = validate_state() # ------------------------------------------------------------------------- @@ -130,7 +130,7 @@ _DCCSI_DCC_APP = validate_state() if __name__ == '__main__': # there are not really tests to run here due to this being a list of # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging @@ -150,5 +150,5 @@ if __name__ == '__main__': _LOGGER.info('{} ... Running script as __main__'.format(_MODULENAME)) _LOGGER.info(STR_CROSSBAR) - _DCCSI_DCC_APP = validate_state() - _LOGGER.info('Is Maya Running? _DCCSI_DCC_APP = {}'.format(_DCCSI_DCC_APP)) + _DCCSI_G_DCC_APP = validate_state() + _LOGGER.info('Is Maya Running? _DCCSI_G_DCC_APP = {}'.format(_DCCSI_G_DCC_APP)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py index 301d49a0ea..e5304cf34c 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/dev/utils/check/running_state.py @@ -21,10 +21,11 @@ import logging as _logging # -------------------------------------------------------------------------- # -- Global Definitions -- -_DCCSI_DCC_APP = None +_DCCSI_G_DCC_APP = None _MODULENAME = 'azpy.dev.utils.check.running_state' _LOGGER = _logging.getLogger(_MODULENAME) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -36,7 +37,7 @@ class CheckRunningState(object): """ # Class Variables - DCCSI_DCC_APP = None + DCCSI_G_DCC_APP = None def __init__(self, *args, **kwargs): ''' @@ -82,32 +83,35 @@ class CheckRunningState(object): def check_known(self): # -- init -- # first let's check if any of these DCC apps are running + + # To Do?: Add O3DE checks, treat as a DCC app? + # 0 - maya first - CheckRunningState.DCCSI_DCC_APP = self.maya_running() + CheckRunningState.DCCSI_G_DCC_APP = self.maya_running() # 1 - then max - if not CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = self.max_running() + if not CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = self.max_running() else: - _LOGGER.warning('DCCSI_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_DCC_APP)) + _LOGGER.warning('DCCSI_G_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_G_DCC_APP)) # 2 - then blender - if not CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = self.blender_running() + if not CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = self.blender_running() else: - _LOGGER.warning('DCCSI_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_DCC_APP)) + _LOGGER.warning('DCCSI_G_DCC_APP is already set: {}'.format(CheckRunningState.DCCSI_G_DCC_APP)) # store checks for DCC info - if CheckRunningState.DCCSI_DCC_APP: + if CheckRunningState.DCCSI_G_DCC_APP: self.dcc_py = True # store check for is maya running headless - if CheckRunningState.DCCSI_DCC_APP == 'maya': + if CheckRunningState.DCCSI_G_DCC_APP == 'maya': self.maya_headless = self.is_maya_headless() # set a envar other modules can easily check - if CheckRunningState.DCCSI_DCC_APP: - os.environ['DCCSI_DCC_APP'] = CheckRunningState.DCCSI_DCC_APP + if CheckRunningState.DCCSI_G_DCC_APP: + os.environ['DCCSI_G_DCC_APP'] = CheckRunningState.DCCSI_G_DCC_APP # --------------------------------------------------------------------- @@ -164,13 +168,13 @@ class CheckRunningState(object): """< To Do: Need to document >""" try: import azpy.dev.utils.check.maya_app as check_dcc - DCCSI_DCC_APP = check_dcc.validate_state() + DCCSI_G_DCC_APP = check_dcc.validate_state() except ImportError as e: _LOGGER.info('Not Implemented: azpy.dev.utils.check.maya_app') - if DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() - os.environ["DCCSI_DCC_APP"] = str(DCCSI_DCC_APP) - return CheckRunningState.DCCSI_DCC_APP + if DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() + os.environ["DCCSI_G_DCC_APP"] = str(DCCSI_G_DCC_APP) + return CheckRunningState.DCCSI_G_DCC_APP #---------------------------------------------------------------------- # --method------------------------------------------------------------- @@ -200,13 +204,13 @@ class CheckRunningState(object): """ try: import azpy.dev.utils.check.max_app as check_dcc - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() except ImportError as e: _LOGGER.info('Not Implemented: azpy.dev.utils.check.max') - if CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() - os.environ["DCCSI_DCC_APP"] = str(CheckRunningState.DCCSI_DCC_APP) - return CheckRunningState.DCCSI_DCC_APP + if CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() + os.environ["DCCSI_G_DCC_APP"] = str(CheckRunningState.DCCSI_G_DCC_APP) + return CheckRunningState.DCCSI_G_DCC_APP #---------------------------------------------------------------------- @@ -217,13 +221,13 @@ class CheckRunningState(object): """ try: import azpy.dev.utils.check.blender_app as check_dcc - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() except ImportError as e: _LOGGER.info('Not Implemented: azpy.dev.utils.check.blender') - if CheckRunningState.DCCSI_DCC_APP: - CheckRunningState.DCCSI_DCC_APP = check_dcc.validate_state() - os.environ["DCCSI_DCC_APP"] = str(CheckRunningState.DCCSI_DCC_APP) - return CheckRunningState.DCCSI_DCC_APP + if CheckRunningState.DCCSI_G_DCC_APP: + CheckRunningState.DCCSI_G_DCC_APP = check_dcc.validate_state() + os.environ["DCCSI_G_DCC_APP"] = str(CheckRunningState.DCCSI_G_DCC_APP) + return CheckRunningState.DCCSI_G_DCC_APP #---------------------------------------------------------------------- @@ -231,7 +235,7 @@ class CheckRunningState(object): # Class Test #========================================================================== if __name__ == '__main__': - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True _LOGGER.setLevel(_logging.DEBUG) # force debugging @@ -251,4 +255,4 @@ if __name__ == '__main__': _LOGGER.info(STR_CROSSBAR) foo = CheckRunningState() - _LOGGER.info('DCCSI_DCC_APP: {}'.format(foo.DCCSI_DCC_APP)) + _LOGGER.info('DCCSI_G_DCC_APP: {}'.format(foo.DCCSI_G_DCC_APP)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py index dd28c98927..cdcf99d762 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/env_base.py @@ -10,7 +10,7 @@ # -- This line is 75 characters ------------------------------------------- from __future__ import unicode_literals ''' -Module: \azpy\shared\common\base_env.py +Module: \\azpy\\shared\\common\\base_env.py This module packs the most basic set of environment variables. @@ -57,7 +57,7 @@ _LOGGER = _logging.getLogger(_PACKAGENAME) _LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) # set up base totally non-functional defauls (denoted with $) @@ -65,15 +65,15 @@ _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _BASE_ENVVAR_DICT = OrderedDict() # project tag -_BASE_ENVVAR_DICT[ENVAR_LY_PROJECT] = '${0}'.format(ENVAR_LY_PROJECT) +_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT] = '${0}'.format(ENVAR_O3DE_PROJECT) # paths -_BASE_ENVVAR_DICT[ENVAR_LY_DEV] = Path('${0}'.format(ENVAR_LY_DEV)) -_BASE_ENVVAR_DICT[ENVAR_LY_PROJECT_PATH] = Path('${0}'.format(ENVAR_LY_PROJECT_PATH)) +_BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] = Path('${0}'.format(ENVAR_O3DE_DEV)) +_BASE_ENVVAR_DICT[ENVAR_O3DE_PROJECT_PATH] = Path('${0}'.format(ENVAR_O3DE_PROJECT_PATH)) _BASE_ENVVAR_DICT[ENVAR_DCCSIG_PATH] = Path('${0}'.format(ENVAR_DCCSIG_PATH)) _BASE_ENVVAR_DICT[ENVAR_DCCSI_LOG_PATH] = Path('${0}'.format(ENVAR_DCCSI_LOG_PATH)) _BASE_ENVVAR_DICT[ENVAR_DCCSI_AZPY_PATH] = Path('${0}'.format(ENVAR_DCCSI_AZPY_PATH)) -_BASE_ENVVAR_DICT[ENVAR_DCCSI_SDK_PATH] = Path('${0}'.format(ENVAR_DCCSI_SDK_PATH)) +_BASE_ENVVAR_DICT[ENVAR_DCCSI_TOOLS_PATH] = Path('${0}'.format(ENVAR_DCCSI_TOOLS_PATH)) # dev env flags _BASE_ENVVAR_DICT[ENVAR_DCCSI_GDEBUG] = '${0}'.format(ENVAR_DCCSI_GDEBUG) @@ -110,16 +110,16 @@ if __name__ == '__main__': # print(setEnvarDefaults(), '\r') #<-- not necissary, already called # print(BASE_ENVVAR_VALUES, '\r') _LOGGER.info('Pretty print: _BASE_ENVVAR_DICT') - print(json.dumps(_BASE_ENVVAR_DICT, - indent=4, sort_keys=False, - ensure_ascii=False), '\r') + _LOGGER.debug(json.dumps(_BASE_ENVVAR_DICT, + indent=4, sort_keys=False, + ensure_ascii=False), '\r') # retreive a Path type key from the Box - foo = _BASE_ENVVAR_DICT[ENVAR_LY_DEV] + foo = _BASE_ENVVAR_DICT[ENVAR_O3DE_DEV] _LOGGER.info('~ foo is: {0}'.format(type(foo), foo)) # simple tests - _ENV_TAG = 'LY_DEV' + _ENV_TAG = 'O3DE_DEV' foo = get_envar_default(_ENV_TAG) _LOGGER.info("~ Results of getVar on tag, '{0}':'{1}'\r".format(_ENV_TAG, foo)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/__init__.py deleted file mode 100755 index be62dedcbe..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/maya/toolbits/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# coding:utf-8 -#!/usr/bin/python -# -# 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 line is 75 characters ------------------------------------------- -# The __init__.py files help guide import statements without automatically -# importing all of the modules -"""azpy.maya.toolbits.__init__""" - -mport os - -from azpy.env_bool import env_bool -from azpy.constants import ENVAR_DCCSI_GDEBUG -from azpy.constants import ENVAR_DCCSI_DEV_MODE - -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) - -_PACKAGENAME = __name__ -if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.maya.toolbits' - -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - -__all__ = ['detach'] - -del _LOGGER -#-------------------------------------------------------------------------- - diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py index a5a4d40aad..478d3c355e 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/return_stub.py @@ -19,7 +19,7 @@ import logging as _logging # ------------------------------------------------------------------------- # global space debug flag, no fancy stuff here we use in bootstrap -_G_DEBUG = False # manually enable to debug this file +_DCCSI_GDEBUG = False # manually enable to debug this file _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': @@ -44,7 +44,7 @@ def return_stub(stub): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER.debug('~ Debug Message: I was not able to find the ' 'path to that file (stub) in a walk-up ' 'from currnet path') diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py index 7c109f42cb..d6b6bc0d1d 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/__init__.py @@ -11,23 +11,27 @@ """azpy.shared.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/boxDumpTest.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/boxDumpTest.json deleted file mode 100644 index 52c556733c..0000000000 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/boxDumpTest.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "ordered_box": true, - "COMPANY": "Amazon.Lumberyard", - "LY_PROJECT": "DccScriptingInterface", - "LY_DEV": "G:\\depot\\JG_PC1_spectrAtom\\dev", - "LY_BUILD_DIR_NAME": "windows_vs2019", - "LY_BUILD_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\windows_vs2019", - "QT_PLUGIN_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\bin\\profile\\EditorPlugins", - "LY_PROJECT_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface", - "DCCSIG_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface", - "DCCSI_AZPY_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface\\azpy", - "DCCSI_SDK_PATH": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Gems\\AtomLyIntegration\\TechnicalArt\\DccScriptingInterface\\SDK", - "DCCSI_WING_VERSION_MAJOR": "7", - "DCCSI_WING_VERSION_MINOR": "1", - "WINGHOME": "C:\\Program Files (x86)\\Wing Pro 7.1", - "DCCSI_PY_DEFAULT": "G:\\depot\\JG_PC1_spectrAtom\\dev\\Tools\\Python\\3.7.5\\windows\\python.exe" -} diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py index ab425ef427..1d552e7ea7 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/__init__.py @@ -12,23 +12,27 @@ # importing all of the modules """azpy.shared.common.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG # global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared.common' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py index 4040620002..629f52bbd7 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/core_utils.py @@ -48,34 +48,34 @@ import os import sys import site import fnmatch +import logging as _logging # 3rd Party from pathlib import Path # from progress.spinner import Spinner # deprecate use (or refactor) - -# Lumberyard extensions -from azpy.constants import * -from azpy import initialize_logger # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# global space debug flag -from azpy.env_bool import env_bool +# global space +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_PACKAGENAME = __name__ -if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.shared.common.core_utils' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.shared.common.core_utils' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -421,7 +421,7 @@ def return_stub(stub): if __name__ == "__main__": '''To Do: Document''' # constants for shared use. - _G_DEBUG = True + _DCCSI_GDEBUG = True # happy _LOGGER.info _LOGGER.info("# {0} #".format('-' * 72)) @@ -435,7 +435,7 @@ if __name__ == "__main__": _KNOWN_SITEDIR_PATHS = site._init_pathinfo() # this is just a debug developer convenience _LOGGER.info (for testing acess) - if _G_DEBUG: + if _DCCSI_GDEBUG: import pkgutil _LOGGER.info('Current working dir: {0}'.format(cwd)) search_path = ['.'] # set to None to see all modules importable from sys.path diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py index fc99df0f30..fc2db6be0c 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/common/envar_utils.py @@ -12,7 +12,7 @@ from __future__ import unicode_literals # ------------------------------------------------------------------------- ''' -Module: \azpy\shared\common\config_utils.py +Module: \\azpy\\shared\\common\\config_utils.py A set of utility functions @@ -33,28 +33,31 @@ import logging as _logging # 3rd Party from box import Box from unipath import Path - -# Lumberyard extensions -from azpy.constants import * # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -from azpy.env_bool import env_bool +# global space +import azpy.env_bool as env_bool +from azpy.constants import ENVAR_O3DE_DEV +from azpy.constants import ENVAR_O3DE_PROJECT from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_PACKAGENAME = __name__ -if _PACKAGENAME is '__main__': - _PACKAGENAME = 'azpy.shared.common.envar_utils' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.shared.common.envar_utils' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- @@ -65,7 +68,7 @@ def get_envar_default(envar, envar_default=None, envar_set=Box(ordered_box=True) Get from the system environment, or the module dictionary (a Box): like the test one in __main__ below, TEST_ENV_VALUES = Box(ordered_box=True) - TEST_ENV_VALUES[ENVAR_LY_PROJECT] = '${0}'.format(ENVAR_LY_PROJECT) + TEST_ENV_VALUES[ENVAR_O3DE_PROJECT] = '${0}'.format(ENVAR_O3DE_PROJECT) This dictionary provides a simple way to pack a default set into a structure and decouple the getter implementation. @@ -88,7 +91,7 @@ def get_envar_default(envar, envar_default=None, envar_set=Box(ordered_box=True) # -- envar util ---------------------------------------------------------- -def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_LY_DEV)): +def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_O3DE_DEV)): """ Set each environment variable if not alreay set with value. Must be safe, will not over-write existing. @@ -98,8 +101,8 @@ def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_LY_DEV)): env_root = Path(env_root) if env_root.exists(): - os.environ[ENVAR_LY_DEV] = env_root - envar_set[ENVAR_LY_DEV] = env_root + os.environ[ENVAR_O3DE_DEV] = env_root + envar_set[ENVAR_O3DE_DEV] = env_root else: raise ValueError("EnvVar Root is not valid: {0}".format(env_root)) @@ -107,7 +110,7 @@ def set_envar_defaults(envar_set, env_root=get_envar_default(ENVAR_LY_DEV)): envar = str(envar) value = os.getenv(envar) - if _G_DEBUG: + if _DCCSI_GDEBUG: if not value: _LOGGER.debug('~ EnVar value NOT found: {0}\r'.format(envar)) @@ -191,8 +194,8 @@ if __name__ == '__main__': # it should be benign but leaving this comment here in case of funk # tes envars - TEST_ENV_VALUES[ENVAR_LY_PROJECT] = '${0}'.format(ENVAR_LY_PROJECT) - TEST_ENV_VALUES[ENVAR_LY_DEV] = Path('${0}'.format(ENVAR_LY_DEV)) + TEST_ENV_VALUES[ENVAR_O3DE_PROJECT] = '${0}'.format(ENVAR_O3DE_PROJECT) + TEST_ENV_VALUES[ENVAR_O3DE_DEV] = Path('${0}'.format(ENVAR_O3DE_DEV)) # try to fetch and set the base values from the environment # this makes sure all envars set, are resolved on import @@ -204,7 +207,7 @@ if __name__ == '__main__': ensure_ascii=False), '\r') # simple tests - _ENV_TAG = 'LY_DEV' + _ENV_TAG = 'O3DE_DEV' foo = get_envar_default(_ENV_TAG) _LOGGER.info("~ Results of getVar on tag, '{0}':'{1}'\r".format(_ENV_TAG, foo)) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py index c18cf9d81a..36a354a965 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/__init__.py @@ -13,31 +13,35 @@ SPDX-License-Identifier: Apache-2.0 OR MIT import os from pathlib import Path import logging as _logging +# ------------------------------------------------------------------------- -from azpy.env_bool import env_bool +# ------------------------------------------------------------------------- +# global space +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared.noodely' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) -# ------------------------------------------------------------------------- -# +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) + __all__ = ['find_arg', 'helpers', 'node', 'synth', 'synth_arg_kwarg', 'test_foo'] -# # ------------------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py index 681c88b33c..dd939f3973 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/find_arg.py @@ -74,7 +74,7 @@ if __name__ == "__main__": print ('~ find_arg.py ... Running script as __main__') print ("# ----------------------------------------------------------------------- #\r") - _G_DEBUG = True + _DCCSI_GDEBUG = True from test_foo import Foo @@ -102,7 +102,7 @@ if __name__ == "__main__": self._kwargsDict[key] = value # synthesize(self, '{0}'.format(key), value) <-- I have a method, # which synthesizes properties... with gettr, settr, etc. - if _G_DEBUG: + if _DCCSI_GDEBUG: print("{0}:{1}".format(key, value)) # representation diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py index ca44b9a6c8..59e25d45e1 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/node.py @@ -47,13 +47,13 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE # global space # To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = 'azpy.shared.noodely.node' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_MODULENAME, log_to_file=False, @@ -66,7 +66,7 @@ _LOGGER.debug('Starting:: {}.'.format({_MODULENAME})) # quick test code (remove later) from hashids import Hashids hashids = Hashids(min_length=16, salt='DCCsi') -if _G_DEBUG: +if _DCCSI_GDEBUG: print (hashids.encrypt(193487)) # test hash # ------------------------------------------------------------------------- @@ -107,7 +107,7 @@ class Node(object): """Class constructor: makes a node.""" # share the debug state - _DEBUG = _G_DEBUG + _DEBUG = _DCCSI_GDEBUG # logger _LOGGER = _G_LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py index 5c5402cb7d..401e524060 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/noodely/pathnode.py @@ -47,13 +47,13 @@ from azpy.constants import ENVAR_DCCSI_DEV_MODE # ------------------------------------------------------------------------- # global space # To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULENAME = 'azpy.shared.noodely.pathnode' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_MODULENAME, log_to_file=False, @@ -78,7 +78,7 @@ class PathNode(Node): """doc string""" # share the debug state - _DEBUG = _G_DEBUG + _DEBUG = _DCCSI_GDEBUG # class header _message_header = 'noodly, PathNode(): Message' diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py index a73ee00d7c..745ec9a7be 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/__init__.py @@ -11,24 +11,26 @@ """azpy.shared.ui.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.shared.ui' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) # ------------------------------------------------------------------------- # __all__ = [] diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py index 311ed484ed..493130eb64 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/base_widget.py @@ -34,7 +34,7 @@ from shiboken2 import wrapInstance # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py index d7b096468d..871b050c7a 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/custom_treemodel.py @@ -30,7 +30,7 @@ import PySide2.QtGui as QtGui # ------------------------------------------------------------------------- # global space # To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) _MODULE_PATH = Path(__file__) @@ -38,7 +38,7 @@ _MODULE_PATH = Path(__file__) _MODULENAME = 'azpy.shared.ui.custom_treemodel' _log_level = int(20) -if _G_DEBUG: +if _DCCSI_GDEBUG: _log_level = int(10) _LOGGER = azpy.initialize_logger(_MODULENAME, log_to_file=False, diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py index f4b53f6b79..7fdeadc4c2 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/help_menu.py @@ -27,7 +27,7 @@ import PySide2.QtWidgets as QtWidgets # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = os.getenv('DCCSI_GDEBUG', False) +_DCCSI_GDEBUG = os.getenv('DCCSI_GDEBUG', False) # global space developer mode flag _DCCSI_DEV_MODE = os.getenv('DCCSI_DEV_MODE', False) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py index 6fd4f359bc..d14d5bcbd6 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_qtextedit_stdout.py @@ -32,7 +32,7 @@ from PySide2.QtCore import QTimer # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = os.getenv('DCCSI_GDEBUG', False) +_DCCSI_GDEBUG = os.getenv('DCCSI_GDEBUG', False) # global space debug flag _DCCSI_DEV_MODE = os.getenv('DCCSI_DEV_MODE', False) @@ -48,7 +48,7 @@ _MODULENAME = __name__ if _MODULENAME is '__main__': _MODULENAME = _TOOL_TAG -if _G_DEBUG: +if _DCCSI_GDEBUG: _LOGGER = initialize_logger(_MODULENAME, log_to_file=True) _LOGGER.debug('Something invoked :: {0}.'.format({_MODULENAME})) else: @@ -130,7 +130,7 @@ if __name__ == '__main__': _TOOL_TAG = 'azpy.shared.ui.pyside2_qtextedit_stdout' _TYPE_TAG = 'test' - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER = initialize_logger('{0}-TEST'.format(_TOOL_TAG), log_to_file=True) _LOGGER.debug('Something invoked :: {0}.'.format({_MODULENAME})) @@ -155,7 +155,7 @@ if __name__ == '__main__': _READER.start('python', ['-u', _TEST_PY_FILE]) # start the process # after that starts, this will show the console - # LY_QSS = Path(_MODULE_PATH.parent, 'resources', 'stylesheets', 'LYstyle.qss') + # O3DE_QSS = Path(_MODULE_PATH.parent, 'resources', 'stylesheets', 'LYstyle.qss') _DARK_STYLE = Path(_MODULE_PATH.parent, 'resources', 'qdarkstyle', 'style.qss') _CONSOLE.qapp.setStyleSheet(_DARK_STYLE.read_file()) _CONSOLE.show() # make the console visible diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py index b996d9baf6..0f229e45b6 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/pyside2_ui_utils.py @@ -51,7 +51,7 @@ import azpy.shared.ui.help_menu as help_menu # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE @@ -112,7 +112,7 @@ def from_ui_generate_form_and_base_class(filename, return_output=False): ui_file.exists() except FileNotFoundError as error: output += 'File does not exist: {0}/r'.format(error) - if _G_DEBUG: + if _DCCSI_GDEBUG: print(error) if return_output: return False, output diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py index de2bc1aaf4..28f6e23ac2 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/qt_settings.py @@ -24,7 +24,7 @@ import azpy.config_utils _config = azpy.config_utils.get_dccsi_config() # ^ this is effectively an import and retreive of \config.py # init lumberyard Qy/PySide2 access -_config.init_ly_pyside(settings.LY_DEV) +_config.init_o3de_pyside(settings.O3DE_DEV) # now we can import lumberyards PySide2 import PySide2.QtCore as QtCore @@ -32,7 +32,7 @@ import PySide2.QtWidgets as QtWidgets # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py index a3700e8cf4..ec1d00b6d0 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/shared/ui/templates.py @@ -47,7 +47,7 @@ import PySide2.QtUiTools as QtUiTools # ------------------------------------------------------------------------- # global space debug flag -_G_DEBUG = settings.DCCSI_GDEBUG +_DCCSI_GDEBUG = settings.DCCSI_GDEBUG # global space debug flag _DCCSI_DEV_MODE = settings.DCCSI_DEV_MODE diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py index 71f2682242..33ac6c6814 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/synthetic_env.py @@ -40,12 +40,11 @@ Configures several useful environment config settings and paths, [key] : [value] # this is the required base environment - LY_PROJECT : name of project (project directory) - LY_DEV : path to Lumberyard \dev root - LY_PROJECT_PATH : path to project dir + O3DE_PROJECT : name of project (project directory) + O3DE_DEV : path to Lumberyard \dev root + O3DE_PROJECT_PATH : path to project dir DCCSIG_PATH : path to the DCCsi Gem root - DCCSI_AZPY_PATH * : path to azpy Python API (code) - DCCSI_SDK_PATH : path to associated (non-api code) DCC SDK + DCCSI_TOOLS_PATH : path to associated (non-api code) DCC SDK # nice to haves in base env to define core support DCCSI_GDEBUG : sets global debug prints @@ -59,7 +58,7 @@ Configures several useful environment config settings and paths, :: Default version py37 has a launcher (activates the env, starts py interpreter) - set DCCSI_PY_BASE=%DCCSI_PYTHON_INSTALL%\python.exe + set DCCSI_PY_BASE=%O3DE_PYTHON_INSTALL%\python.exe :: shared location for 64bit python 3.7 BASE location set DCCSI_PY_DCCSI=%DCCSI_LAUNCHERS_PATH%\Launch_pyBASE.bat @@ -74,7 +73,7 @@ Configures several useful environment config settings and paths, :: shared location for 64bit DCCSI_PY_MAYA python 2.7 DEV location set DCCSI_PY_MAYA=%MAYA_LOCATION%\bin\mayapy.exe :: wingIDE can use more then one defined/managed interpreters - :: allowing you to _G_DEBUG code in multiple runtimes in one session + :: allowing you to _DCCSI_GDEBUG code in multiple runtimes in one session ${DCCSI_PY_MAYA} # related to the WING as the default DCCSI_GDEBUGGER @@ -92,8 +91,9 @@ import os import sys import site import re -#import inspect +import inspect import json +import importlib.util import logging as _logging from collections import OrderedDict @@ -110,35 +110,33 @@ _MODULE_PATH = os.path.realpath(__file__) # To Do: what if frozen? _DCCSIG_PATH = os.path.normpath(os.path.join(_MODULE_PATH, '../..')) _DCCSIG_PATH = os.getenv('DCCSIG_PATH', _DCCSIG_PATH) site.addsitedir(_DCCSIG_PATH) -print(_DCCSIG_PATH) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# Lumberyard extensions +# O3DE extensions from pathlib import Path # set up global space, logging etc. -import azpy -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_PACKAGENAME = 'DCCsi.azpy.sunthetic_env' +_PACKAGENAME = 'DCCsi.azpy.synthetic_env' -_log_level = int(20) -if _G_DEBUG: - _log_level = int(10) -_LOGGER = azpy.initialize_logger(_PACKAGENAME, - log_to_file=True, - default_log_level=_log_level) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) -_LOGGER.debug('Starting up: {0}.'.format({_PACKAGENAME})) _LOGGER.debug('_DCCSIG_PATH: {}'.format(_DCCSIG_PATH)) -_LOGGER.debug('_G_DEBUG: {}'.format(_G_DEBUG)) +_LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG)) _LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) if _DCCSI_DEV_MODE: @@ -170,7 +168,7 @@ if os.path.exists(_DCCSI_PYTHON_LIB_PATH): # ------------------------------------------------------------------------- # post-bootstrap global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) # ------------------------------------------------------------------------- @@ -225,7 +223,7 @@ def return_stub(stub='dccsi_stub'): break if (len(tail) == 0): path = "" - if _G_DEBUG: + if _DCCSI_GDEBUG: _LOGGER.debug('~Not able to find the path to that file ' '(stub) in a walk-up from currnet path.') break @@ -262,7 +260,7 @@ def get_stub_check_path(in_path, check_stub='engineroot.txt'): # ------------------------------------------------------------------------- # TO DO: Move to a util package or module -def resolve_envar_path(envar='LY_DEV', +def resolve_envar_path(envar='O3DE_DEV', start_path=__file__, check_stub='engineroot.txt', dir_name='dev', @@ -276,7 +274,7 @@ def resolve_envar_path(envar='LY_DEV', That is a pretty safe indicator that we found the right '\dev' - Second it checks if the env var 'LY_DEV' is set, use that instead! + Second it checks if the env var 'O3DE_DEV' is set, use that instead! """ @@ -360,14 +358,14 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): # \dev Lumberyard ROOT PATH # someone decided to use this as a root stub (for similar reasons in C++?) - # STUB_LY_DEV = str('engineroot.txt') + # STUB_O3DE_DEV = str('engineroot.txt') # I don't own \dev so I didn't want to check in anything new there - _LY_DEV = resolve_envar_path(ENVAR_LY_DEV, # envar + _O3DE_DEV = resolve_envar_path(ENVAR_O3DE_DEV, # envar _THIS_MODULE_PATH, # search path - STUB_LY_DEV, # stub - TAG_DIR_LY_DEV) # dir + STUB_O3DE_DEV, # stub + TAG_DIR_O3DE_DEV) # dir - _SYNTH_ENV_DICT[ENVAR_LY_DEV] = _LY_DEV.as_posix() + _SYNTH_ENV_DICT[ENVAR_O3DE_DEV] = _O3DE_DEV.as_posix() # project name is a string, it should be project dir name # for siloed testing and a purely synthetc env (nothing previously set) @@ -376,17 +374,17 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): # for testing overrides of the default synthetic env # we can do two things here, - # first we can try to fetch from the env os.getenv('LY_PROJECT') + # first we can try to fetch from the env os.getenv('O3DE_PROJECT') # If comes back None, allows you to specify a default fallback # changed to just make the fallback what is set in boostrap # so now it's less of a fallnack and more correct if not # explicitly set - _LY_PROJECT = os.getenv(ENVAR_LY_PROJECT) - _SYNTH_ENV_DICT[ENVAR_LY_PROJECT] = _LY_PROJECT + _O3DE_PROJECT = os.getenv(ENVAR_O3DE_PROJECT) + _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT] = _O3DE_PROJECT - _LY_BUILD_DIR_NAME = os.getenv(ENVAR_LY_BUILD_DIR_NAME, - TAG_DIR_LY_BUILD) - _SYNTH_ENV_DICT[ENVAR_LY_BUILD_DIR_NAME] = _LY_BUILD_DIR_NAME + _O3DE_BUILD_DIR_NAME = os.getenv(ENVAR_O3DE_BUILD_DIR_NAME, + TAG_DIR_O3DE_BUILD_FOLDER) + _SYNTH_ENV_DICT[ENVAR_O3DE_BUILD_DIR_NAME] = _O3DE_BUILD_DIR_NAME # pattern for the above is (and will be repeated) # _SOME_ENVAR = resolve_envar_path('ENVAR', @@ -406,31 +404,31 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): # so we guess based on how I set up the original dev environment # -- envar -- - _LY_BUILD_PATH = Path(os.getenv(ENVAR_LY_BUILD_PATH, - PATH_LY_BUILD_PATH)) - _SYNTH_ENV_DICT[ENVAR_LY_BUILD_PATH] = _LY_BUILD_PATH.as_posix() + _O3DE_BUILD_PATH = Path(os.getenv(ENVAR_O3DE_BUILD_PATH, + PATH_O3DE_BUILD_PATH)) + _SYNTH_ENV_DICT[ENVAR_O3DE_BUILD_PATH] = _O3DE_BUILD_PATH.as_posix() # -- envar -- - _LY_BIN_PATH = Path(os.getenv(ENVAR_LY_BIN_PATH, - PATH_LY_BIN_PATH)) + _O3DE_BIN_PATH = Path(os.getenv(ENVAR_O3DE_BIN_PATH, + PATH_O3DE_BIN_PATH)) # some of these need hard checks - if not _LY_BIN_PATH.exists(): - raise Exception('LY_BIN_PATH does NOT exist: {0}'.format(_LY_BIN_PATH)) + if not _O3DE_BIN_PATH.exists(): + raise Exception('O3DE_BIN_PATH does NOT exist: {0}'.format(_O3DE_BIN_PATH)) else: - _SYNTH_ENV_DICT[ENVAR_LY_BIN_PATH] = _LY_BIN_PATH.as_posix() + _SYNTH_ENV_DICT[ENVAR_O3DE_BIN_PATH] = _O3DE_BIN_PATH.as_posix() # adding to sys.path apparently doesn't work for .dll locations like Qt - os.environ['PATH'] = _LY_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] + os.environ['PATH'] = _O3DE_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] # -- envar -- # if that stub marker doesn't exist assume DCCsi path (fallback 1) - _LY_PROJECT_PATH = Path(os.getenv(ENVAR_LY_PROJECT_PATH, - Path(_LY_DEV, _LY_PROJECT))) - _SYNTH_ENV_DICT[ENVAR_LY_PROJECT_PATH] = _LY_PROJECT_PATH.as_posix() + _O3DE_PROJECT_PATH = Path(os.getenv(ENVAR_O3DE_PROJECT_PATH, + Path(_O3DE_DEV, _O3DE_PROJECT))) + _SYNTH_ENV_DICT[ENVAR_O3DE_PROJECT_PATH] = _O3DE_PROJECT_PATH.as_posix() # -- envar -- _DCCSIG_PATH = resolve_envar_path(ENVAR_DCCSIG_PATH, # envar _THIS_MODULE_PATH, # search path - STUB_LY_ROOT_DCCSI, # stub name + STUB_O3DE_ROOT_DCCSI, # stub name TAG_DEFAULT_PROJECT) # dir _SYNTH_ENV_DICT[ENVAR_DCCSIG_PATH] = _DCCSIG_PATH.as_posix() @@ -440,9 +438,9 @@ def stash_env(_SYNTH_ENV_DICT = OrderedDict()): _SYNTH_ENV_DICT[ENVAR_DCCSI_AZPY_PATH] = _AZPY_PATH.as_posix() # -- envar -- - _DCCSI_SDK_PATH = Path(os.getenv(ENVAR_DCCSI_SDK_PATH, - Path(_DCCSIG_PATH, TAG_DIR_DCCSI_SDK))) - _SYNTH_ENV_DICT[ENVAR_DCCSI_SDK_PATH] = _DCCSI_SDK_PATH.as_posix() + _DCCSI_TOOLS_PATH = Path(os.getenv(ENVAR_DCCSI_TOOLS_PATH, + Path(_DCCSIG_PATH, TAG_DIR_DCCSI_TOOLS))) + _SYNTH_ENV_DICT[ENVAR_DCCSI_TOOLS_PATH] = _DCCSI_TOOLS_PATH.as_posix() # -- envar -- # external dccsi site-packages @@ -497,7 +495,7 @@ def init_ly_pyside(env_dict=_SYNTH_ENV_DICT): - QTFORPYTHON_PATH = Path.joinpath(LY_DEV, + QTFORPYTHON_PATH = Path.joinpath(O3DE_DEV, 'Gems', 'QtForPython', '3rdParty', @@ -509,16 +507,16 @@ def init_ly_pyside(env_dict=_SYNTH_ENV_DICT): sys.path.insert(1, str(QTFORPYTHON_PATH)) site.addsitedir(str(QTFORPYTHON_PATH)) - LY_BIN_PATH = Path.joinpath(LY_DEV, + O3DE_BIN_PATH = Path.joinpath(O3DE_DEV, 'windows_vs2019', 'bin', 'profile').resolve() - os.environ["DYNACONF_LY_BIN_PATH"] = str(LY_BIN_PATH) - os.environ["LY_BIN_PATH"] = str(LY_BIN_PATH) - site.addsitedir(str(LY_BIN_PATH)) - sys.path.insert(1, str(LY_BIN_PATH)) + os.environ["DYNACONF_O3DE_BIN_PATH"] = str(O3DE_BIN_PATH) + os.environ["O3DE_BIN_PATH"] = str(O3DE_BIN_PATH) + site.addsitedir(str(O3DE_BIN_PATH)) + sys.path.insert(1, str(O3DE_BIN_PATH)) - QT_PLUGIN_PATH = Path.joinpath(LY_BIN_PATH, + QT_PLUGIN_PATH = Path.joinpath(O3DE_BIN_PATH, 'EditorPlugins').resolve() os.environ["DYNACONF_QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH) os.environ["QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH) @@ -536,7 +534,7 @@ def init_ly_pyside(env_dict=_SYNTH_ENV_DICT): if sys.platform.startswith('win'): path = os.environ['PATH'] newPath = '' - newPath += str(LY_BIN_PATH) + os.pathsep + newPath += str(O3DE_BIN_PATH) + os.pathsep newPath += str(Path.joinpath(QTFORPYTHON_PATH, 'shiboken2').resolve()) + os.pathsep newPath += str(Path.joinpath(QTFORPYTHON_PATH, @@ -591,12 +589,12 @@ def set_env(dict_object): def test_Qt(): try: import PySide2 - print('PySide2: {0}'.format(Path(PySide2.__file__).as_posix())) + _LOGGER.info('PySide2: {0}'.format(Path(PySide2.__file__).as_posix())) # builtins.ImportError: DLL load failed: The specified procedure could not be found. from PySide2 import QtCore from PySide2 import QtWidgets except IOError as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e try: @@ -610,7 +608,7 @@ def test_Qt(): qapp.instance().quit qapp.exit() except Exception as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e # ------------------------------------------------------------------------- @@ -620,14 +618,14 @@ def main(argv, env_dict_object, debug=False, devmode=False): import getopt try: opts, args = getopt.getopt(argv, "hvt:", ["verbose=", "test="]) - except getopt.GetoptError: + except getopt.GetoptError as e: # not logging, print to cmd line console - print('synthetic_env.py -v -t ') + _LOGGER.error('synthetic_env.py -v -t ') sys.exit(2) for opt, arg in opts: if opt == '-h': - print('synthetic_env.py -v -t ') + _LOGGER.info('synthetic_env.py -v -t ') sys.exit() elif opt in ("-t", "--test"): @@ -639,14 +637,14 @@ def main(argv, env_dict_object, debug=False, devmode=False): try: from box import Box except ImportError as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e try: env_dict_object = Box(env_dict_object) - print(str(env_dict_object.to_json(sort_keys=False, + _LOGGER.info(str(env_dict_object.to_json(sort_keys=False, indent=4))) except Exception as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e # ------------------------------------------------------------------------- @@ -656,16 +654,16 @@ def main(argv, env_dict_object, debug=False, devmode=False): # ------------------------------------------------------------------------- if __name__ == '__main__': # run simple tests? - _G_DEBUG = True + _DCCSI_GDEBUG = True _DCCSI_DEV_MODE = True if _DCCSI_DEV_MODE: try: import azpy.test.entry_test - print('SUCCESS: import azpy.test.entry_test') + _LOGGER.info('SUCCESS: import azpy.test.entry_test') azpy.test.entry_test.main(verbose=True, connect_debugger=True) except ImportError as e: - print('ERROR: {0}'.format(e)) + _LOGGER.error('ERROR: {0}'.format(e)) raise e # init, stash and then activate @@ -673,9 +671,9 @@ if __name__ == '__main__': _SYNTH_ENV_DICT = stash_env(_SYNTH_ENV_DICT) _SYNTH_ENV_DICT = set_env(_SYNTH_ENV_DICT) - main(sys.argv[1:], _SYNTH_ENV_DICT, _G_DEBUG, _DCCSI_DEV_MODE) + main(sys.argv[1:], _SYNTH_ENV_DICT, _DCCSI_GDEBUG, _DCCSI_DEV_MODE) - if _G_DEBUG: + if _DCCSI_GDEBUG: tempBoxJsonFilePath = Path(_SYNTH_ENV_DICT['DCCSIG_PATH'], '.temp') tempBoxJsonFilePath = Path(tempBoxJsonFilePath, 'boxDumpTest.json') diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py index 20df0e5813..554d320654 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/__init__.py @@ -12,28 +12,44 @@ # importing all of the modules """azpy.test.__init__""" -import os +import logging as _logging -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) _PACKAGENAME = __name__ if _PACKAGENAME is '__main__': _PACKAGENAME = 'azpy.test' -import azpy -_LOGGER = azpy.initialize_logger(_PACKAGENAME) -_LOGGER.debug('Invoking __init__.py for {0}.'.format({_PACKAGENAME})) - -# ------------------------------------------------------------------------- +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_PACKAGENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_PACKAGENAME})) __all__ = ['entry_test'] +# ------------------------------------------------------------------------- + +# ------------------------------------------------------------------------- +def init(): + """If the substance api is required for a package/module to import, + then it should be initialized and added here so general imports + don't fail""" + + # Make sure we can import the native apis + # import + + # __all__.append() + + # Importing local packages/modules + pass # ------------------------------------------------------------------------- @@ -48,20 +64,4 @@ if _DCCSI_DEV_MODE: # ------------------------------------------------------------------------- -# ------------------------------------------------------------------------- -def init(): - """If the substance api is required for a package/module to import, - then it should be initialized and added here so general imports - don't fail""" - - # __all__.append() - - # Make sure we can import the native apis - # import - - # Importing local packages/modules - pass - -# ------------------------------------------------------------------------- - del _LOGGER diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py index d9d136f96e..aad440d11f 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/azpy/test/entry_test.py @@ -11,9 +11,9 @@ from __future__ import unicode_literals # ------------------------------------------------------------------------- -import sys import os import site +import logging as _logging # note: some modules not available in py2.7 unless we boostrap with config.py # See example: @@ -23,34 +23,34 @@ from pathlib import Path # ------------------------------------------------------------------------- _BOOT_CHECK = False # set true to test breakpoint in this module directly -import azpy -from azpy.env_bool import env_bool +import azpy.env_bool as env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import FRMT_LOG_LONG -# global space -# To Do: update to dynaconf dynamic env and settings? -_G_DEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) -_DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) +_DCCSI_GDEBUG = env_bool.env_bool(ENVAR_DCCSI_GDEBUG, False) +_DCCSI_DEV_MODE = env_bool.env_bool(ENVAR_DCCSI_DEV_MODE, False) -_MODULENAME = 'azpy.test.entry_test' +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'azpy.test.entry_test' -_log_level = int(20) -if _G_DEBUG: - _log_level = int(10) -_LOGGER = azpy.initialize_logger(_MODULENAME, - log_to_file=False, - default_log_level=_log_level) -_LOGGER.debug('Starting:: {}.'.format({_MODULENAME})) +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) +_LOGGER = _logging.getLogger(_MODULENAME) +_logging.basicConfig(format=FRMT_LOG_LONG) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -def main(verbose=_G_DEBUG, connect_debugger=True): - _LOGGER.info('{}'.format('-' * 74)) - _LOGGER.info('entry_test.main()') - _LOGGER.info('Root test import successful:') - _LOGGER.info('~ {}'.format(__file__)) +def main(verbose=_DCCSI_GDEBUG, connect_debugger=True): + if verbose: + _LOGGER.info('{}'.format('-' * 74)) + _LOGGER.info('entry_test.main()') + _LOGGER.info('Root test import successful:') + _LOGGER.info('~ {}'.format(__file__)) if connect_debugger: status = connect_wing() @@ -67,7 +67,7 @@ def connect_wing(): _WINGHOME = os.environ['WINGHOME'] # test _LOGGER.info('~ WINGHOME: {0}'.format(_WINGHOME)) except Exception as e: - _LOGGER.warning(e) + _LOGGER.info(e) from azpy.constants import PATH_DEFAULT_WINGHOME _WINGHOME = PATH_DEFAULT_WINGHOME os.environ['WINGHOME'] = PATH_DEFAULT_WINGHOME @@ -134,5 +134,5 @@ def connect_wing(): # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - _G_DEBUG = True - main(verbose=_G_DEBUG, connect_debugger=_G_DEBUG) + _DCCSI_GDEBUG = True + main(verbose=_DCCSI_GDEBUG, connect_debugger=_DCCSI_GDEBUG) diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py index 0c25d16133..5578bcd577 100755 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/config.py @@ -6,8 +6,11 @@ SPDX-License-Identifier: Apache-2.0 OR MIT """ # ------------------------------------------------------------------------- -"""Extend the .env using dynaconf (dynamic configuration and settings) -This config.py module assumes a minimal enviornment is defined in the .env +""".config +Generate dynamic and synethetic environment contest and settings +using dynaconf (dynamic configuration and settings) +This config.py synthetic env can be overriden or extended with a local .env +See: example.env.tmp (copy and rename to .env) To do: ensure that we can stack/layer the dynamic env to work with O3DE projects """ @@ -16,60 +19,97 @@ import os import sys import site import re +import logging as _logging # 3rdParty (possibly) py3 ships with pathlib, 2.7 does not # import pathlib # our framework for dcc tools need to run in apps like Maya that may still be # on py27 so we need to import and use after some boostrapping +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +_O3DE_RUNNING=None +try: + import azlmbr + _O3DE_RUNNING=True +except: + _O3DE_RUNNING=False +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def attach_debugger(): + _DCCSI_GDEBUG = True + os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) + + _DCCSI_DEV_MODE = True + os.environ["DYNACONF_DCCSI_DEV_MODE"] = str(_DCCSI_DEV_MODE) + + from azpy.test.entry_test import connect_wing + _debugger = connect_wing() + + return _debugger +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +# global scope +_MODULENAME = __name__ +if _MODULENAME is '__main__': + _MODULENAME = 'DCCsi.config' -# -------------------------------------------------------------------+------ #os.environ['PYTHONINSPECT'] = 'True' _MODULE_PATH = os.path.abspath(__file__) # we don't have access yet to the DCCsi Lib\site-packages # (1) this will give us import access to azpy (always?) -_DCCSIG_PATH = os.getenv('DCCSIG_PATH', +_DCCSI_PATH = os.getenv('DCCSI_PATH', os.path.abspath(os.path.dirname(_MODULE_PATH))) # ^ we assume this config is in the root of the DCCsi -# if it's not, be sure to set envar 'DCCSIG_PATH' to ensure it -site.addsitedir(_DCCSIG_PATH) # PYTHONPATH +# if it's not, be sure to set envar 'DCCSI_PATH' to ensure it +site.addsitedir(_DCCSI_PATH) # must be done for azpy # now we have azpy api access import azpy from azpy.env_bool import env_bool from azpy.constants import ENVAR_DCCSI_GDEBUG from azpy.constants import ENVAR_DCCSI_DEV_MODE +from azpy.constants import ENVAR_DCCSI_LOGLEVEL # set up global space, logging etc. # set these true if you want them set globally for debugging _DCCSI_GDEBUG = env_bool(ENVAR_DCCSI_GDEBUG, False) _DCCSI_DEV_MODE = env_bool(ENVAR_DCCSI_DEV_MODE, False) - -_PACKAGENAME = 'DCCsi.config' - -_LOG_LEVEL = int(20) +_DCCSI_LOGLEVEL = int(env_bool(ENVAR_DCCSI_LOGLEVEL, int(20))) if _DCCSI_GDEBUG: - _LOG_LEVEL = int(10) -_LOGGER = azpy.initialize_logger(_PACKAGENAME, - log_to_file=False, - default_log_level=_LOG_LEVEL) -_LOGGER.info('Starting up: {}.'.format({_PACKAGENAME})) -_LOGGER.info('site.addsitedir({})'.format(_DCCSIG_PATH)) -_LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG)) -_LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) + _DCCSI_LOGLEVEL = int(10) # early attach WingIDE debugger (can refactor to include other IDEs later) +# requires externally enabling via ENVAR if _DCCSI_DEV_MODE: - from azpy.test.entry_test import connect_wing - foo = connect_wing() + _debugger = attach_debugger() # to do: ^ this should be replaced with full featured azpy.dev.util # that supports additional debuggers (pycharm, vscode, etc.) + +# set up module logging +for handler in _logging.root.handlers[:]: + _logging.root.removeHandler(handler) + +_LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) +_LOGGER.debug('Initializing: {0}.'.format({_MODULENAME})) +_LOGGER.info('site.addsitedir({})'.format(_DCCSI_PATH)) +_LOGGER.debug('_DCCSI_GDEBUG: {}'.format(_DCCSI_GDEBUG)) +_LOGGER.debug('_DCCSI_DEV_MODE: {}'.format(_DCCSI_DEV_MODE)) +_LOGGER.debug('_DCCSI_LOGLEVEL: {}'.format(_DCCSI_LOGLEVEL)) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# (2) this will give us import access to modules we provide -_DCCSI_PYTHON_LIB_PATH = azpy.config_utils.bootstrap_dccsi_py_libs(_DCCSIG_PATH) +# this will give us import access to additional modules we provide with DCCsi +_DCCSI_PYTHON_LIB_PATH = azpy.config_utils.bootstrap_dccsi_py_libs(_DCCSI_PATH) # Now we should be able to just carry on with pth lib and dynaconf from dynaconf import Dynaconf @@ -79,82 +119,103 @@ except: import pathlib2 as pathlib from pathlib import Path -_DCCSIG_PATH = Path(_DCCSIG_PATH).resolve() -_DCCSI_PYTHON_LIB_PATH = Path(_DCCSI_PYTHON_LIB_PATH).resolve() +_DCCSI_PATH = Path(_DCCSI_PATH) # pathify +_DCCSI_PYTHON_PATH = Path(_DCCSI_PATH,'3rdParty','Python') +_DCCSI_PYTHON_LIB_PATH = Path(_DCCSI_PYTHON_LIB_PATH) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -def init_ly_pyside(LY_DEV=None): - """sets access to lumberyards Qt dlls and PySide""" +# start locally prepping known default values for dyanmic environment settings +_O3DE_DCCSI_PATH = os.environ['PATH'] +os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH - LY_DEV = Path(LY_DEV).resolve() - if not LY_DEV.exists(): - raise Exception('LY_DEV does NOT exist: {0}'.format(LY_DEV)) +# this will retreive the O3DE engine root +_O3DE_DEV = azpy.config_utils.get_o3de_engine_root() +# set up dynamic config envars +os.environ["DYNACONF_O3DE_DEV"] = str(_O3DE_DEV.resolve()) + +from azpy.constants import TAG_DIR_O3DE_BUILD_FOLDER +_O3DE_BUILD_FOLDER = TAG_DIR_O3DE_BUILD_FOLDER +os.environ["DYNACONF_O3DE_BUILD_FOLDER"] = str(_O3DE_BUILD_FOLDER) +_O3DE_BUILD_PATH = Path(_O3DE_DEV, TAG_DIR_O3DE_BUILD_FOLDER) +os.environ["DYNACONF_O3DE_BUILD_PATH"] = str(_O3DE_BUILD_PATH.resolve()) + +from azpy.constants import STR_O3DE_BIN_PATH +_O3DE_BIN_PATH = Path(STR_O3DE_BIN_PATH.format(_O3DE_BUILD_PATH)) +os.environ["DYNACONF_O3DE_BIN_PATH"] = str(_O3DE_BIN_PATH.resolve()) + +# this in most cases will return the project folder +# if it returns a matching engine folder then we don't know the project folder +_O3DE_PROJECT_PATH = azpy.config_utils.get_o3de_project_path() +os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_PROJECT_PATH.resolve()) + +# special, a home for stashing PYTHONPATHs into managed settings +_O3DE_PYTHONPATH = list() +_O3DE_PYTHONPATH.append(_DCCSI_PATH) +# ------------------------------------------------------------------------- + + +# ------------------------------------------------------------------------- +def init_o3de_pyside2(dccsi_path=_DCCSI_PATH, + engine_bin=_O3DE_BIN_PATH): + """Initialize the DCCsi Qt/PySide dynamic env and settings + sets access to lumberyards Qt dlls and PySide""" + + _DCCSI_PATH = Path(dccsi_path) + _O3DE_BIN_PATH = Path(engine_bin) + + if not _O3DE_BIN_PATH.exists(): + raise Exception('_O3DE_BIN_PATH does NOT exist: {0}'.format(_O3DE_BIN_PATH)) else: - # to do: 'windows_vs2019' might change or be different locally - # 'windows_vs2019' is defined as a str tag in constants - # we may not yet have access to azpy.constants :( - from azpy.constants import TAG_DIR_LY_BUILD - from azpy.constants import PATH_LY_BUILD_PATH - from azpy.constants import PATH_LY_BIN_PATH - # to do: pull some of these str and tags from constants - LY_BUILD_PATH = Path.joinpath(LY_DEV, - TAG_DIR_LY_BUILD).resolve() - LY_BIN_PATH = Path.joinpath(LY_BUILD_PATH, - 'bin', - 'profile').resolve() + pass + + # python config + _DCCSI_PYTHON_PATH = Path(_DCCSI_PATH,'3rdParty','Python') + os.environ["DYNACONF_DCCSI_PYTHON_PATH"] = str(_DCCSI_PYTHON_PATH.resolve()) # # allows to retreive from settings.QTFORPYTHON_PATH # from azpy.constants import STR_QTFORPYTHON_PATH # a path string constructor - # QTFORPYTHON_PATH = Path(STR_QTFORPYTHON_PATH.format(LY_DEV)).resolve() + # QTFORPYTHON_PATH = Path(STR_QTFORPYTHON_PATH.format(O3DE_DEV)).resolve() # os.environ["DYNACONF_QTFORPYTHON_PATH"] = str(QTFORPYTHON_PATH) # site.addsitedir(str(QTFORPYTHON_PATH)) # PYTHONPATH - QT_PLUGIN_PATH = Path.joinpath(LY_BIN_PATH, - 'EditorPlugins').resolve() - os.environ["DYNACONF_QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH) + QT_PLUGIN_PATH = Path.joinpath(_O3DE_BIN_PATH,'EditorPlugins') + os.environ["DYNACONF_QT_PLUGIN_PATH"] = str(QT_PLUGIN_PATH.resolve()) os.environ['PATH'] = QT_PLUGIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] - QT_QPA_PLATFORM_PLUGIN_PATH = Path.joinpath(QT_PLUGIN_PATH, - 'platforms').resolve() - os.environ["DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH) + QT_QPA_PLATFORM_PLUGIN_PATH = Path.joinpath(QT_PLUGIN_PATH, 'platforms') + os.environ["DYNACONF_QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH.resolve()) # if the line below is removed external standalone apps can't load PySide2 - os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH) + os.environ["QT_QPA_PLATFORM_PLUGIN_PATH"] = str(QT_QPA_PLATFORM_PLUGIN_PATH.resolve()) # ^^ bypass trying to set only with DYNACONF environment os.environ['PATH'] = QT_QPA_PLATFORM_PLUGIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] # ^^ this particular env only works correctly if put on the PATH in this manner # add Qt binaries to the Windows path to handle findings DLL file dependencies if sys.platform.startswith('win'): - # path = os.environ['PATH'] - # newPath = '' - # newPath += str(LY_BIN_PATH) + os.pathsep - # newPath += str(Path.joinpath(QTFORPYTHON_PATH, - # 'shiboken2').resolve()) + os.pathsep - # newPath += str(Path.joinpath(QTFORPYTHON_PATH, - # 'PySide2').resolve()) + os.pathsep - # newPath += path - # os.environ['PATH']=newPath - _LOGGER.debug('PySide2 bootstrapped PATH for Windows.') + _LOGGER.info('~ Qt/PySide2 bootstrapped PATH for Windows.') + else: + _LOGGER.warning('~ Not tested on Non-Windows platforms.') + # To Do: figure out how to test and/or modify to work try: import PySide2 - _LOGGER.debug('DCCsi, config.py: SUCCESS: import PySide2') + _LOGGER.info('~ SUCCESS: import PySide2') _LOGGER.debug(PySide2) status = True except ImportError as e: - _LOGGER.debug('DCCsi, config.py: FAILURE: import PySide2') + _LOGGER.error('~ FAILURE: import PySide2') status = False raise(e) try: import shiboken2 - _LOGGER.debug('DCCsi, config.py: SUCCESS: import shiboken2') + _LOGGER.info('~ SUCCESS: import shiboken2') _LOGGER.debug(shiboken2) status = True except ImportError as e: - _LOGGER.debug('DCCsi, config.py: FAILURE: import shiboken2') + _LOGGER.error('~ FAILURE: import shiboken2') status = False raise(e) @@ -162,19 +223,44 @@ def init_ly_pyside(LY_DEV=None): # to do: move path construction string to constants and build off of SDK # have not done that yet as I really want to get legal approval and # add this to the QtForPython Gem - # please pass this on the current code review - DCCSI_PYSIDE2_TOOLS = Path.joinpath(LY_DEV, - 'Gems', - 'AtomLyIntegration', - 'TechnicalArt', - 'DccScriptingInterface', - '.dev', - 'QtForPython', - 'pyside2-tools-dev') - os.environ["DYNACONF_DCCSI_PYSIDE2_TOOLS"] = str(DCCSI_PYSIDE2_TOOLS.resolve()) - os.environ['PATH'] = DCCSI_PYSIDE2_TOOLS.as_posix() + os.pathsep + os.environ['PATH'] + # please pass this in current code reviews + _DCCSI_PYSIDE2_TOOLS = Path(_DCCSI_PYTHON_PATH,'pyside2-tools') + if _DCCSI_PYSIDE2_TOOLS.exists(): + os.environ["DYNACONF_DCCSI_PYSIDE2_TOOLS"] = str(_DCCSI_PYSIDE2_TOOLS.resolve()) + os.environ['PATH'] = _DCCSI_PYSIDE2_TOOLS.as_posix() + os.pathsep + os.environ['PATH'] + + site.addsitedir(_DCCSI_PYSIDE2_TOOLS) + _O3DE_PYTHONPATH.append(_DCCSI_PYSIDE2_TOOLS.resolve()) + _LOGGER.info('~ PySide2-Tools bootstrapped PATH for Windows.') + try: + import pyside2uic + _LOGGER.info('~ SUCCESS: import pyside2uic') + _LOGGER.debug(shiboken2) + status = True + except ImportError as e: + _LOGGER.error('~ FAILURE: import pyside2uic') + status = False + raise(e) + else: + _LOGGER.warning('~ No PySide2 Tools: {}'.format(_DCCSI_PYSIDE2_TOOLS.resolve)) + + _O3DE_DCCSI_PATH = os.environ['PATH'] + os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + + try: + _DCCSI_PYTHONPATH = os.environ['PYTHONPATH'] + os.environ["DYNACONF_PYTHONPATH"] = _DCCSI_PYTHONPATH + except: + pass - return status + from dynaconf import settings + + _LOGGER.info('~ config.init_o3de_pyside() ... DONE') + + if status: + return settings + else: + return None # ------------------------------------------------------------------------- @@ -182,90 +268,219 @@ def init_ly_pyside(LY_DEV=None): def test_pyside2(): """Convenience method to test Qt / PySide2 access""" # now test - _LOGGER.info('Testing Qt / PySide2') + _LOGGER.info('~ Testing Qt / PySide2') try: from PySide2.QtWidgets import QApplication, QPushButton app = QApplication(sys.argv) - hello = QPushButton("Hello world!") + hello = QPushButton("~ O3DE DCCsi PySide2 Test!") hello.resize(200, 60) hello.show() except Exception as e: - _LOGGER.error('FAILURE: Qt / PySide2') + _LOGGER.error('~ FAILURE: Qt / PySide2') status = False raise(e) - _LOGGER.info('SUCCESS: .test_pyside2()') + _LOGGER.info('~ SUCCESS: .test_pyside2()') sys.exit(app.exec_()) # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- -# `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`. -# `settings_files` = Load this files in the order. -# here we are modifying or adding to the dynamic config settings on import -settings = Dynaconf( - envvar_prefix="DYNACONF", - settings_files=['settings.json', '.secrets.json'], -) +def init_o3de_core(engine_path=_O3DE_DEV, + build_folder=_O3DE_BUILD_FOLDER, + project_name=None, + project_path=_O3DE_PROJECT_PATH): + """Initialize the DCCsi Core dynamic env and settings""" + # `envvar_prefix` = export envvars with `export DYNACONF_FOO=bar`. + # `settings_files` = Load this files in the order. + # here we are modifying or adding to the dynamic config settings on import + settings = Dynaconf(envvar_prefix='DYNACONF', + settings_files=['settings.json', + 'dev.settings.json', + 'user.settings.json', + '.secrets.json']) + + # global settings + os.environ["DYNACONF_DCCSI_OS_FOLDER"] = azpy.config_utils.get_os() + os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) + os.environ["DYNACONF_DCCSI_DEV_MODE"] = str(_DCCSI_DEV_MODE) + os.environ['DYNACONF_DCCSI_LOGLEVEL'] = str(_DCCSI_LOGLEVEL) -from azpy.constants import PATH_LY_BUILD_PATH -from azpy.constants import PATH_LY_BIN_PATH + os.environ["DYNACONF_DCCSI_PATH"] = str(_DCCSI_PATH.resolve()) + os.environ['PATH'] = _DCCSI_PATH.as_posix() + os.pathsep + os.environ['PATH'] -# global settings -os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) -os.environ["DYNACONF_DCCSI_DEV_MODE"] = str(_DCCSI_DEV_MODE) + # we already defaulted to discovering these two early because of importance + #os.environ["DYNACONF_O3DE_DEV"] = str(_O3DE_DEV.resolve()) + #os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_PROJECT_PATH) + # we also already added them to DYNACONF_ + # this in an explicit pass in + if project_path: + _project_path = Path(project_path) + try: + _project_path.exists() + _O3DE_PROJECT_PATH = _project_path + os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_PROJECT_PATH.resolve()) + except FileExistsError as e: + _LOGGER.error('~ The project path specified does not appear to exist!') + _LOGGER.warning('~ project_path: {}'.format(project_path)) + _LOGGER.warning('~ fallback to engine root: {}'.format()) + project_path = _O3DE_DEV + os.environ["DYNACONF_O3DE_PROJECT_PATH"] = str(_O3DE_DEV.resolve()) -# search up to get \dev -_LY_DEV = azpy.config_utils.get_stub_check_path(in_path=_DCCSIG_PATH, - check_stub='engine.json') -os.environ["DYNACONF_LY_DEV"] = str(_LY_DEV.resolve()) -_LY_PROJECT = azpy.config_utils.get_current_project() -os.environ["DYNACONF_LY_PROJECT"] = str(_LY_PROJECT.resolve()) -_LY_PROJECT_PATH = Path(_LY_DEV, _LY_PROJECT) -os.environ["DYNACONF_LY_PROJECT_PATH"] = str(_LY_PROJECT_PATH) -os.environ["DYNACONF_DCCSIG_PATH"] = str(_DCCSIG_PATH) -_DCCSI_CONFIG_PATH = Path(_MODULE_PATH).resolve() -os.environ["DYNACONF_DCCSI_CONFIG_PATH"] = str(_DCCSI_CONFIG_PATH) -_DCCSIG_SDK_PATH = Path.joinpath(_DCCSIG_PATH, 'SDK').resolve() -os.environ["DYNACONF_DCCSIG_SDK_PATH"] = str(_DCCSIG_SDK_PATH) -os.environ["DYNACONF_DCCSI_PYTHON_LIB_PATH"] = str(_DCCSI_PYTHON_LIB_PATH) -os.environ["DYNACONF_OS_FOLDER"] = azpy.config_utils.get_os() + # we can pull the O3DE_PROJECT (name) from the project path + if not project_name: + project_name = Path(_O3DE_PROJECT_PATH).name + os.environ["DYNACONF_O3DE_PROJECT"] = str(project_name) + # To Do: there might be a project namespace in the project.json? -# we need to set up the Ly dev build \bin\path (for Qt dll access) -_LY_BUILD_PATH = Path(PATH_LY_BUILD_PATH).resolve() -os.environ["DYNACONF_LY_BUILD_PATH"] = str(_LY_BUILD_PATH) -_LY_BIN_PATH = Path(PATH_LY_BIN_PATH).resolve() -os.environ["DYNACONF_LY_BIN_PATH"] = str(_LY_BIN_PATH) + # -- O3DE build -- set up \bin\path (for Qt dll access) + os.environ["DYNACONF_O3DE_BUILD_FOLDER"] = str(build_folder) + _O3DE_BUILD_PATH = Path(_O3DE_DEV, build_folder) + + os.environ["DYNACONF_O3DE_BUILD_PATH"] = str(_O3DE_BUILD_PATH.resolve()) + + _O3DE_BIN_PATH = Path(STR_O3DE_BIN_PATH.format(_O3DE_BUILD_PATH)) + os.environ["DYNACONF_O3DE_BIN_PATH"] = str(_O3DE_BIN_PATH.resolve()) + + # hard check + if not _O3DE_BIN_PATH.exists(): + raise Exception('O3DE_BIN_PATH does NOT exist: {0}'.format(_O3DE_BIN_PATH)) + else: + # adding to sys.path apparently doesn't work for .dll locations like Qt + os.environ['PATH'] = _O3DE_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] + # -- + + from azpy.constants import TAG_DIR_DCCSI_TOOLS + _DCCSI_TOOLS_PATH = Path(_DCCSI_PATH, TAG_DIR_DCCSI_TOOLS) + os.environ["DYNACONF_DCCSI_TOOLS_PATH"] = str(_DCCSI_TOOLS_PATH.resolve()) + + from azpy.constants import TAG_DCCSI_NICKNAME + from azpy.constants import PATH_DCCSI_LOG_PATH + _DCCSI_LOG_PATH = Path(PATH_DCCSI_LOG_PATH.format(O3DE_PROJECT_PATH=project_path, + TAG_DCCSI_NICKNAME=TAG_DCCSI_NICKNAME)) + os.environ["DYNACONF_DCCSI_LOG_PATH"] = str(_DCCSI_LOG_PATH) + + from azpy.constants import TAG_DIR_REGISTRY, TAG_DCCSI_CONFIG + _DCCSI_CONFIG_PATH = Path(project_path, TAG_DIR_REGISTRY, TAG_DCCSI_CONFIG) + os.environ["DYNACONF_DCCSI_CONFIG_PATH"] = str(_DCCSI_CONFIG_PATH.resolve()) + + from azpy.constants import TAG_DIR_DCCSI_TOOLS + _DCCSIG_TOOLS_PATH = Path.joinpath(_DCCSI_PATH, TAG_DIR_DCCSI_TOOLS) + os.environ["DYNACONF_DCCSIG_TOOLS_PATH"] = str(_DCCSIG_TOOLS_PATH.resolve()) + + _O3DE_DCCSI_PATH = os.environ['PATH'] + os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + + from dynaconf import settings + + _LOGGER.info('~ config.init_o3de_core() ... DONE') + + return settings +# ------------------------------------------------------------------------- -# project cache log dir path -from azpy.constants import ENVAR_DCCSI_LOG_PATH -from azpy.constants import PATH_DCCSI_LOG_PATH -_DCCSI_LOG_PATH = Path(os.getenv(ENVAR_DCCSI_LOG_PATH, - Path(PATH_DCCSI_LOG_PATH.format(LY_DEV=_LY_DEV, - LY_PROJECT=_LY_PROJECT)))) -os.environ["DYNACONF_DCCSI_LOG_PATH"] = str(_DCCSI_LOG_PATH) -# hard checks -if not _LY_BIN_PATH.exists(): - raise Exception('LY_BIN_PATH does NOT exist: {0}'.format(_LY_BIN_PATH)) -else: - # adding to sys.path apparently doesn't work for .dll locations like Qt - os.environ['PATH'] = _LY_BIN_PATH.as_posix() + os.pathsep + os.environ['PATH'] +# ------------------------------------------------------------------------- +def init_o3de_python(engine_path=_O3DE_DEV, + engine_bin=_O3DE_BIN_PATH, + dccsi_path=_DCCSI_PATH): + + # pathify + _O3DE_DEV = Path(engine_path) + _O3DE_BIN_PATH = Path(engine_bin) + _DCCSI_PATH = Path(dccsi_path) + + # python config + _DCCSI_PYTHON_PATH = Path(_DCCSI_PATH,'3rdParty','Python') + os.environ["DYNACONF_DCCSI_PYTHON_PATH"] = str(_DCCSI_PYTHON_PATH.resolve()) + + _DCCSI_PYTHON_LIB_PATH = azpy.config_utils.bootstrap_dccsi_py_libs(_DCCSI_PATH) + os.environ["DYNACONF_DCCSI_PYTHON_LIB_PATH"] = str(_DCCSI_PYTHON_LIB_PATH.resolve()) + os.environ['PATH'] = _DCCSI_PYTHON_LIB_PATH.as_posix() + os.pathsep + os.environ['PATH'] + site.addsitedir(_DCCSI_PYTHON_LIB_PATH) + _O3DE_PYTHONPATH.append(_DCCSI_PYTHON_LIB_PATH.resolve()) + + site.addsitedir(_O3DE_BIN_PATH) + _O3DE_PYTHONPATH.append(_O3DE_BIN_PATH.resolve()) + + _O3DE_PY_EXE = Path(sys.executable) + _DCCSI_PY_IDE = Path(_O3DE_PY_EXE) + os.environ["DYNACONF_DCCSI_PY_IDE"] = str(_DCCSI_PY_IDE.resolve()) + + _O3DE_PYTHONHOME = Path(_O3DE_PY_EXE.parents[0]) + os.environ["DYNACONF_O3DE_PYTHONHOME"] = str(_O3DE_PYTHONHOME.resolve()) + os.environ['PATH'] = _O3DE_PYTHONHOME.as_posix() + os.pathsep + os.environ['PATH'] + _LOGGER.info('~ O3DE_PYTHONHOME - is now the folder containing O3DE python executable') + + _O3DE_PYTHON_INSTALL = Path(_O3DE_DEV, 'python') + os.environ["DYNACONF_O3DE_PYTHON_INSTALL"] = str(_O3DE_PYTHON_INSTALL.resolve()) + os.environ['PATH'] = _O3DE_PYTHON_INSTALL.as_posix() + os.pathsep + os.environ['PATH'] -_LOGGER.info('Dynaconf config.py ... DONE') + if sys.platform.startswith('win'): + _DCCSI_PY_BASE = Path(_O3DE_PYTHON_INSTALL, 'python.cmd') + elif sys.platform == "linux": + _DCCSI_PY_BASE = Path(_O3DE_PYTHON_INSTALL, 'python.sh') + elif sys.platform == "darwin": + _DCCSI_PY_BASE = Path(_O3DE_PYTHON_INSTALL, 'python.sh') + else: + _DCCSI_PY_BASE = None + + if _DCCSI_PY_BASE: + os.environ["DYNACONF_DCCSI_PY_BASE"] = str(_DCCSI_PY_BASE.resolve()) + + _O3DE_DCCSI_PATH = os.environ['PATH'] + os.environ["DYNACONF_PATH"] = _O3DE_DCCSI_PATH + + try: + _DCCSI_PYTHONPATH = os.environ['PYTHONPATH'] + os.environ["DYNACONF_PYTHONPATH"] = _DCCSI_PYTHONPATH + except: + pass + + from dynaconf import settings + + _LOGGER.info('~ config.init_o3de_python() ... DONE') + + return settings # ------------------------------------------------------------------------- # ------------------------------------------------------------------------- # settings.setenv() # doing this will add the additional DYNACONF_ envars -def get_config_settings(setup_ly_pyside=False): - """Convenience method to set and retreive settings directly from module.""" +def get_config_settings(engine_path=_O3DE_DEV, + build_folder=_O3DE_BUILD_FOLDER, + project_name=None, + project_path=_O3DE_PROJECT_PATH, + enable_o3de_python=None, + enable_o3de_pyside2=None, + set_env=True): + """Convenience method to initialize and retreive settings directly from module.""" + + settings = init_o3de_core(engine_path, + build_folder, + project_name, + project_path) + + if enable_o3de_python: + settings = init_o3de_python(settings.O3DE_DEV, + settings.O3DE_BIN_PATH, + settings.DCCSI_PATH) + + # These should ONLY be set for O3DE and non-DCC environments + # They will most likely cause other Qt/PySide DCC apps to fail + # or hopefully they can be overridden for DCC envionments + # that provide their own Qt dlls and Pyside2 + # _LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) + # _LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) + # assume our standalone python tools wants this access? + # it's safe to do this for dev and from ide + if enable_o3de_pyside2: + settings = init_o3de_pyside2(settings.DCCSI_PATH, + settings.O3DE_BIN_PATH) + + # now standalone we can validate the config. env, settings. from dynaconf import settings - - if setup_ly_pyside: - init_ly_pyside(settings.LY_DEV) - - settings.setenv() + if set_env: + settings.setenv() return settings # --- END ----------------------------------------------------------------- @@ -274,54 +489,216 @@ def get_config_settings(setup_ly_pyside=False): # Main Code Block, runs this script as main (testing) # ------------------------------------------------------------------------- if __name__ == '__main__': - """Run this file as main""" + """Run this file as a standalone cli script""" + + _MODULENAME = __name__ + if _MODULENAME is '__main__': + _MODULENAME = 'DCCsi.config' + + from azpy.constants import STR_CROSSBAR + + while 0: # temp internal debug flag + _DCCSI_GDEBUG = True + break + + # overide logger for standalone to be more verbose and log to file + _LOGGER = azpy.initialize_logger(_MODULENAME, + log_to_file=_DCCSI_GDEBUG, + default_log_level=_DCCSI_LOGLEVEL) - _LOG_LEVEL = int(10) # same as _logging.DEBUG + # happy print + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('~ constants.py ... Running script as __main__') + _LOGGER.info(STR_CROSSBAR) - _LOGGER = azpy.initialize_logger(_PACKAGENAME, - log_to_file=True, - default_log_level=_LOG_LEVEL) + # go ahead and run the rest of the configuration + # parse the command line args + import argparse + parser = argparse.ArgumentParser( + description='O3DE DCCsi Dynamic Config (dynaconf)', + epilog="Attempts to determine O3DE project if -pp not set") + parser.add_argument('-gd', '--global-debug', + type=bool, + required=False, + help='Enables global debug flag.') + parser.add_argument('-dm', '--developer-mode', + type=bool, + required=False, + help='Enables dev mode for early auto attaching debugger.') + parser.add_argument('-ep', '--engine-path', + type=pathlib.Path, + required=False, + help='The path to the o3de engine.') + parser.add_argument('-bf', '--build-folder', + type=str, + required=False, + help='The name (tag) of the o3de build folder, example build or windows_vs2019.') + parser.add_argument('-pp', '--project-path', + type=pathlib.Path, + required=False, + help='The path to the project.') + parser.add_argument('-pn', '--project-name', + type=str, + required=False, + help='The name of the project.') + parser.add_argument('-py', '--enable-python', + type=bool, + required=False, + help='Enables O3DE python access.') + parser.add_argument('-qt', '--enable-qt', + type=bool, + required=False, + help='Enables O3DE Qt\PySide2 access.') + parser.add_argument('-sd', '--set-debugger', + type=str, + required=False, + help='Default debugger: WING, others: PYCHARM, VSCODE (not yet implemented).') + parser.add_argument('-pc', '--project-config', + type=bool, + required=False, + help='Enables reading the projects registry\dccsiconfiguration.setreg.') + parser.add_argument('-es', '--export-settings', + type=pathlib.Path, + required=False, + help='Writes managed settings to specified path.') + parser.add_argument('-ec', '--export-configuration', + type=bool, + required=False, + help='writes settings as a O3DE registry\dccsiconfiguration.setreg.') + parser.add_argument('-tp', '--test-pyside2', + type=bool, + required=False, + help='Runs Qt/PySide2 tests and reports.') + args = parser.parse_args() - from dynaconf import settings + # easy overrides + if args.global_debug: + _DCCSI_GDEBUG = True + os.environ["DYNACONF_DCCSI_GDEBUG"] = str(_DCCSI_GDEBUG) + if args.developer_mode: + attach_debugger() # attempts to start debugger + if args.set_debugger: + _LOGGER.info('Setting and switching debugger type from WingIDE not implemented.') + # To Do: implement debugger plugin pattern + + # need to do a little plumbing + if not args.engine_path: + args.engine_path=_O3DE_DEV + if not args.build_folder: + from azpy.constants import TAG_DIR_O3DE_BUILD_FOLDER + args.build_folder = TAG_DIR_O3DE_BUILD_FOLDER + if not args.project_path: + args.project_path=_O3DE_PROJECT_PATH + + if _DCCSI_GDEBUG: + args.enable_python = True + args.enable_qt = True + + # now standalone we can validate the config. env, settings. + settings = get_config_settings(engine_path=args.engine_path, + build_folder=args.build_folder, + project_name=args.project_name, + project_path=args.project_path, + enable_o3de_python=args.enable_python, + enable_o3de_pyside2=args.enable_qt) + + ## CORE + _LOGGER.info(STR_CROSSBAR) # not using fstrings in this module because it might run in py2.7 (maya) _LOGGER.info('DCCSI_GDEBUG: {}'.format(settings.DCCSI_GDEBUG)) _LOGGER.info('DCCSI_DEV_MODE: {}'.format(settings.DCCSI_DEV_MODE)) _LOGGER.info('DCCSI_LOGLEVEL: {}'.format(settings.DCCSI_LOGLEVEL)) - - _LOGGER.info('OS_FOLDER: {}'.format(settings.OS_FOLDER)) - _LOGGER.info('LY_PROJECT: {}'.format(settings.LY_PROJECT)) - _LOGGER.info('LY_PROJECT_PATH: {}'.format(settings.LY_PROJECT_PATH)) - _LOGGER.info('LY_DEV: {}'.format(settings.LY_DEV)) - _LOGGER.info('LY_BUILD_PATH: {}'.format(settings.LY_BUILD_PATH)) - _LOGGER.info('LY_BIN_PATH: {}'.format(settings.LY_BIN_PATH)) - + _LOGGER.info('DCCSI_OS_FOLDER: {}'.format(settings.DCCSI_OS_FOLDER)) + + _LOGGER.info('O3DE_DEV: {}'.format(settings.O3DE_DEV)) + _LOGGER.info('O3DE_O3DE_BUILD_FOLDER: {}'.format(settings.O3DE_BUILD_PATH)) + _LOGGER.info('O3DE_BUILD_PATH: {}'.format(settings.O3DE_BUILD_PATH)) + _LOGGER.info('O3DE_BIN_PATH: {}'.format(settings.O3DE_BIN_PATH)) + + _LOGGER.info('O3DE_PROJECT: {}'.format(settings.O3DE_PROJECT)) + _LOGGER.info('O3DE_PROJECT_PATH: {}'.format(settings.O3DE_PROJECT_PATH)) + + _LOGGER.info('DCCSI_PATH: {}'.format(settings.DCCSI_PATH)) _LOGGER.info('DCCSI_LOG_PATH: {}'.format(settings.DCCSI_LOG_PATH)) - _LOGGER.info('DCCSI_CONFIG_PATH: {}'.format(settings.DCCSI_CONFIG_PATH)) - _LOGGER.info('DCCSIG_PATH: {}'.format(settings.DCCSIG_PATH)) - _LOGGER.info('DCCSI_PYTHON_LIB_PATH: {}'.format(settings.DCCSI_PYTHON_LIB_PATH)) - _LOGGER.info('DDCCSI_PY_BASE: {}'.format(settings.DDCCSI_PY_BASE)) - - # To Do: These should ONLY be set for Lumberyard and non-DCC environments - # They will most likely cause Qt/PySide DCC apps to fail - # or hopefully they can be overridden for DCC envionments - # that provide their own Qt dlls and Pyside2 - #_LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) - #_LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) - - init_ly_pyside(settings.LY_DEV) # init lumberyard Qt/PySide2 - # from dynaconf import settings # <-- no need to reimport + + if settings.O3DE_DCCSI_ENV_TEST: + _LOGGER.info('O3DE_DCCSI_ENV_TEST: {}'.format(settings.O3DE_DCCSI_ENV_TEST)) + + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('') + + if args.enable_python: + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('DCCSI_PYTHON_PATH'.format(settings.DCCSI_PYTHON_PATH)) + _LOGGER.info('DCCSI_PYTHON_LIB_PATH: {}'.format(settings.DCCSI_PYTHON_LIB_PATH)) + _LOGGER.info('DCCSI_PY_IDE'.format(settings.DCCSI_PY_IDE)) + _LOGGER.info('O3DE_PYTHONHOME'.format(settings.O3DE_PYTHONHOME)) + _LOGGER.info('O3DE_PYTHON_INSTALL'.format(settings.O3DE_PYTHON_INSTALL)) + _LOGGER.info('DCCSI_PY_BASE: {}'.format(settings.DCCSI_PY_BASE)) + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('') + else: + _LOGGER.info('Tip: add arg --enable-python to extend the environment with O3DE python access') + + if args.enable_qt: + _LOGGER.info(STR_CROSSBAR) + # _LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) + _LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) + _LOGGER.info('QT_QPA_PLATFORM_PLUGIN_PATH: {}'.format(settings.QT_QPA_PLATFORM_PLUGIN_PATH)) + _LOGGER.info('DCCSI_PYSIDE2_TOOLS: {}'.format(settings.DCCSI_PYSIDE2_TOOLS)) + _LOGGER.info(STR_CROSSBAR) + _LOGGER.info('') + else: + _LOGGER.info('Tip: add arg --enable-qt to extend the environment with O3DE Qt/PySide2 support') + settings.setenv() # doing this will add/set the additional DYNACONF_ envars + + if _DCCSI_GDEBUG or args.export_settings: + # to do: need to add malformed json validation to \settings.json + # this can cause a bad error that is deep and hard to debug + + # writting settings + from dynaconf import loaders + from dynaconf.utils.boxing import DynaBox + + _settings_dict = settings.as_dict() + + # default temp filename + _settings_file = Path('settings_export.json.tmp') + + # writes to a file, the format is inferred by extension + # can be .yaml, .toml, .ini, .json, .py + #loaders.write(_settings_file, DynaBox(data).to_dict(), merge=False, env='development') + #loaders.write(_settings_file, DynaBox(data).to_dict()) + + # we want to possibly modify or stash our settings into a o3de .setreg + from box import Box + _settings_box = Box(_settings_dict) + + _LOGGER.info('Pretty print, _settings_box: {}'.format(_settings_file)) + _LOGGER.info(str(_settings_box.to_json(sort_keys=True, + indent=4))) + + # writes settings box + _settings_box.to_json(filename=_settings_file.as_posix(), + sort_keys=True, + indent=4) + + if _DCCSI_GDEBUG or args.test_pyside2: + test_pyside2() # test PySide2 access with a pop-up button + try: + import pyside2uic + except ImportError as e: + _LOGGER.warning("Could not import 'pyside2uic'") + _LOGGER.warning("Refer to: '< local DCCsi >\3rdParty\Python\README.txt'") + _LOGGER.error(e) - #_LOGGER.info('QTFORPYTHON_PATH: {}'.format(settings.QTFORPYTHON_PATH)) - _LOGGER.info('LY_BIN_PATH: {}'.format(settings.LY_BIN_PATH)) - _LOGGER.info('QT_PLUGIN_PATH: {}'.format(settings.QT_PLUGIN_PATH)) - _LOGGER.info('QT_QPA_PLATFORM_PLUGIN_PATH: {}'.format(settings.QT_QPA_PLATFORM_PLUGIN_PATH)) - _LOGGER.info('DCCSI_PYSIDE2_TOOLS: {}'.format(settings.DCCSI_PYSIDE2_TOOLS)) - - test_pyside2() # test PySide2 access with a pop-up button + # return + sys.exit() +# --- END ----------------------------------------------------------------- diff --git a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json index 0967ef424b..718c6110c3 100644 --- a/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json +++ b/Gems/AtomLyIntegration/TechnicalArt/DccScriptingInterface/settings.json @@ -1 +1,13 @@ -{} +{ + "LOAD_DOTENV": true, + "COMPANY": "Amazon", + "DCCSI_GDEBUG": "False", + "DCCSI_DEV_MODE": "False", + "DCCSI_GDEBUGGER": "WING", + "DCCSI_LOGLEVEL": 20, + "DEFAULT_SETTINGS_PATHS": [ + "settings.py", + "settings.json", + ".secrets.json" + ] +} \ No newline at end of file diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass index 6ae8b9526e..c8c90e24f7 100644 --- a/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass +++ b/Gems/AtomTressFX/Assets/Passes/HairParentPass.pass @@ -71,6 +71,12 @@ } } ], + "FallbackConnections": [ + { + "Input": "DepthLinearInput", + "Output": "DepthLinear" + } + ], "PassRequests": [ { "Name": "HairGlobalShapeConstraintsComputePass", diff --git a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass index 7322611e2a..83f9f0d432 100644 --- a/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass +++ b/Gems/AtomTressFX/Assets/Passes/HairParentShortCutPass.pass @@ -12,7 +12,8 @@ "SlotType": "InputOutput", "ScopeAttachmentUsage": "RenderTarget" }, - { // used for copy from MSAA to regular RT + { + // used for copy from MSAA to regular RT "Name": "RenderTargetInputOnly", "SlotType": "Input", "ScopeAttachmentUsage": "Shader" @@ -29,7 +30,7 @@ // If DepthLinear is not availbale - connect to another viewport (non MSAA) image. { "Name": "DepthLinearInput", - "SlotType": "InputOutput" + "SlotType": "Input" }, { "Name": "DepthLinear", @@ -71,6 +72,12 @@ } } ], + "FallbackConnections": [ + { + "Input": "DepthLinearInput", + "Output": "DepthLinear" + } + ], "PassRequests": [ { "Name": "HairGlobalShapeConstraintsComputePass", @@ -257,7 +264,8 @@ "Attachment": "HairColorRenderTarget" } }, - { // The final render target - this is MSAA mode RT - would it be cheaper to + { + // The final render target - this is MSAA mode RT - would it be cheaper to // use non-MSAA and then copy? "LocalSlot": "RenderTargetInputOutput", "AttachmentRef": { @@ -340,7 +348,8 @@ "TemplateName": "HairShortCutResolveColorPassTemplate", "Enabled": true, "Connections": [ - { // The final render target - this is MSAA mode RT - would it be cheaper to + { + // The final render target - this is MSAA mode RT - would it be cheaper to // use non-MSAA and then copy? "LocalSlot": "RenderTargetInputOutput", "AttachmentRef": { diff --git a/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli b/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli index 4b0fa654e3..ffabf7c298 100644 --- a/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli +++ b/Gems/AtomTressFX/Assets/Shaders/HairLightTypes.azsli @@ -465,7 +465,7 @@ void ApplyLighting(inout Surface surface, inout LightingData lightingData) const uint shadowIndex = ViewSrg::m_shadowIndexDirectionalLight; if (o_enableShadows && shadowIndex < SceneSrg::m_directionalLightCount) { - DirectionalLightShadow::GetShadowCoords(shadowIndex, surface.position, lightingData.shadowCoords); + DirectionalLightShadow::GetShadowCoords(shadowIndex, surface.position, surface.vertexNormal, lightingData.shadowCoords); } // Light loops application. diff --git a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp index b7c6bbce13..a0be18f0e2 100644 --- a/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp +++ b/Gems/AtomTressFX/Code/Rendering/HairFeatureProcessor.cpp @@ -144,25 +144,11 @@ namespace AZ void HairFeatureProcessor::EnablePasses([[maybe_unused]] bool enable) { - return; - - // [To Do] - This part should be enabled (remove the return) to reduce overhead - // when Hair is disabled / doesn't exist in the scene. - // Currently it might break features such as fog that depend on the output and for some - // reason doesn't quite work for ShortCut. - // The current overhead is minimal (< 0.1 msec) and this Gem is disabled by default. -/* - if (!m_initialized) - { - return; - } - RPI::Ptr desiredPass = m_renderPipeline->GetRootPass()->FindPassByNameRecursive(HairParentPassName); if (desiredPass) { desiredPass->SetEnabled(enable); } -*/ } bool HairFeatureProcessor::RemoveHairRenderObject(Data::Instance renderObject) @@ -184,15 +170,13 @@ namespace AZ void HairFeatureProcessor::UpdateHairSkinning() { - // Copying CPU side m_SimCB content to the GPU buffer (matrices, wind parameters..) - - for (auto objIter = m_hairRenderObjects.begin(); objIter != m_hairRenderObjects.end(); ++objIter) + // Copying CPU side m_SimCB content to the GPU buffer (matrices, wind parameters..) + for (auto& hairRenderObject : m_hairRenderObjects) { - if (!objIter->get()->IsEnabled()) + if (hairRenderObject->IsEnabled()) { - return; + hairRenderObject->Update(); } - objIter->get()->Update(); } } diff --git a/Gems/AudioEngineWwise/Code/CMakeLists.txt b/Gems/AudioEngineWwise/Code/CMakeLists.txt index 98a31fb91a..33c80bc31c 100644 --- a/Gems/AudioEngineWwise/Code/CMakeLists.txt +++ b/Gems/AudioEngineWwise/Code/CMakeLists.txt @@ -18,31 +18,18 @@ set(AUDIOENGINEWWISE_COMPILEDEFINITIONS find_package(Wwise MODULE) ################################################################################ -# Server / Unsupported +# Servers +# (and situations where Wwise SDK is not found or otherwise unavailable) ################################################################################ -if (PAL_TRAIT_BUILD_SERVER_SUPPORTED OR PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # Stub gem for server and unsupported platforms. Audio Engine Wwise is client only - ly_add_target( - NAME AudioEngineWwise.Stub ${PAL_TRAIT_MONOLITHIC_DRIVEN_MODULE_TYPE} - NAMESPACE Gem - FILES_CMAKE - audioenginewwise_stub_files.cmake - BUILD_DEPENDENCIES - PRIVATE - AZ::AzCore - ) -endif() - -if (PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB OR NOT Wwise_FOUND) - # setup aliases so stubs will be used if something references AudioEngineWwise(.Editor) - add_library(Gem::AudioEngineWwise ALIAS AudioEngineWwise.Stub) - add_library(Gem::AudioEngineWwise.Editor ALIAS AudioEngineWwise.Stub) +if(NOT PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED OR NOT Wwise_FOUND) + # Don't create any Gem targets and aliases. Nothing should depend on this + # Gem directly, because if it doesn't define targets it will cause an error. return() endif() ################################################################################ -# Runtime / Game +# Clients ################################################################################ ly_add_target( NAME AudioEngineWwise.Static STATIC @@ -181,7 +168,7 @@ if (PAL_TRAIT_BUILD_TESTS_SUPPORTED) endif() ################################################################################ -# Tools / Editor +# Tools / Builders ################################################################################ if (PAL_TRAIT_BUILD_HOST_TOOLS) ly_add_target( diff --git a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Android/PAL_android.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Linux/PAL_linux.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Mac/PAL_mac.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/Windows/PAL_windows.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake index e3729baea7..c706c464d3 100644 --- a/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake +++ b/Gems/AudioEngineWwise/Code/Platform/iOS/PAL_ios.cmake @@ -6,4 +6,4 @@ # # -set(PAL_TRAIT_AUDIO_ENGINE_WWISE_USE_STUB FALSE) +set(PAL_TRAIT_AUDIO_ENGINE_WWISE_SUPPORTED TRUE) diff --git a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp b/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp deleted file mode 100644 index 4344eb6072..0000000000 --- a/Gems/AudioEngineWwise/Code/Source/AudioEngineWwiseModule_Stub.cpp +++ /dev/null @@ -1,11 +0,0 @@ -/* - * 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 - -AZ_DECLARE_MODULE_CLASS(Gem_AudioEngineWwise, AZ::Module) diff --git a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h index 1fe6e35d75..949d3add0d 100644 --- a/Gems/Blast/Code/Tests/Mocks/BlastMocks.h +++ b/Gems/Blast/Code/Tests/Mocks/BlastMocks.h @@ -209,6 +209,7 @@ namespace Blast AZStd::shared_ptr( const Physics::ColliderConfiguration&, const Physics::ShapeConfiguration&)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void*)); + MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void*)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration&)); MOCK_METHOD0(GetDefaultMaterial, AZStd::shared_ptr()); MOCK_METHOD1( diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp index c5e50ed9dd..a0a811eff0 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphMotionNode.cpp @@ -597,7 +597,7 @@ namespace EMotionFX // reset several settings to rewind the motion instance motionInstance->ResetTimes(); motionInstance->SetIsFrozen(false); - SetSyncIndex(animGraphInstance, MCORE_INVALIDINDEX32); + SetSyncIndex(animGraphInstance, InvalidIndex); uniqueData->SetCurrentPlayTime(motionInstance->GetCurrentTime()); uniqueData->SetDuration(motionInstance->GetDuration()); uniqueData->SetPreSyncTime(uniqueData->GetCurrentPlayTime()); diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h index b515621c5b..95d9c0d77d 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNode.h @@ -213,12 +213,12 @@ namespace EMotionFX */ virtual void SkipOutput([[maybe_unused]] AnimGraphInstance* animGraphInstance) {} - MCORE_INLINE float GetDuration(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetDuration(); } + float GetDuration(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetDuration(); } virtual void SetCurrentPlayTime(AnimGraphInstance* animGraphInstance, float timeInSeconds) { FindOrCreateUniqueNodeData(animGraphInstance)->SetCurrentPlayTime(timeInSeconds); } virtual float GetCurrentPlayTime(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetCurrentPlayTime(); } - MCORE_INLINE size_t GetSyncIndex(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetSyncIndex(); } - MCORE_INLINE void SetSyncIndex(AnimGraphInstance* animGraphInstance, size_t syncIndex) { FindOrCreateUniqueNodeData(animGraphInstance)->SetSyncIndex(syncIndex); } + size_t GetSyncIndex(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetSyncIndex(); } + void SetSyncIndex(AnimGraphInstance* animGraphInstance, size_t syncIndex) { FindOrCreateUniqueNodeData(animGraphInstance)->SetSyncIndex(syncIndex); } virtual void SetPlaySpeed(AnimGraphInstance* animGraphInstance, float speedFactor) { FindOrCreateUniqueNodeData(animGraphInstance)->SetPlaySpeed(speedFactor); } virtual float GetPlaySpeed(AnimGraphInstance* animGraphInstance) const { return FindOrCreateUniqueNodeData(animGraphInstance)->GetPlaySpeed(); } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h index 1ad82b1d55..5c2f0b281b 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphNodeData.h @@ -51,52 +51,52 @@ namespace EMotionFX void Init(AnimGraphInstance* animGraphInstance, AnimGraphNode* node); void Init(AnimGraphNodeData* nodeData); - MCORE_INLINE AnimGraphNode* GetNode() const { return reinterpret_cast(m_object); } - MCORE_INLINE void SetNode(AnimGraphNode* node) { m_object = reinterpret_cast(node); } + AnimGraphNode* GetNode() const { return reinterpret_cast(m_object); } + void SetNode(AnimGraphNode* node) { m_object = reinterpret_cast(node); } - MCORE_INLINE void SetSyncIndex(size_t syncIndex) { m_syncIndex = syncIndex; } - MCORE_INLINE size_t GetSyncIndex() const { return m_syncIndex; } + void SetSyncIndex(size_t syncIndex) { m_syncIndex = syncIndex; } + size_t GetSyncIndex() const { return m_syncIndex; } - MCORE_INLINE void SetCurrentPlayTime(float absoluteTime) { m_currentTime = absoluteTime; } - MCORE_INLINE float GetCurrentPlayTime() const { return m_currentTime; } + void SetCurrentPlayTime(float absoluteTime) { m_currentTime = absoluteTime; } + float GetCurrentPlayTime() const { return m_currentTime; } - MCORE_INLINE void SetPlaySpeed(float speed) { m_playSpeed = speed; } - MCORE_INLINE float GetPlaySpeed() const { return m_playSpeed; } + void SetPlaySpeed(float speed) { m_playSpeed = speed; } + float GetPlaySpeed() const { return m_playSpeed; } - MCORE_INLINE void SetDuration(float durationInSeconds) { m_duration = durationInSeconds; } - MCORE_INLINE float GetDuration() const { return m_duration; } + void SetDuration(float durationInSeconds) { m_duration = durationInSeconds; } + float GetDuration() const { return m_duration; } - MCORE_INLINE void SetPreSyncTime(float timeInSeconds) { m_preSyncTime = timeInSeconds; } - MCORE_INLINE float GetPreSyncTime() const { return m_preSyncTime; } + void SetPreSyncTime(float timeInSeconds) { m_preSyncTime = timeInSeconds; } + float GetPreSyncTime() const { return m_preSyncTime; } - MCORE_INLINE void SetGlobalWeight(float weight) { m_globalWeight = weight; } - MCORE_INLINE float GetGlobalWeight() const { return m_globalWeight; } + void SetGlobalWeight(float weight) { m_globalWeight = weight; } + float GetGlobalWeight() const { return m_globalWeight; } - MCORE_INLINE void SetLocalWeight(float weight) { m_localWeight = weight; } - MCORE_INLINE float GetLocalWeight() const { return m_localWeight; } + void SetLocalWeight(float weight) { m_localWeight = weight; } + float GetLocalWeight() const { return m_localWeight; } - MCORE_INLINE uint8 GetInheritFlags() const { return m_inheritFlags; } + uint8 GetInheritFlags() const { return m_inheritFlags; } - MCORE_INLINE bool GetIsBackwardPlaying() const { return (m_inheritFlags & INHERITFLAGS_BACKWARD) != 0; } - MCORE_INLINE void SetBackwardFlag() { m_inheritFlags |= INHERITFLAGS_BACKWARD; } - MCORE_INLINE void ClearInheritFlags() { m_inheritFlags = 0; } + bool GetIsBackwardPlaying() const { return (m_inheritFlags & INHERITFLAGS_BACKWARD) != 0; } + void SetBackwardFlag() { m_inheritFlags |= INHERITFLAGS_BACKWARD; } + void ClearInheritFlags() { m_inheritFlags = 0; } - MCORE_INLINE uint8 GetPoseRefCount() const { return m_poseRefCount; } - MCORE_INLINE void IncreasePoseRefCount() { m_poseRefCount++; } - MCORE_INLINE void DecreasePoseRefCount() { m_poseRefCount--; } - MCORE_INLINE void SetPoseRefCount(uint8 refCount) { m_poseRefCount = refCount; } + uint8 GetPoseRefCount() const { return m_poseRefCount; } + void IncreasePoseRefCount() { m_poseRefCount++; } + void DecreasePoseRefCount() { m_poseRefCount--; } + void SetPoseRefCount(uint8 refCount) { m_poseRefCount = refCount; } - MCORE_INLINE uint8 GetRefDataRefCount() const { return m_refDataRefCount; } - MCORE_INLINE void IncreaseRefDataRefCount() { m_refDataRefCount++; } - MCORE_INLINE void DecreaseRefDataRefCount() { m_refDataRefCount--; } - MCORE_INLINE void SetRefDataRefCount(uint8 refCount) { m_refDataRefCount = refCount; } + uint8 GetRefDataRefCount() const { return m_refDataRefCount; } + void IncreaseRefDataRefCount() { m_refDataRefCount++; } + void DecreaseRefDataRefCount() { m_refDataRefCount--; } + void SetRefDataRefCount(uint8 refCount) { m_refDataRefCount = refCount; } - MCORE_INLINE void SetRefCountedData(AnimGraphRefCountedData* data) { m_refCountedData = data; } - MCORE_INLINE AnimGraphRefCountedData* GetRefCountedData() const { return m_refCountedData; } + void SetRefCountedData(AnimGraphRefCountedData* data) { m_refCountedData = data; } + AnimGraphRefCountedData* GetRefCountedData() const { return m_refCountedData; } - MCORE_INLINE const AnimGraphSyncTrack* GetSyncTrack() const { return m_syncTrack; } - MCORE_INLINE AnimGraphSyncTrack* GetSyncTrack() { return m_syncTrack; } - MCORE_INLINE void SetSyncTrack(AnimGraphSyncTrack* syncTrack) { m_syncTrack = syncTrack; } + const AnimGraphSyncTrack* GetSyncTrack() const { return m_syncTrack; } + AnimGraphSyncTrack* GetSyncTrack() { return m_syncTrack; } + void SetSyncTrack(AnimGraphSyncTrack* syncTrack) { m_syncTrack = syncTrack; } bool GetIsMirrorMotion() const { return m_isMirrorMotion; } void SetIsMirrorMotion(bool newValue) { m_isMirrorMotion = newValue; } diff --git a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp index e90491b696..c6d9b4eeec 100644 --- a/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp +++ b/Gems/EMotionFX/Code/EMotionFX/Source/AnimGraphStateTransition.cpp @@ -326,7 +326,7 @@ namespace EMotionFX uniqueData->m_totalSeconds = 0.0f; uniqueData->m_blendProgress = 0.0f; - m_targetNode->SetSyncIndex(animGraphInstance, MCORE_INVALIDINDEX32); + m_targetNode->SetSyncIndex(animGraphInstance, InvalidIndex); // Trigger action for (AnimGraphTriggerAction* action : m_actionSetup.GetActions()) diff --git a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h index 882d198092..6649cea55e 100644 --- a/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h +++ b/Gems/EMotionFX/Code/Tests/Mocks/PhysicsSystem.h @@ -46,6 +46,7 @@ namespace Physics } MOCK_METHOD2(CreateShape, AZStd::shared_ptr(const Physics::ColliderConfiguration& colliderConfiguration, const Physics::ShapeConfiguration& configuration)); MOCK_METHOD1(ReleaseNativeMeshObject, void(void* nativeMeshObject)); + MOCK_METHOD1(ReleaseNativeHeightfieldObject, void(void* nativeMeshObject)); MOCK_METHOD1(CreateMaterial, AZStd::shared_ptr(const Physics::MaterialConfiguration& materialConfiguration)); MOCK_METHOD3(CookConvexMeshToFile, bool(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount)); MOCK_METHOD3(CookConvexMeshToMemory, bool(const AZ::Vector3* vertices, AZ::u32 vertexCount, AZStd::vector& result)); diff --git a/Gems/GradientSignal/Code/CMakeLists.txt b/Gems/GradientSignal/Code/CMakeLists.txt index 64f565cf99..5b88044116 100644 --- a/Gems/GradientSignal/Code/CMakeLists.txt +++ b/Gems/GradientSignal/Code/CMakeLists.txt @@ -107,7 +107,16 @@ endif() # Tests ################################################################################ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) - + ly_add_target( + NAME GradientSignal.Mocks HEADERONLY + NAMESPACE Gem + FILES_CMAKE + gradientsignal_mocks_files.cmake + INCLUDE_DIRECTORIES + INTERFACE + Mocks + ) + ly_add_target( NAME GradientSignal.Tests ${PAL_TRAIT_TEST_TARGET_TYPE} NAMESPACE Gem @@ -122,6 +131,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest Gem::GradientSignal.Static Gem::LmbrCentral + Gem::GradientSignal.Mocks ) ly_add_googletest( NAME Gem::GradientSignal.Tests diff --git a/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h b/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h new file mode 100644 index 0000000000..69e96268f6 --- /dev/null +++ b/Gems/GradientSignal/Code/Mocks/GradientSignal/Ebuses/MockGradientRequestBus.h @@ -0,0 +1,34 @@ +/* + * 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 + +namespace UnitTest +{ + class MockGradientRequests + : private GradientSignal::GradientRequestBus::Handler + { + public: + MockGradientRequests(AZ::EntityId entityId) + { + GradientSignal::GradientRequestBus::Handler::BusConnect(entityId); + } + + ~MockGradientRequests() + { + GradientSignal::GradientRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD1(GetValue, float(const GradientSignal::GradientSampleParams&)); + MOCK_CONST_METHOD1(IsEntityInHierarchy, bool(const AZ::EntityId&)); + }; +} // namespace UnitTest + diff --git a/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake b/Gems/GradientSignal/Code/gradientsignal_mocks_files.cmake new file mode 100644 index 0000000000..82c8c3793f --- /dev/null +++ b/Gems/GradientSignal/Code/gradientsignal_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/GradientSignal/Ebuses/MockGradientRequestBus.h +) diff --git a/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h b/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h index 6129c3ec08..e31d5743c3 100644 --- a/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h +++ b/Gems/ImGui/Code/Include/LYImGuiUtils/HistogramContainer.h @@ -85,6 +85,10 @@ namespace ImGui //! Calculate the min and maximum values for the present samples. void CalcMinMaxValues(float& outMin, float& outMax); + //! Set/get color used by either the lines in case ViewType is Lines or bars in case or Histogram. + void SetBarLineColor(const ImColor& color) { m_barLineColor = color; } + ImColor GetBarLineColor() const { return m_barLineColor; } + private: // Set the Max Size and clear the container void SetMaxSize(int size); @@ -99,6 +103,7 @@ namespace ImGui bool m_dispalyOverlays; ScaleMode m_scaleMode; //! Determines if the vertical range of the histogram will be manually specified, auto-expanded or automatically scaled based on the samples. float m_autoScaleSpeed = 0.05f; //! Indicates how fast the min max values and the visible vertical range are adapting to new samples. + ImColor m_barLineColor = ImColor(66, 166, 178); //! Color used by either the lines in case ViewType is Lines or bars in case or Histogram. bool m_collapsed; bool m_drawMostRecentValueText; }; diff --git a/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp b/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp index 1915cc3721..51bcc29b99 100644 --- a/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp +++ b/Gems/ImGui/Code/Source/LYImGuiUtils/HistogramContainer.cpp @@ -147,6 +147,8 @@ namespace ImGui float imGuiHistoWidgetHeight = m_collapsed ? histogramHeight : (histogramHeight - 15); if (GetSize() > 0) { + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, m_barLineColor.Value); + switch (m_viewType) { default: @@ -160,6 +162,8 @@ namespace ImGui ImGui::PlotLines(AZStd::string::format("##%s_lines", m_histogramName.c_str()).c_str(), ImGui::LYImGuiUtils::s_histogramContainerGetter, this, GetSize(), 0, m_histogramName.c_str(), m_minScale, m_maxScale, ImVec2(histogramWidth - 10, imGuiHistoWidgetHeight)); break; } + + ImGui::PopStyleColor(); } diff --git a/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h b/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h index 094f9591d1..e180e1d536 100644 --- a/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h +++ b/Gems/LmbrCentral/Code/Source/Shape/AxisAlignedBoxShapeComponent.h @@ -15,12 +15,6 @@ namespace LmbrCentral { - /// Type ID for the AxisAlignedBoxShapeComponent - static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}"; - - /// Type ID for the EditorAxisAlignedBoxShapeComponent - static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}"; - /// Provide a Component interface for AxisAlignedBoxShape functionality. class AxisAlignedBoxShapeComponent : public AZ::Component diff --git a/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h b/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h index 488e8b5617..2e13334a76 100644 --- a/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h +++ b/Gems/LmbrCentral/Code/include/LmbrCentral/Shape/BoxShapeComponentBus.h @@ -24,6 +24,12 @@ namespace LmbrCentral /// Type ID for the BoxShapeConfig static const AZ::Uuid BoxShapeConfigTypeId = "{F034FBA2-AC2F-4E66-8152-14DFB90D6283}"; + /// Type ID for the AxisAlignedBoxShapeComponent + static const AZ::Uuid AxisAlignedBoxShapeComponentTypeId = "{641D817E-1BC6-406A-BBB2-218541808E45}"; + + /// Type ID for the EditorAxisAlignedBoxShapeComponent + static const AZ::Uuid EditorAxisAlignedBoxShapeComponentTypeId = "{8C027DF6-E157-4159-9BF8-F1B925466F1F}"; + /// Configuration data for BoxShapeComponent class BoxShapeConfig : public ShapeComponentConfig diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h index 25fe473135..678ec2d6fd 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/LocalPredictionPlayerInputComponent.h @@ -57,6 +57,14 @@ namespace Multiplayer const AzNetworking::PacketEncodingBuffer& correction ) override; + //! Forcibly enables ProcessInput to execute on the entity. + //! Note that this function is quite dangerous and should normally never be used + void ForceEnableAutonomousUpdate(); + + //! Forcibly disables ProcessInput from executing on the entity. + //! Note that this function is quite dangerous and should normally never be used + void ForceDisableAutonomousUpdate(); + //! Return true if we're currently migrating from one host to another. //! @return boolean true if we're currently migrating from one host to another bool IsMigrating() const; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h index 265137a078..577e29e34f 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetBindComponent.h @@ -32,7 +32,7 @@ namespace Multiplayer using EntityStopEvent = AZ::Event; using EntityDirtiedEvent = AZ::Event<>; using EntitySyncRewindEvent = AZ::Event<>; - using EntityServerMigrationEvent = AZ::Event; + using EntityServerMigrationEvent = AZ::Event; using EntityPreRenderEvent = AZ::Event; using EntityCorrectionEvent = AZ::Event<>; @@ -113,7 +113,7 @@ namespace Multiplayer void MarkDirty(); void NotifyLocalChanges(); void NotifySyncRewindState(); - void NotifyServerMigration(const HostId& hostId, AzNetworking::ConnectionId connectionId); + void NotifyServerMigration(const HostId& remoteHostId); void NotifyPreRender(float deltaTime); void NotifyCorrection(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h index a31cd90efd..af1878dd49 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyChildComponent.h @@ -58,11 +58,10 @@ namespace Multiplayer void BindNetworkHierarchyLeaveEventHandler(NetworkHierarchyLeaveEvent::Handler& handler) override; //! @} - protected: - //! Used by @NetworkHierarchyRootComponent - void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); - private: + //! Used by @NetworkHierarchyRootComponent + void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot); + AZ::ChildChangedEvent::Handler m_childChangedHandler; void OnChildChanged(AZ::ChildChangeType type, AZ::EntityId child); @@ -80,5 +79,8 @@ namespace Multiplayer bool m_isHierarchyEnabled = true; void NotifyChildrenHierarchyDisbanded(); + + AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId; + void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override; }; } diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h index cffcd3e97d..9b78cc43e6 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/Components/NetworkHierarchyRootComponent.h @@ -30,7 +30,6 @@ namespace Multiplayer { friend class NetworkHierarchyChildComponent; friend class NetworkHierarchyRootComponentController; - friend class ServerToClientReplicationWindow; public: AZ_MULTIPLAYER_COMPONENT(Multiplayer::NetworkHierarchyRootComponent, s_networkHierarchyRootComponentConcreteUuid, Multiplayer::NetworkHierarchyRootComponentBase); @@ -61,10 +60,9 @@ namespace Multiplayer bool SerializeEntityCorrection(AzNetworking::ISerializer& serializer); - protected: - void SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot); - private: + void SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot); + AZ::ChildChangedEvent::Handler m_childChangedHandler; AZ::ParentChangedEvent::Handler m_parentChangedHandler; @@ -81,16 +79,19 @@ namespace Multiplayer //! Rebuilds hierarchy starting from this root component's entity. void RebuildHierarchy(); - + //! @param underEntity Walk the child entities that belong to @underEntity and consider adding them to the hierarchy. //! Builds the hierarchy using breadth-first iterative method. void InternalBuildHierarchyList(AZ::Entity* underEntity); - void SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity); + void SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity); //! Set to false when deactivating or otherwise not to be included in hierarchy considerations. bool m_isHierarchyEnabled = true; + AzNetworking::ConnectionId m_previousOwningConnectionId = AzNetworking::InvalidConnectionId; + void SetOwningConnectionId(AzNetworking::ConnectionId connectionId) override; + friend class HierarchyBenchmarkBase; }; diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h index 19289e1e42..7245dbde9b 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/IMultiplayer.h @@ -45,7 +45,7 @@ namespace Multiplayer using ClientMigrationStartEvent = AZ::Event; using ClientMigrationEndEvent = AZ::Event<>; using ClientDisconnectedEvent = AZ::Event<>; - using NotifyClientMigrationEvent = AZ::Event; + using NotifyClientMigrationEvent = AZ::Event; using NotifyEntityMigrationEvent = AZ::Event; using ConnectionAcquiredEvent = AZ::Event; using ServerAcceptanceReceivedEvent = AZ::Event<>; @@ -136,10 +136,12 @@ namespace Multiplayer virtual void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) = 0; //! Signals a NotifyClientMigrationEvent with the provided parameters. - //! @param hostId the host id of the host the client is migrating to - //! @param userIdentifier the user identifier the client will provide the new host to validate identity - //! @param lastClientInputId the last processed clientInputId by the current host - virtual void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) = 0; + //! @param connectionId the connection id of the client that is migrating + //! @param hostId the host id of the host the client is migrating to + //! @param userIdentifier the user identifier the client will provide the new host to validate identity + //! @param lastClientInputId the last processed clientInputId by the current host + //! @param controlledEntityId the entityId of the clients autonomous entity + virtual void SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) = 0; //! Signals a NotifyEntityMigrationEvent with the provided parameters. //! @param entityHandle the network entity handle of the entity being migrated @@ -181,6 +183,18 @@ namespace Multiplayer //! @return pointer to the filtered entity manager, or nullptr if not set virtual IFilterEntityManager* GetFilterEntityManager() = 0; + //! Registers a temp userId to allow a host to look up a players controlled entity in the event of a rejoin or migration event. + //! @param temporaryUserIdentifier the temporary user identifier used to identify a player across hosts + //! @param controlledEntityId the controlled entityId of the players autonomous entity + virtual void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) = 0; + + //! Completes a client migration event by informing the appropriate client to migrate between hosts. + //! @param temporaryUserIdentifier the temporary user identifier used to identify a player across hosts + //! @param connectionId the connection id of the player being migrated + //! @param publicHostId the public address of the new host the client should connect to + //! @param migratedClientInputId the last clientInputId processed prior to migration + virtual void CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) = 0; + //! Enables or disables automatic instantiation of netbound entities. //! This setting is controlled by the networking layer and should not be touched //! If enabled, netbound entities will instantiate as spawnables are loaded into the game world, generally true for the server diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h index 96035083d8..58a0ae63fa 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/MultiplayerTypes.h @@ -29,7 +29,7 @@ namespace Multiplayer using HostId = AzNetworking::IpAddress; static const HostId InvalidHostId = HostId(); - AZ_TYPE_SAFE_INTEGRAL(NetEntityId, uint32_t); + AZ_TYPE_SAFE_INTEGRAL(NetEntityId, uint64_t); static constexpr NetEntityId InvalidNetEntityId = static_cast(-1); AZ_TYPE_SAFE_INTEGRAL(NetComponentId, uint16_t); @@ -68,6 +68,7 @@ namespace Multiplayer Server, // A simulated proxy on a server Authority // An authoritative proxy on a server (full authority) }; + const char* GetEnumString(NetEntityRole value); enum class ComponentSerializationType : uint8_t { @@ -113,6 +114,24 @@ namespace Multiplayer bool Serialize(AzNetworking::ISerializer& serializer); }; + inline const char* GetEnumString(NetEntityRole value) + { + switch (value) + { + case NetEntityRole::InvalidRole: + return "InvalidRole"; + case NetEntityRole::Client: + return "Client"; + case NetEntityRole::Autonomous: + return "Autonomous"; + case NetEntityRole::Server: + return "Server"; + case NetEntityRole::Authority: + return "Authority"; + } + return "Unknown"; + } + inline PrefabEntityId::PrefabEntityId(AZ::Name name, uint32_t entityOffset) : m_prefabName(name) , m_entityOffset(entityOffset) diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h index 2e1f83ae38..10346ad777 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/EntityReplication/EntityReplicationManager.h @@ -57,7 +57,6 @@ namespace Multiplayer EntityReplicationManager(AzNetworking::IConnection& connection, AzNetworking::IConnectionListener& connectionListener, Mode mode); ~EntityReplicationManager() = default; - void SetRemoteHostId(const HostId& hostId); const HostId& GetRemoteHostId() const; void ActivatePendingEntities(); diff --git a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h index 90f622a8ae..139db2a949 100644 --- a/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h +++ b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h @@ -43,8 +43,7 @@ namespace Multiplayer //! Constructor for an entity delete message. //! @param entityId the networkId of the entity being deleted //! @param isMigrated whether or not the entity is being migrated or deleted - //! @param takeOwnership true if the remote replicator should take ownership of the entity - explicit NetworkEntityUpdateMessage(NetEntityId entityId, bool isMigrated, bool takeOwnership); + explicit NetworkEntityUpdateMessage(NetEntityId entityId, bool isMigrated); NetworkEntityUpdateMessage& operator =(NetworkEntityUpdateMessage&& rhs); NetworkEntityUpdateMessage& operator =(const NetworkEntityUpdateMessage& rhs); @@ -71,10 +70,6 @@ namespace Multiplayer //! @return whether or not the entity was migrated bool GetWasMigrated() const; - //! Gets the current value of TakeOwnership. - //! @return the current value of TakeOwnership - bool GetTakeOwnership() const; - //! Gets the current value of HasValidPrefabId. //! @return the current value of HasValidPrefabId bool GetHasValidPrefabId() const; @@ -110,7 +105,6 @@ namespace Multiplayer NetEntityId m_entityId = InvalidNetEntityId; bool m_isDelete = false; bool m_wasMigrated = false; - bool m_takeOwnership = false; bool m_hasValidPrefabId = false; PrefabEntityId m_prefabEntityId; diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputArray.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputArray.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputChild.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputHistory.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputHistory.h diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.h b/Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h similarity index 100% rename from Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.h rename to Gems/Multiplayer/Code/Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h diff --git a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml index 194c05577d..31751469de 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/LocalPredictionPlayerInputComponent.AutoComponent.xml @@ -13,9 +13,9 @@ - - - + + + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml index 091043f034..a5bf169fb4 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/Multiplayer.AutoPackets.xml @@ -9,6 +9,7 @@ + diff --git a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml index fbe6ff2135..b789506d0f 100644 --- a/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml +++ b/Gems/Multiplayer/Code/Source/AutoGen/NetworkHierarchyRootComponent.AutoComponent.xml @@ -8,7 +8,7 @@ OverrideInclude="Multiplayer/Components/NetworkHierarchyRootComponent.h" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> - + diff --git a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp index 4d0756d9a5..96d6a5e31e 100644 --- a/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/LocalPredictionPlayerInputComponent.cpp @@ -354,6 +354,16 @@ namespace Multiplayer } } + void LocalPredictionPlayerInputComponentController::ForceEnableAutonomousUpdate() + { + m_autonomousUpdateEvent.Enqueue(AZ::TimeMs{ 1 }, true); + } + + void LocalPredictionPlayerInputComponentController::ForceDisableAutonomousUpdate() + { + m_autonomousUpdateEvent.RemoveFromQueue(); + } + bool LocalPredictionPlayerInputComponentController::IsMigrating() const { return m_lastMigratedInputId != ClientInputId{ 0 }; diff --git a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp index fbf83b6e6c..cc71000d33 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetBindComponent.cpp @@ -394,9 +394,9 @@ namespace Multiplayer m_syncRewindEvent.Signal(); } - void NetBindComponent::NotifyServerMigration(const HostId& hostId, AzNetworking::ConnectionId connectionId) + void NetBindComponent::NotifyServerMigration(const HostId& remoteHostId) { - m_entityServerMigrationEvent.Signal(m_netEntityHandle, hostId, connectionId); + m_entityServerMigrationEvent.Signal(m_netEntityHandle, remoteHostId); } void NetBindComponent::NotifyPreRender(float deltaTime) diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp index 1f32387893..59782b1f4d 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyChildComponent.cpp @@ -129,34 +129,48 @@ namespace Multiplayer handler.Connect(m_networkHierarchyLeaveEvent); } - void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot) + void NetworkHierarchyChildComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot) { - if (m_rootEntity != hierarchyRoot) + if (newHierarchyRoot) { - m_rootEntity = hierarchyRoot; + if (m_rootEntity != newHierarchyRoot) + { + m_rootEntity = newHierarchyRoot; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyChildComponentController* controller = static_cast(GetController()); + const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); + controller->SetHierarchyRoot(netRootId); + } + + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); + } + } + else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot) + { + m_rootEntity = nullptr; if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) { NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - if (m_rootEntity) - { - const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); - controller->SetHierarchyRoot(netRootId); - - m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); - } - else - { - controller->SetHierarchyRoot(InvalidNetEntityId); - - m_networkHierarchyLeaveEvent.Signal(); - } + controller->SetHierarchyRoot(InvalidNetEntityId); } - if (m_rootEntity == nullptr) - { - NotifyChildrenHierarchyDisbanded(); - } + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); + + NotifyChildrenHierarchyDisbanded(); + } + } + + void NetworkHierarchyChildComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) + { + NetworkHierarchyChildComponentBase::SetOwningConnectionId(connectionId); + if (IsHierarchicalChild() == false) + { + m_previousOwningConnectionId = connectionId; } } @@ -180,14 +194,18 @@ namespace Multiplayer if (m_rootEntity != newRoot) { m_rootEntity = newRoot; + + m_previousOwningConnectionId = GetNetBindComponent()->GetOwningConnectionId(); + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } } else { + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); m_isHierarchyEnabled = false; m_rootEntity = nullptr; - m_networkHierarchyLeaveEvent.Signal(); } } @@ -203,11 +221,11 @@ namespace Multiplayer { if (auto* hierarchyChildComponent = childEntity->FindComponent()) { - hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr); + hierarchyChildComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr); } else if (auto* hierarchyRootComponent = childEntity->FindComponent()) { - hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr); + hierarchyRootComponent->SetTopLevelHierarchyRootEntity(nullptr, nullptr); } } } diff --git a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp index 8d485a973e..76f4bddb1a 100644 --- a/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Components/NetworkHierarchyRootComponent.cpp @@ -107,7 +107,7 @@ namespace Multiplayer { if (const AZ::Entity* childEntity = AZ::Interface::Get()->FindEntity(childEntityId)) { - SetRootForEntity(nullptr, childEntity); + SetRootForEntity(GetEntity(), nullptr, childEntity); } } } @@ -209,7 +209,7 @@ namespace Multiplayer auto [rootComponent, childComponent] = GetHierarchyComponents(parentEntity); if (rootComponent == nullptr && childComponent == nullptr) { - RebuildHierarchy(); + SetRootForEntity(nullptr, nullptr, GetEntity()); } else { @@ -219,7 +219,7 @@ namespace Multiplayer else { // Detached from parent - RebuildHierarchy(); + SetRootForEntity(nullptr, nullptr, GetEntity()); } } @@ -247,14 +247,14 @@ namespace Multiplayer { // This is a newly added entity to the network hierarchy. hierarchyChanged = true; - SetRootForEntity(GetEntity(), currentEntity); + SetRootForEntity(nullptr, GetEntity(), currentEntity); } } // These entities were removed since last rebuild. for (const AZ::Entity* previousEntity : previousEntities) { - SetRootForEntity(nullptr, previousEntity); + SetRootForEntity(GetEntity(), nullptr, previousEntity); } if (!previousEntities.empty()) @@ -307,45 +307,66 @@ namespace Multiplayer } } - void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* root, const AZ::Entity* childEntity) + void NetworkHierarchyRootComponent::SetRootForEntity(AZ::Entity* previousKnownRoot, AZ::Entity* newRoot, const AZ::Entity* childEntity) { auto [hierarchyRootComponent, hierarchyChildComponent] = GetHierarchyComponents(childEntity); if (hierarchyChildComponent) { - hierarchyChildComponent->SetTopLevelHierarchyRootEntity(root); + hierarchyChildComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot); } else if (hierarchyRootComponent) { - hierarchyRootComponent->SetTopLevelHierarchyRootEntity(root); + hierarchyRootComponent->SetTopLevelHierarchyRootEntity(previousKnownRoot, newRoot); } } - void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* hierarchyRoot) + void NetworkHierarchyRootComponent::SetTopLevelHierarchyRootEntity(AZ::Entity* previousHierarchyRoot, AZ::Entity* newHierarchyRoot) { - m_rootEntity = hierarchyRoot; - - if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + if (newHierarchyRoot) { - NetworkHierarchyChildComponentController* controller = static_cast(GetController()); - if (hierarchyRoot) + if (m_rootEntity != newHierarchyRoot) { - const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(hierarchyRoot->GetId()); - controller->SetHierarchyRoot(netRootId); - } - else - { - controller->SetHierarchyRoot(InvalidNetEntityId); + m_rootEntity = newHierarchyRoot; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyRootComponentController* controller = static_cast(GetController()); + const NetEntityId netRootId = GetNetworkEntityManager()->GetNetEntityIdById(m_rootEntity->GetId()); + controller->SetHierarchyRoot(netRootId); + } + + GetNetBindComponent()->SetOwningConnectionId(m_rootEntity->FindComponent()->GetOwningConnectionId()); + m_networkHierarchyChangedEvent.Signal(m_rootEntity->GetId()); } } - - if (m_rootEntity == nullptr) + else if ((previousHierarchyRoot && m_rootEntity == previousHierarchyRoot) || !previousHierarchyRoot) { + m_rootEntity = nullptr; + + if (HasController() && GetNetBindComponent()->GetNetEntityRole() == NetEntityRole::Authority) + { + NetworkHierarchyRootComponentController* controller = static_cast(GetController()); + controller->SetHierarchyRoot(InvalidNetEntityId); + } + + GetNetBindComponent()->SetOwningConnectionId(m_previousOwningConnectionId); + m_networkHierarchyLeaveEvent.Signal(); + // We lost the parent hierarchical entity, so as a root we need to re-build our own hierarchy. RebuildHierarchy(); } } + void NetworkHierarchyRootComponent::SetOwningConnectionId(AzNetworking::ConnectionId connectionId) + { + NetworkHierarchyRootComponentBase::SetOwningConnectionId(connectionId); + if (IsHierarchicalChild() == false) + { + m_previousOwningConnectionId = connectionId; + } + } + NetworkHierarchyRootComponentController::NetworkHierarchyRootComponentController(NetworkHierarchyRootComponent& parent) : NetworkHierarchyRootComponentControllerBase(parent) { @@ -370,7 +391,7 @@ namespace Multiplayer void NetworkHierarchyRootComponentController::CreateInput(Multiplayer::NetworkInput& input, float deltaTime) { NetworkHierarchyRootComponent& component = GetParent(); - if(!component.IsHierarchicalRoot()) + if (!component.IsHierarchicalRoot()) { return; } @@ -386,7 +407,7 @@ namespace Multiplayer for (AZ::Entity* child : entities) { - if(child == component.GetEntity()) + if (child == component.GetEntity()) { continue; // Avoid infinite recursion } diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp index a9b8e03126..56eed58af7 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.cpp @@ -23,22 +23,16 @@ namespace Multiplayer ServerToClientConnectionData::ServerToClientConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener, - NetworkEntityHandle controlledEntity + AzNetworking::IConnectionListener& connectionListener ) : m_connection(connection) , m_controlledEntityRemovedHandler([this](const ConstNetworkEntityHandle&) { OnControlledEntityRemove(); }) - , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, AzNetworking::ConnectionId connectionId) { OnControlledEntityMigration(entityHandle, remoteHostId, connectionId); }) - , m_controlledEntity(controlledEntity) + , m_controlledEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) + { + OnControlledEntityMigration(entityHandle, remoteHostId); + }) , m_entityReplicationManager(*connection, connectionListener, EntityReplicationManager::Mode::LocalServerToRemoteClient) { - NetBindComponent* netBindComponent = m_controlledEntity.GetNetBindComponent(); - if (netBindComponent != nullptr) - { - netBindComponent->AddEntityStopEventHandler(m_controlledEntityRemovedHandler); - netBindComponent->AddEntityServerMigrationEventHandler(m_controlledEntityMigrationHandler); - } - m_entityReplicationManager.SetMaxRemoteEntitiesPendingCreationCount(sv_ClientMaxRemoteEntitiesPendingCreationCount); m_entityReplicationManager.SetEntityPendingRemovalMs(sv_ClientEntityReplicatorPendingRemovalTimeMs); } @@ -54,6 +48,20 @@ namespace Multiplayer m_controlledEntityRemovedHandler.Disconnect(); } + void ServerToClientConnectionData::SetControlledEntity(NetworkEntityHandle primaryPlayerEntity) + { + m_controlledEntityRemovedHandler.Disconnect(); + m_controlledEntityMigrationHandler.Disconnect(); + + m_controlledEntity = primaryPlayerEntity; + NetBindComponent* netBindComponent = m_controlledEntity.GetNetBindComponent(); + if (netBindComponent != nullptr) + { + netBindComponent->AddEntityStopEventHandler(m_controlledEntityRemovedHandler); + netBindComponent->AddEntityServerMigrationEventHandler(m_controlledEntityMigrationHandler); + } + } + ConnectionDataType ServerToClientConnectionData::GetConnectionDataType() const { return ConnectionDataType::ServerToClient; @@ -94,8 +102,7 @@ namespace Multiplayer void ServerToClientConnectionData::OnControlledEntityMigration ( [[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, - [[maybe_unused]] const HostId& remoteHostId, - [[maybe_unused]] AzNetworking::ConnectionId connectionId + const HostId& remoteHostId ) { ClientInputId migratedClientInputId = ClientInputId{ 0 }; @@ -109,14 +116,12 @@ namespace Multiplayer } // Generate crypto-rand user identifier, send to both server and client so they can negotiate the autonomous entity to assume predictive control over after migration - const uint64_t randomUserIdentifier = AzNetworking::CryptoRand64(); + const uint64_t temporaryUserIdentifier = AzNetworking::CryptoRand64(); // Tell the new host that a client is about to (re)join - GetMultiplayer()->SendNotifyClientMigrationEvent(remoteHostId, randomUserIdentifier, migratedClientInputId); - - // Tell the client who to join - MultiplayerPackets::ClientMigration clientMigration(remoteHostId, randomUserIdentifier, migratedClientInputId); - GetConnection()->SendReliablePacket(clientMigration); + GetMultiplayer()->SendNotifyClientMigrationEvent(GetConnection()->GetConnectionId(), remoteHostId, temporaryUserIdentifier, migratedClientInputId, m_controlledEntity.GetNetEntityId()); + // We need to send a MultiplayerPackets::ClientMigration packet to complete this process + // This happens inside MultiplayerSystemComponent, once we're certain the remote host has appropriately prepared m_controlledEntity = NetworkEntityHandle(); m_canSendUpdates = false; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h index 1b4ee2cc04..4bd30a56a9 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.h @@ -20,11 +20,12 @@ namespace Multiplayer ServerToClientConnectionData ( AzNetworking::IConnection* connection, - AzNetworking::IConnectionListener& connectionListener, - NetworkEntityHandle controlledEntity + AzNetworking::IConnectionListener& connectionListener ); ~ServerToClientConnectionData() override; + void SetControlledEntity(NetworkEntityHandle primaryPlayerEntity); + //! IConnectionData interface //! @{ ConnectionDataType GetConnectionDataType() const override; @@ -44,7 +45,7 @@ namespace Multiplayer private: void OnControlledEntityRemove(); - void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId, AzNetworking::ConnectionId connectionId); + void OnControlledEntityMigration(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId); void OnGameplayStarted(); EntityReplicationManager m_entityReplicationManager; diff --git a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl index e4348fe539..c166a2a961 100644 --- a/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl +++ b/Gems/Multiplayer/Code/Source/ConnectionData/ServerToClientConnectionData.inl @@ -18,7 +18,6 @@ namespace Multiplayer m_canSendUpdates = canSendUpdates; } - inline NetworkEntityHandle ServerToClientConnectionData::GetPrimaryPlayerEntity() { return m_controlledEntity; diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp index 7c38a340eb..ad28307204 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugHierarchyReporter.cpp @@ -74,7 +74,7 @@ namespace Multiplayer { ImGui::Text("%s", entity->GetId().ToString().c_str()); ImGui::NextColumn(); - ImGui::Text("%u", GetMultiplayer()->GetNetworkEntityManager()->GetNetEntityIdById(entity->GetId())); + ImGui::Text("%llu", static_cast(GetMultiplayer()->GetNetworkEntityManager()->GetNetEntityIdById(entity->GetId()))); ImGui::NextColumn(); ImGui::Text("%s", entity->GetName().c_str()); ImGui::NextColumn(); diff --git a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp index b23bc677f0..4ff78d3815 100644 --- a/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/Debug/MultiplayerDebugSystemComponent.cpp @@ -28,24 +28,29 @@ namespace Multiplayer ->Version(1); } } + void MultiplayerDebugSystemComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) { provided.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent")); } + void MultiplayerDebugSystemComponent::GetRequiredServices([[maybe_unused]] AZ::ComponentDescriptor::DependencyArrayType& required) { ; } + void MultiplayerDebugSystemComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatbile) { incompatbile.push_back(AZ_CRC_CE("MultiplayerDebugSystemComponent")); } + void MultiplayerDebugSystemComponent::Activate() { #ifdef IMGUI_ENABLED ImGui::ImGuiUpdateListenerBus::Handler::BusConnect(); #endif } + void MultiplayerDebugSystemComponent::Deactivate() { #ifdef IMGUI_ENABLED @@ -75,6 +80,7 @@ namespace Multiplayer ImGui::EndMenu(); } } + void AccumulatePerSecondValues(const MultiplayerStats& stats, const MultiplayerStats::Metric& metric, float& outCallsPerSecond, float& outBytesPerSecond) { uint64_t summedCalls = 0; @@ -107,6 +113,7 @@ namespace Multiplayer ImGui::Text("%11.2f", bytesPerSecond); return open; } + bool DrawSummaryRow(const char* name, const MultiplayerStats& stats) { const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateTotalPropertyUpdateSentMetrics(); @@ -123,6 +130,7 @@ namespace Multiplayer AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond); return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond); } + bool DrawComponentRow(const char* name, const MultiplayerStats& stats, NetComponentId netComponentId) { const MultiplayerStats::Metric propertyUpdatesSent = stats.CalculateComponentPropertyUpdateSentMetrics(netComponentId); @@ -139,6 +147,7 @@ namespace Multiplayer AccumulatePerSecondValues(stats, rpcsRecv, callsPerSecond, bytesPerSecond); return DrawMetricsRow(name, true, totalCalls, totalBytes, callsPerSecond, bytesPerSecond); } + void DrawComponentDetails(const MultiplayerStats& stats, NetComponentId netComponentId) { MultiplayerComponentRegistry* componentRegistry = GetMultiplayerComponentRegistry(); @@ -503,4 +512,3 @@ void OnDebugEntities_ShowBandwidth_Changed(const bool& showBandwidth) AZ::Interface::Get()->HideEntityBandwidthDebugOverlay(); } } - diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp index 42a7fe43e5..fdd7602f71 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.cpp @@ -8,6 +8,7 @@ #include #include +#include #include #include #include @@ -76,6 +77,7 @@ namespace Multiplayer "The address of the remote server or host to connect to"); AZ_CVAR(uint16_t, cl_serverport, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port of the remote host to connect to for game traffic"); AZ_CVAR(uint16_t, sv_port, DefaultServerPort, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The port that this multiplayer gem will bind to for game traffic"); + AZ_CVAR(uint16_t, sv_portRange, 999, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The range of ports the host will incrementally attempt to bind to when initializing"); AZ_CVAR(AZ::CVarFixedString, sv_map, "nolevel", nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "The map the server should load"); AZ_CVAR(ProtocolType, sv_protocol, ProtocolType::Udp, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "This flag controls whether we use TCP or UDP for game networking"); AZ_CVAR(bool, sv_isDedicated, true, nullptr, AZ::ConsoleFunctorFlags::DontReplicate, "Whether the host command creates an independent or client hosted server"); @@ -168,6 +170,7 @@ namespace Multiplayer AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom ) { OnConsoleCommandInvoked(command, args, flags, invokedFrom); }) + , m_autonomousEntityReplicatorCreatedHandler([this]([[maybe_unused]] NetEntityId netEntityId) { OnAutonomousEntityReplicatorCreated(); }) { AZ::Interface::Register(this); } @@ -205,8 +208,23 @@ namespace Multiplayer bool MultiplayerSystemComponent::StartHosting(uint16_t port, bool isDedicated) { - InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); - return m_networkInterface->Listen(port); + if (port != sv_port) + { + sv_port = port; + } + + const uint16_t maxPort = sv_port + sv_portRange; + while (sv_port <= maxPort) + { + if (m_networkInterface->Listen(sv_port)) + { + InitializeMultiplayer(isDedicated ? MultiplayerAgentType::DedicatedServer : MultiplayerAgentType::ClientServer); + return true; + } + AZLOG_WARN("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); + sv_port = sv_port + 1; + } + return false; } bool MultiplayerSystemComponent::Connect(const AZStd::string& remoteAddress, uint16_t port) @@ -328,6 +346,11 @@ namespace Multiplayer void MultiplayerSystemComponent::OnTick(float deltaTime, [[maybe_unused]] AZ::ScriptTimePoint time) { + if (bg_multiplayerDebugDraw) + { + m_networkEntityManager.DebugDraw(); + } + const AZ::TimeMs deltaTimeMs = aznumeric_cast(static_cast(deltaTime * 1000.0f)); const AZ::TimeMs serverRateMs = static_cast(sv_serverSendRateMs); const float serverRateSeconds = static_cast(serverRateMs) / 1000.0f; @@ -412,11 +435,6 @@ namespace Multiplayer { m_networkInterface->GetConnectionSet().VisitConnections(visitor); } - - if (bg_multiplayerDebugDraw) - { - m_networkEntityManager.DebugDraw(); - } } int MultiplayerSystemComponent::GetTickOrder() @@ -487,17 +505,39 @@ namespace Multiplayer auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::TerminatedByUser, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); return true; - } + } } reinterpret_cast(connection->GetUserData())->SetProviderTicket(packet.GetTicket().c_str()); + // Hosts will spawn a new default player prefab for the user that just connected + if (GetAgentType() == MultiplayerAgentType::ClientServer + || GetAgentType() == MultiplayerAgentType::DedicatedServer) + { + // We use a temporary userId over the clients address so we can maintain client lookups even in the event of wifi handoff + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(packet.GetTemporaryUserId()); + EnableAutonomousControl(controlledEntity, connection->GetConnectionId()); + + ServerToClientConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); + AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); + connectionData->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + connectionData->SetControlledEntity(controlledEntity); + + // If this is a migrate or rejoin, immediately ready the connection for updates + if (packet.GetTemporaryUserId() != 0) + { + connectionData->SetCanSendUpdates(true); + } + } + if (connection->SendReliablePacket(MultiplayerPackets::Accept(sv_map))) { reinterpret_cast(connection->GetUserData())->SetDidHandshake(true); - - // Sync our console - ConsoleReplicator consoleReplicator(connection); - AZ::Interface::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); }); + if (packet.GetTemporaryUserId() == 0) + { + // Sync our console + ConsoleReplicator consoleReplicator(connection); + AZ::Interface::Get()->VisitRegisteredFunctors([&consoleReplicator](AZ::ConsoleFunctorBase* functor) { consoleReplicator.Visit(functor); }); + } return true; } return false; @@ -511,10 +551,26 @@ namespace Multiplayer ) { reinterpret_cast(connection->GetUserData())->SetDidHandshake(true); - AZ::CVarFixedString commandString = "sv_map " + packet.GetMap(); - AZ::Interface::Get()->PerformCommand(commandString.c_str()); - AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap(); - AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + if (m_temporaryUserIdentifier == 0) + { + AZ::CVarFixedString commandString = "sv_map " + packet.GetMap(); + AZ::Interface::Get()->PerformCommand(commandString.c_str()); + AZ::CVarFixedString loadLevelString = "LoadLevel " + packet.GetMap(); + AZ::Interface::Get()->PerformCommand(loadLevelString.c_str()); + } + else + { + // Bypass map loading and immediately ready the connection for updates + IConnectionData* connectionData = reinterpret_cast(connection->GetUserData()); + if (connectionData) + { + connectionData->SetCanSendUpdates(true); + + // @nt: TODO - delete once dropped RPC problem fixed + // Connection has migrated, we are now waiting for the autonomous entity replicator to be created + connectionData->GetReplicationManager().AddAutonomousEntityReplicatorCreatedHandler(m_autonomousEntityReplicatorCreatedHandler); + } + } m_serverAcceptanceReceivedEvent.Signal(); return true; @@ -637,13 +693,17 @@ namespace Multiplayer // Store the temporary user identifier so we can transmit it with our next Connect packet // The new server will use this to re-attach our set of autonomous entities + m_temporaryUserIdentifier = packet.GetTemporaryUserIdentifier(); // Disconnect our existing server connection auto visitor = [](IConnection& connection) { connection.Disconnect(DisconnectReason::ClientMigrated, TerminationEndpoint::Local); }; m_networkInterface->GetConnectionSet().VisitConnections(visitor); AZLOG_INFO("Migrating to new server shard"); m_clientMigrationStartEvent.Signal(packet.GetLastClientInputId()); - m_networkInterface->Connect(packet.GetRemoteServerAddress()); + if (m_networkInterface->Connect(packet.GetRemoteServerAddress()) == AzNetworking::InvalidConnectionId) + { + AZLOG_ERROR("Failed to connect to new host during client migration event"); + } return true; } @@ -673,7 +733,7 @@ namespace Multiplayer providerTicket = m_pendingConnectionTickets.front(); m_pendingConnectionTickets.pop(); } - connection->SendReliablePacket(MultiplayerPackets::Connect(0, providerTicket.c_str())); + connection->SendReliablePacket(MultiplayerPackets::Connect(0, m_temporaryUserIdentifier, providerTicket.c_str())); } else { @@ -681,29 +741,10 @@ namespace Multiplayer m_connectionAcquiredEvent.Signal(datum); } - // Hosts will spawn a new default player prefab for the user that just connected if (GetAgentType() == MultiplayerAgentType::ClientServer || GetAgentType() == MultiplayerAgentType::DedicatedServer) { - INetworkEntityManager::EntityList entityList = SpawnDefaultPlayerPrefab(); - for (auto& netEntity : entityList) - { - if (netEntity.Exists()) - { - netEntity.GetNetBindComponent()->SetOwningConnectionId(connection->GetConnectionId()); - } - netEntity.Activate(); - } - - NetworkEntityHandle controlledEntity; - if (entityList.size() > 0) - { - controlledEntity = entityList[0]; - } - - connection->SetUserData(new ServerToClientConnectionData(connection, *this, controlledEntity)); - AZStd::unique_ptr window = AZStd::make_unique(controlledEntity, connection); - reinterpret_cast(connection->GetUserData())->GetReplicationManager().SetReplicationWindow(AZStd::move(window)); + connection->SetUserData(new ServerToClientConnectionData(connection, *this)); } else { @@ -725,9 +766,9 @@ namespace Multiplayer void MultiplayerSystemComponent::OnDisconnect(AzNetworking::IConnection* connection, DisconnectReason reason, TerminationEndpoint endpoint) { - const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remote host disconnected"; + const char* endpointString = (endpoint == TerminationEndpoint::Local) ? "Disconnecting" : "Remotely disconnected"; AZStd::string reasonString = ToString(reason); - AZLOG_INFO("%s due to %s from remote address: %s", endpointString, reasonString.c_str(), connection->GetRemoteAddress().GetString().c_str()); + AZLOG_INFO("%s from remote address %s due to %s", endpointString, connection->GetRemoteAddress().GetString().c_str(), reasonString.c_str()); // The client is disconnecting if (GetAgentType() == MultiplayerAgentType::Client) @@ -809,16 +850,8 @@ namespace Multiplayer // Spawn the default player for this host since the host is also a player (not a dedicated server) if (m_agentType == MultiplayerAgentType::ClientServer) { - INetworkEntityManager::EntityList entityList = SpawnDefaultPlayerPrefab(); - - for (NetworkEntityHandle controlledEntity : entityList) - { - if (NetBindComponent* controlledEntityNetBindComponent = controlledEntity.GetNetBindComponent()) - { - controlledEntityNetBindComponent->SetAllowAutonomy(true); - } - controlledEntity.Activate(); - } + NetworkEntityHandle controlledEntity = SpawnDefaultPlayerPrefab(0); + EnableAutonomousControl(controlledEntity, AzNetworking::InvalidConnectionId); } AZLOG_INFO("Multiplayer operating in %s mode", GetEnumString(m_agentType)); @@ -869,9 +902,9 @@ namespace Multiplayer handler.Connect(m_shutdownEvent); } - void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) + void MultiplayerSystemComponent::SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) { - m_notifyClientMigrationEvent.Signal(hostId, userIdentifier, lastClientInputId); + m_notifyClientMigrationEvent.Signal(connectionId, hostId, userIdentifier, lastClientInputId, controlledEntityId); } void MultiplayerSystemComponent::SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) @@ -925,6 +958,22 @@ namespace Multiplayer return m_filterEntityManager; } + void MultiplayerSystemComponent::RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) + { + m_playerRejoinData[temporaryUserIdentifier] = controlledEntityId; + } + + void MultiplayerSystemComponent::CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) + { + IConnection* connection = m_networkInterface->GetConnectionSet().GetConnection(connectionId); + if (connection != nullptr) // Make sure the player has not disconnected since the start of migration + { + // Tell the client who to join + MultiplayerPackets::ClientMigration clientMigration(publicHostId, temporaryUserIdentifier, migratedClientInputId); + connection->SendReliablePacket(clientMigration); + } + } + void MultiplayerSystemComponent::SetShouldSpawnNetworkEntities(bool value) { m_spawnNetboundEntities = value; @@ -1055,6 +1104,13 @@ namespace Multiplayer m_cvarCommands.PushBackItem(AZStd::move(replicateString)); } + void MultiplayerSystemComponent::OnAutonomousEntityReplicatorCreated() + { + m_autonomousEntityReplicatorCreatedHandler.Disconnect(); + //m_networkEntityManager.GetNetworkEntityAuthorityTracker()->ResetTimeoutTime(AZ::TimeMs{ 2000 }); + m_clientMigrationEndEvent.Signal(); + } + void MultiplayerSystemComponent::ExecuteConsoleCommandList(IConnection* connection, const AZStd::fixed_vector& commands) { AZ::IConsole* console = AZ::Interface::Get(); @@ -1066,19 +1122,69 @@ namespace Multiplayer } } - INetworkEntityManager::EntityList MultiplayerSystemComponent::SpawnDefaultPlayerPrefab() + NetworkEntityHandle MultiplayerSystemComponent::SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier) { + const auto node = m_playerRejoinData.find(temporaryUserIdentifier); + if (node != m_playerRejoinData.end()) + { + return m_networkEntityManager.GetNetworkEntityTracker()->Get(node->second); + } + PrefabEntityId playerPrefabEntityId(AZ::Name(static_cast(sv_defaultPlayerSpawnAsset).c_str())); INetworkEntityManager::EntityList entityList = m_networkEntityManager.CreateEntitiesImmediate(playerPrefabEntityId, NetEntityRole::Authority, AZ::Transform::CreateIdentity(), Multiplayer::AutoActivate::DoNotActivate); - return entityList; + for (NetworkEntityHandle subEntity : entityList) + { + subEntity.Activate(); + } + + NetworkEntityHandle controlledEntity; + if (!entityList.empty()) + { + controlledEntity = entityList[0]; + } + return controlledEntity; + } + + void MultiplayerSystemComponent::EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId) + { + if (!entityHandle.Exists()) + { + AZLOG_WARN("Attempting to enable autonomous control for an invalid entity"); + return; + } + + entityHandle.GetNetBindComponent()->SetOwningConnectionId(connectionId); + if (connectionId == InvalidConnectionId) + { + entityHandle.GetNetBindComponent()->SetAllowAutonomy(true); + } + + auto* hierarchyComponent = entityHandle.FindComponent(); + if (hierarchyComponent != nullptr) + { + for (AZ::Entity* subEntity : hierarchyComponent->GetHierarchicalEntities()) + { + NetworkEntityHandle subEntityHandle = NetworkEntityHandle(subEntity); + NetBindComponent* subEntityNetBindComponent = subEntityHandle.GetNetBindComponent(); + + if (subEntityNetBindComponent != nullptr) + { + subEntityNetBindComponent->SetOwningConnectionId(connectionId); + if (connectionId == InvalidConnectionId) + { + subEntityNetBindComponent->SetAllowAutonomy(true); + } + } + } + } } void host([[maybe_unused]] const AZ::ConsoleCommandContainer& arguments) { if (!AZ::Interface::Get()->StartHosting(sv_port, sv_isDedicated)) { - AZLOG_ERROR("Failed to start listening on port %u, port is in use?", static_cast(sv_port)); + AZLOG_ERROR("Failed to start listening on any allocated port"); } } AZ_CONSOLEFREEFUNC(host, AZ::ConsoleFunctorFlags::DontReplicate, "Opens a multiplayer connection as a host for other clients to connect to"); diff --git a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h index 9ab78e0f0b..87d084d5bc 100644 --- a/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h +++ b/Gems/Multiplayer/Code/Source/MultiplayerSystemComponent.h @@ -123,7 +123,7 @@ namespace Multiplayer void AddSessionInitHandler(SessionInitEvent::Handler& handler) override; void AddSessionShutdownHandler(SessionShutdownEvent::Handler& handler) override; void AddServerAcceptanceReceivedHandler(ServerAcceptanceReceivedEvent::Handler& handler) override; - void SendNotifyClientMigrationEvent(const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId) override; + void SendNotifyClientMigrationEvent(AzNetworking::ConnectionId connectionId, const HostId& hostId, uint64_t userIdentifier, ClientInputId lastClientInputId, NetEntityId controlledEntityId) override; void SendNotifyEntityMigrationEvent(const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) override; void SendReadyForEntityUpdates(bool readyForEntityUpdates) override; AZ::TimeMs GetCurrentHostTimeMs() const override; @@ -132,6 +132,8 @@ namespace Multiplayer INetworkEntityManager* GetNetworkEntityManager() override; void SetFilterEntityManager(IFilterEntityManager* entityFilter) override; IFilterEntityManager* GetFilterEntityManager() override; + void RegisterPlayerIdentifierForRejoin(uint64_t temporaryUserIdentifier, NetEntityId controlledEntityId) override; + void CompleteClientMigration(uint64_t temporaryUserIdentifier, AzNetworking::ConnectionId connectionId, const HostId& publicHostId, ClientInputId migratedClientInputId) override; void SetShouldSpawnNetworkEntities(bool value) override; bool GetShouldSpawnNetworkEntities() const override; //! @} @@ -145,9 +147,11 @@ namespace Multiplayer void TickVisibleNetworkEntities(float deltaTime, float serverRateSeconds); void OnConsoleCommandInvoked(AZStd::string_view command, const AZ::ConsoleCommandContainer& args, AZ::ConsoleFunctorFlags flags, AZ::ConsoleInvokedFrom invokedFrom); + void OnAutonomousEntityReplicatorCreated(); void ExecuteConsoleCommandList(AzNetworking::IConnection* connection, const AZStd::fixed_vector& commands); - INetworkEntityManager::EntityList SpawnDefaultPlayerPrefab(); - + NetworkEntityHandle SpawnDefaultPlayerPrefab(uint64_t temporaryUserIdentifier); + void EnableAutonomousControl(NetworkEntityHandle entityHandle, AzNetworking::ConnectionId connectionId); + AZ_CONSOLEFUNC(MultiplayerSystemComponent, DumpStats, AZ::ConsoleFunctorFlags::Null, "Dumps stats for the current multiplayer session"); AzNetworking::INetworkInterface* m_networkInterface = nullptr; @@ -170,12 +174,16 @@ namespace Multiplayer ClientMigrationEndEvent m_clientMigrationEndEvent; NotifyClientMigrationEvent m_notifyClientMigrationEvent; NotifyEntityMigrationEvent m_notifyEntityMigrationEvent; + AZ::Event::Handler m_autonomousEntityReplicatorCreatedHandler; AZStd::queue m_pendingConnectionTickets; + AZStd::unordered_map m_playerRejoinData; AZ::TimeMs m_lastReplicatedHostTimeMs = AZ::TimeMs{ 0 }; HostFrameId m_lastReplicatedHostFrameId = HostFrameId(0); + uint64_t m_temporaryUserIdentifier = 0; // Used in the event of a migration or rejoin + double m_serverSendAccumulator = 0.0; float m_renderBlendFactor = 0.0f; float m_tickFactor = 0.0f; diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp index 6f65d01e50..fa099842c5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicationManager.cpp @@ -47,6 +47,9 @@ namespace Multiplayer , m_entityExitDomainEventHandler([this](const ConstNetworkEntityHandle& entityHandle) { OnEntityExitDomain(entityHandle); }) , m_notifyEntityMigrationHandler([this](const ConstNetworkEntityHandle& entityHandle, const HostId& remoteHostId) { OnPostEntityMigration(entityHandle, remoteHostId); }) { + // Set up our remote host identifier, by default we use the IP address of the remote host + m_remoteHostId = connection.GetRemoteAddress(); + // Our max payload size is whatever is passed in, minus room for a udp packetheader m_maxPayloadSize = connection.GetConnectionMtu() - UdpPacketHeaderSerializeSize - ReplicationManagerPacketOverhead; @@ -62,12 +65,10 @@ namespace Multiplayer networkEntityManager->AddEntityExitDomainHandler(m_entityExitDomainEventHandler); } - GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); - } - - void EntityReplicationManager::SetRemoteHostId(const HostId& hostId) - { - m_remoteHostId = hostId; + if (m_updateMode == Mode::LocalServerToRemoteServer) + { + GetMultiplayer()->AddNotifyEntityMigrationEventHandler(m_notifyEntityMigrationHandler); + } } const HostId& EntityReplicationManager::GetRemoteHostId() const @@ -258,8 +259,8 @@ namespace Multiplayer { AZLOG_WARN ( - "Serializing extremely large entity (%u) - MaxPayload: %d NeededSize %d", - aznumeric_cast(replicator->GetEntityHandle().GetNetEntityId()), + "Serializing extremely large entity (%llu) - MaxPayload: %d NeededSize %d", + aznumeric_cast(replicator->GetEntityHandle().GetNetEntityId()), m_maxPayloadSize, nextMessageSize ); @@ -364,15 +365,29 @@ namespace Multiplayer const bool changedRemoteRole = (remoteNetworkRole != entityReplicator->GetRemoteNetworkRole()); // Check if we've changed our bound local role - this can occur when we gain Autonomous or lose Autonomous on a client bool changedLocalRole(false); - if (AZ::Entity* localEnt = entityReplicator->GetEntityHandle().GetEntity()) + NetBindComponent* netBindComponent = entityReplicator->GetEntityHandle().GetNetBindComponent(); + if (netBindComponent != nullptr) { - NetBindComponent* netBindComponent = entityReplicator->GetEntityHandle().GetNetBindComponent(); - AZ_Assert(netBindComponent != nullptr, "No NetBindComponent"); changedLocalRole = (netBindComponent->GetNetEntityRole() != entityReplicator->GetBoundLocalNetworkRole()); } if (changedRemoteRole || changedLocalRole) { + const AZ::u64 intEntityId = static_cast(netBindComponent->GetNetEntityId()); + const char* entityName = entityReplicator->GetEntityHandle().GetEntity()->GetName().c_str(); + if (changedLocalRole) + { + const char* oldRoleString = GetEnumString(entityReplicator->GetRemoteNetworkRole()); + const char* newRoleString = GetEnumString(remoteNetworkRole); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%llu) changed local role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); + } + if (changedRemoteRole) + { + const char* oldRoleString = GetEnumString(entityReplicator->GetBoundLocalNetworkRole()); + const char* newRoleString = GetEnumString(netBindComponent->GetNetEntityRole()); + AZLOG(NET_ReplicatorRoles, "Replicator %s(%llu) changed remote role, old role = %s, new role = %s", entityName, intEntityId, oldRoleString, newRoleString); + } + // If we changed roles, we need to reset everything if (!entityReplicator->IsMarkedForRemoval()) { @@ -387,8 +402,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Reinited replicator for %u from remote host %s role %d", - entityHandle.GetNetEntityId(), + "Reinited replicator for netEntityId %llu from remote host %s role %d", + static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str(), aznumeric_cast(remoteNetworkRole) ); @@ -404,8 +419,8 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Added replicator for %u from remote host %s role %d", - entityHandle.GetNetEntityId(), + "Added replicator for netEntityId %llu from remote host %s role %d", + static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str(), aznumeric_cast(remoteNetworkRole) ); @@ -413,7 +428,7 @@ namespace Multiplayer } else { - AZLOG_ERROR("Failed to add entity replicator, entity does not exist, entity id %u", entityHandle.GetNetEntityId()); + AZLOG_ERROR("Failed to add entity replicator, entity does not exist, netEntityId %llu", static_cast(entityHandle.GetNetEntityId())); AZ_Assert(false, "Failed to add entity replicator, entity does not exist"); } return entityReplicator; @@ -502,24 +517,20 @@ namespace Multiplayer { if (entityReplicator->IsMarkedForRemoval()) { - AZLOG(NET_RepDeletes, "Got a replicator delete message that is a duplicate id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Got a replicator delete message that is a duplicate id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } else if (entityReplicator->OwnsReplicatorLifetime()) { // This can occur if we migrate entities quickly - if this is a replicator from C to A, A migrates to B, B then migrates to C, and A's delete replicator has not arrived at C - AZLOG(NET_RepDeletes, "Got a replicator delete message for a replicator we own id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Got a replicator delete message for a replicator we own id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } else { shouldDeleteEntity = true; entityReplicator->MarkForRemoval(); - AZLOG(NET_RepDeletes, "Deleting replicater for entity id %u remote host %s", updateMessage.GetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Deleting replicater for entity id %llu remote host %s", static_cast(updateMessage.GetEntityId()), GetRemoteHostId().GetString().c_str()); } } - else - { - shouldDeleteEntity = updateMessage.GetTakeOwnership(); - } // Handle entity cleanup if (shouldDeleteEntity) @@ -529,17 +540,17 @@ namespace Multiplayer { if (updateMessage.GetWasMigrated()) { - AZLOG(NET_RepDeletes, "Leaving id %u using timeout remote host %s", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Leaving id %llu using timeout remote host %s", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); } else { - AZLOG(NET_RepDeletes, "Deleting entity id %u remote host %s", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Deleting entity id %llu remote host %s", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); GetNetworkEntityManager()->MarkForRemoval(entity); } } else { - AZLOG(NET_RepDeletes, "Trying to delete entity id %u remote host %s, but it has been removed", entity.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Trying to delete entity id %llu remote host %s, but it has been removed", static_cast(entity.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); } } @@ -583,9 +594,9 @@ namespace Multiplayer NetBindComponent* netBindComponent = replicatorEntity.GetNetBindComponent(); AZ_Assert(netBindComponent != nullptr, "No NetBindComponent"); - if (createEntity) + if (netBindComponent->GetOwningConnectionId() != invokingConnection->GetConnectionId()) { - // Always set our invoking connectionId for any newly created entities, since this connection now 'owns' them from a rewind perspective + // Always ensure our owning connectionId is correct for correct rewind behaviour netBindComponent->SetOwningConnectionId(invokingConnection->GetConnectionId()); } @@ -595,10 +606,11 @@ namespace Multiplayer AZ_Assert(localNetworkRole != NetEntityRole::Authority, "UpdateMessage trying to set local role to Authority, this should only happen via migration"); AZLOG_INFO ( - "EntityReplicationManager: Changing network role on entity %u, old role %u new role %u", - aznumeric_cast(netEntityId), - aznumeric_cast(netBindComponent->GetNetEntityRole()), - aznumeric_cast(localNetworkRole) + "EntityReplicationManager: Changing network role on entity %s(%llu), old role %s new role %s", + replicatorEntity.GetEntity()->GetName().c_str(), + aznumeric_cast(netEntityId), + GetEnumString(netBindComponent->GetNetEntityRole()), + GetEnumString(localNetworkRole) ); if (NetworkRoleHasController(localNetworkRole)) @@ -708,9 +720,9 @@ namespace Multiplayer AZLOG_WARN ( "Dropping Packet and LocalServerToRemoteClient connection, unexpected packet " - "LocalShard=%s EntityId=%u RemoteNetworkRole=%u BoundLocalNetworkRole=%u ActualNetworkRole=%u IsMarkedForRemoval=%s", + "LocalShard=%s EntityId=%llu RemoteNetworkRole=%u BoundLocalNetworkRole=%u ActualNetworkRole=%u IsMarkedForRemoval=%s", GetNetworkEntityManager()->GetHostId().GetString().c_str(), - aznumeric_cast(entityReplicator->GetEntityHandle().GetNetEntityId()), + aznumeric_cast(entityReplicator->GetEntityHandle().GetNetEntityId()), aznumeric_cast(entityReplicator->GetRemoteNetworkRole()), aznumeric_cast(entityReplicator->GetBoundLocalNetworkRole()), aznumeric_cast(entityReplicator->GetNetBindComponent()->GetNetEntityRole()), @@ -760,13 +772,13 @@ namespace Multiplayer result = UpdateValidationResult::DropMessage; if (updateMessage.GetIsDelete()) { - AZLOG(NET_RepDeletes, "EntityReplicationManager: Received old DeleteProxy message for entity id %u, sequence %d latest sequence %d from remote host %s", - updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "EntityReplicationManager: Received old DeleteProxy message for entity id %llu, sequence %d latest sequence %d from remote host %s", + (AZ::u64)updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); } else { - AZLOG(NET_RepUpdate, "EntityReplicationManager: Received old PropertyChangeMessage message for entity id %u, sequence %d latest sequence %d from remote host %s", - updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepUpdate, "EntityReplicationManager: Received old PropertyChangeMessage message for entity id %llu, sequence %d latest sequence %d from remote host %s", + (AZ::u64)updateMessage.GetEntityId(), (uint32_t)packetId, (uint32_t)propSubscriber->GetLastReceivedPacketId(), GetRemoteHostId().GetString().c_str()); } } } @@ -853,10 +865,10 @@ namespace Multiplayer { AZLOG_INFO ( - "EntityReplicationManager: Dropping remote RPC message for component %s of rpc index %s, entityId %u has already been deleted", + "EntityReplicationManager: Dropping remote RPC message for component %s of rpc index %s, entityId %llu has already been deleted", GetMultiplayerComponentRegistry()->GetComponentName(message.GetComponentId()), GetMultiplayerComponentRegistry()->GetComponentRpcName(message.GetComponentId(), message.GetRpcIndex()), - message.GetEntityId() + static_cast(message.GetEntityId()) ); return false; } @@ -1113,7 +1125,7 @@ namespace Multiplayer if (m_updateMode == EntityReplicationManager::Mode::LocalServerToRemoteServer) { - netBindComponent->NotifyServerMigration(GetRemoteHostId(), GetConnection().GetConnectionId()); + netBindComponent->NotifyServerMigration(GetRemoteHostId()); } bool didSucceed = true; @@ -1145,7 +1157,7 @@ namespace Multiplayer AZ_Assert(didSucceed, "Failed to migrate entity from server"); m_sendMigrateEntityEvent.Signal(m_connection, message); - AZLOG(NET_RepDeletes, "Migration packet sent %u to remote host %s", netEntityId, GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Migration packet sent %llu to remote host %s", static_cast(netEntityId), GetRemoteHostId().GetString().c_str()); // Notify all other EntityReplicationManagers that this entity has migrated so they can adjust their own replicators given our new proxy status GetMultiplayer()->SendNotifyEntityMigrationEvent(entityHandle, GetRemoteHostId()); @@ -1201,7 +1213,7 @@ namespace Multiplayer // Change the role on the replicator AddEntityReplicator(entityHandle, NetEntityRole::Server); - AZLOG(NET_RepDeletes, "Handle Migration %u new authority from remote host %s", entityHandle.GetNetEntityId(), GetRemoteHostId().GetString().c_str()); + AZLOG(NET_RepDeletes, "Handle Migration %llu new authority from remote host %s", static_cast(entityHandle.GetNetEntityId()), GetRemoteHostId().GetString().c_str()); return true; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp index 05001b5960..67527bf962 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/EntityReplication/EntityReplicator.cpp @@ -103,8 +103,8 @@ namespace Multiplayer AZ_Assert ( m_boundLocalNetworkRole != m_remoteNetworkRole, - "Invalid configuration detected, bound local role must differ from remote network role Role: %d", - aznumeric_cast(m_boundLocalNetworkRole) + "Invalid configuration detected, bound local role must differ from remote network role: %s", + GetEnumString(m_boundLocalNetworkRole) ); if (RemoteManagerOwnsEntityLifetime()) @@ -176,7 +176,6 @@ namespace Multiplayer switch (GetBoundLocalNetworkRole()) { case NetEntityRole::Authority: - { if (GetRemoteNetworkRole() == NetEntityRole::Client || GetRemoteNetworkRole() == NetEntityRole::Autonomous) { m_onSendRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); @@ -189,10 +188,8 @@ namespace Multiplayer { m_onForwardRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); } - } - break; + break; case NetEntityRole::Server: - { if (GetRemoteNetworkRole() == NetEntityRole::Authority) { m_onSendRpcHandler.Connect(netBindComponent->GetSendServerToAuthorityRpcEvent()); @@ -204,23 +201,21 @@ namespace Multiplayer // Listen for these to forward the rpc along to the other Client replicators m_onSendRpcHandler.Connect(netBindComponent->GetSendAuthorityToClientRpcEvent()); } - // NOTE: e_Autonomous is not connected to e_ServerProxy, it is always connected to an e_Authority - AZ_Assert(GetRemoteNetworkRole() != NetEntityRole::Autonomous, "Unexpected autonomous remote role") - } - break; + else if (GetRemoteNetworkRole() == NetEntityRole::Autonomous) + { + // NOTE: Autonomous is not connected to ServerProxy, it is always connected to an Authority + AZ_Assert(false, "Unexpected autonomous remote role") + } + break; case NetEntityRole::Client: - { // Nothing allowed, no Client to Server communication - } - break; + break; case NetEntityRole::Autonomous: - { if (GetRemoteNetworkRole() == NetEntityRole::Authority) { m_onSendRpcHandler.Connect(netBindComponent->GetSendAutonomousToAuthorityRpcEvent()); } - } - break; + break; default: AZ_Assert(false, "Unexpected network role"); } @@ -252,22 +247,9 @@ namespace Multiplayer if (entity->GetState() != AZ::Entity::State::Init) { - AZLOG_WARN("Trying to activate an entity that is not in the Init state (%u)", GetEntityHandle().GetNetEntityId()); + AZLOG_WARN("Trying to activate an entity that is not in the Init state (%llu)", static_cast(GetEntityHandle().GetNetEntityId())); } - // First we need to make sure the transform component has been updated with the correct value prior to activation - // This is because vanilla az components may only depend on the transform component, not the multiplayer transform component - //if (auto* locationComponent = FindCommonComponent(GetEntityHandle())) - //{ - // AZ::Transform newTransform = locationComponent->GetTransform(); - // auto* transformComponent = entity->FindComponent(); - // if (transformComponent) - // { - // // We can't use EBus here since the TransFormBus does not get connected until the activate call below - // transformComponent->SetWorldTM(newTransform); - // } - //} - // Ugly, but this is the only time we need to call a non-const function on this entity entity->Activate(); m_replicationManager.m_orphanedEntityRpcs.DispatchOrphanedRpcs(*this); @@ -281,8 +263,7 @@ namespace Multiplayer NetBindComponent* netBindComponent = m_netBindComponent; AZ_Assert(netBindComponent, "No Multiplayer::NetBindComponent"); - bool isAuthority = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetBoundLocalNetworkRole() == netBindComponent->GetNetEntityRole()); + bool isAuthority = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetBoundLocalNetworkRole() == netBindComponent->GetNetEntityRole()); bool isClient = GetRemoteNetworkRole() == NetEntityRole::Client; bool isAutonomous = GetBoundLocalNetworkRole() == NetEntityRole::Autonomous; if (isAuthority || isClient || isAutonomous) @@ -296,10 +277,10 @@ namespace Multiplayer bool EntityReplicator::OwnsReplicatorLifetime() const { bool ret(false); - if (GetBoundLocalNetworkRole() == NetEntityRole::Authority - || (GetBoundLocalNetworkRole() == NetEntityRole::Server + if (GetBoundLocalNetworkRole() == NetEntityRole::Authority // Authority always owns lifetime + || (GetBoundLocalNetworkRole() == NetEntityRole::Server // Server also owns lifetime if the remote endpoint is a client of some form && (GetRemoteNetworkRole() == NetEntityRole::Client - || GetRemoteNetworkRole() == NetEntityRole::Autonomous))) + || GetRemoteNetworkRole() == NetEntityRole::Autonomous))) { ret = true; } @@ -309,10 +290,9 @@ namespace Multiplayer bool EntityReplicator::RemoteManagerOwnsEntityLifetime() const { bool isServer = (GetBoundLocalNetworkRole() == NetEntityRole::Server) - && (GetRemoteNetworkRole() == NetEntityRole::Authority); + && (GetRemoteNetworkRole() == NetEntityRole::Authority); bool isClient = (GetBoundLocalNetworkRole() == NetEntityRole::Client) - || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous); - + || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous); return isServer || isClient; } @@ -429,10 +409,8 @@ namespace Multiplayer if (const NetworkTransformComponent* networkTransform = entity->FindComponent()) { const NetEntityId parentId = networkTransform->GetParentEntityId(); - /* - * For root entities attached to a level, a network parent won't be set. - * In this case, this entity is the root entity of the hierarchy and it will be activated first. - */ + // For root entities attached to a level, a network parent won't be set. + // In this case, this entity is the root entity of the hierarchy and it will be activated first. if (parentId != InvalidNetEntityId) { ConstNetworkEntityHandle parentHandle = GetNetworkEntityManager()->GetEntity(parentId); @@ -452,9 +430,9 @@ namespace Multiplayer AZLOG ( NET_HierarchyActivationInfo, - "Hierchical entity %s asking for activation - waiting on the parent %u", + "Hierchical entity %s asking for activation - waiting on the parent %llu", entity->GetName().c_str(), - aznumeric_cast(parentId) + aznumeric_cast(parentId) ); return false; } @@ -472,19 +450,19 @@ namespace Multiplayer AZLOG ( NET_RepDeletes, - "Sending delete replicator id %u migrated %d to remote host %s", - aznumeric_cast(GetEntityHandle().GetNetEntityId()), + "Sending delete replicator id %llu migrated %d to remote host %s", + aznumeric_cast(GetEntityHandle().GetNetEntityId()), WasMigrated() ? 1 : 0, m_replicationManager.GetRemoteHostId().GetString().c_str() ); - return NetworkEntityUpdateMessage(GetEntityHandle().GetNetEntityId(), WasMigrated(), m_propertyPublisher->IsRemoteReplicatorEstablished()); + return NetworkEntityUpdateMessage(GetEntityHandle().GetNetEntityId(), WasMigrated()); } NetBindComponent* netBindComponent = GetNetBindComponent(); - //const bool sendSliceName = !m_propertyPublisher->IsRemoteReplicatorEstablished(); + const bool sendSliceName = !m_propertyPublisher->IsRemoteReplicatorEstablished(); NetworkEntityUpdateMessage updateMessage(GetRemoteNetworkRole(), GetEntityHandle().GetNetEntityId()); - //if (sendSliceName) + if (sendSliceName) { updateMessage.SetPrefabEntityId(netBindComponent->GetPrefabEntityId()); } @@ -553,42 +531,33 @@ namespace Multiplayer switch (entityRpcMessage.GetRpcDeliveryType()) { case RpcDeliveryType::AuthorityToClient: - { if (((GetBoundLocalNetworkRole() == NetEntityRole::Client) || (GetBoundLocalNetworkRole() == NetEntityRole::Autonomous)) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are a local client, and we are connected to server, aka AuthorityToClient result = RpcValidationResult::HandleRpc; } - if ((GetBoundLocalNetworkRole() == NetEntityRole::Server) - && (GetRemoteNetworkRole() == NetEntityRole::Authority)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Server) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are on a server, and we received this message from another server, therefore we should forward this to any connected clients result = RpcValidationResult::ForwardToClient; } - } - break; + break; case RpcDeliveryType::AuthorityToAutonomous: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Autonomous) - && (GetRemoteNetworkRole() == NetEntityRole::Authority)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Autonomous) && (GetRemoteNetworkRole() == NetEntityRole::Authority)) { // We are an autonomous client, and we are connected to server, aka AuthorityToAutonomous result = RpcValidationResult::HandleRpc; } - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Server)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Server)) { // We are on a server, and we received this message from another server, therefore we should forward this to our autonomous player // This can occur if we've recently migrated result = RpcValidationResult::ForwardToAutonomous; } - } - break; + break; case RpcDeliveryType::AutonomousToAuthority: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Autonomous)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Autonomous)) { if (IsMarkedForRemoval()) { @@ -610,12 +579,9 @@ namespace Multiplayer result = RpcValidationResult::HandleRpc; } } - } - break; + break; case RpcDeliveryType::ServerToAuthority: - { - if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) - && (GetRemoteNetworkRole() == NetEntityRole::Server)) + if ((GetBoundLocalNetworkRole() == NetEntityRole::Authority) && (GetRemoteNetworkRole() == NetEntityRole::Server)) { // if we're marked for removal, then we should forward to whomever now owns this entity if (IsMarkedForRemoval()) @@ -638,9 +604,9 @@ namespace Multiplayer result = RpcValidationResult::HandleRpc; } } + break; } - break; - } + if (result == RpcValidationResult::DropRpcAndDisconnect) { bool isLocalServer = (GetBoundLocalNetworkRole() == NetEntityRole::Authority) || (GetBoundLocalNetworkRole() == NetEntityRole::Server); @@ -654,30 +620,29 @@ namespace Multiplayer { AZLOG_ERROR ( - "Dropping RPC and Connection EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + "Dropping RPC and Connection EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); } } + if (result == RpcValidationResult::DropRpc) { AZLOG ( NET_Rpc, - "Dropping RPC EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + "Dropping RPC EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); @@ -696,13 +661,12 @@ namespace Multiplayer { AZLOG_WARN ( - "Dropping RPC since entity deleted EntityId=%u LocalRole=%u RemoteRole=%u RpcDeliveryType=%u ComponentId=%u RpcType=%u IsReliable=%s IsMarkedForRemoval=%s", - aznumeric_cast(m_entityHandle.GetNetEntityId()), - aznumeric_cast(GetBoundLocalNetworkRole()), - aznumeric_cast(GetRemoteNetworkRole()), + "Dropping RPC since entity deleted EntityId=%llu LocalRole=%s RemoteRole=%s RpcDeliveryType=%u RpcName=%s IsReliable=%s IsMarkedForRemoval=%s", + aznumeric_cast(m_entityHandle.GetNetEntityId()), + GetEnumString(GetBoundLocalNetworkRole()), + GetEnumString(GetRemoteNetworkRole()), aznumeric_cast(entityRpcMessage.GetRpcDeliveryType()), - aznumeric_cast(entityRpcMessage.GetComponentId()), - aznumeric_cast(entityRpcMessage.GetRpcIndex()), + GetMultiplayerComponentRegistry()->GetComponentRpcName(entityRpcMessage.GetComponentId(), entityRpcMessage.GetRpcIndex()), entityRpcMessage.GetReliability() == ReliabilityType::Reliable ? "true" : "false", IsMarkedForRemoval() ? "true" : "false" ); @@ -740,23 +704,23 @@ namespace Multiplayer case RpcValidationResult::DropRpcAndDisconnect: return false; case RpcValidationResult::ForwardToClient: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendAuthorityToClientRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendAuthorityToClientRpcEvent().Signal(entityRpcMessage); + } return true; - } case RpcValidationResult::ForwardToAutonomous: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendAuthorityToAutonomousRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendAuthorityToAutonomousRpcEvent().Signal(entityRpcMessage); + } return true; - } case RpcValidationResult::ForwardToAuthority: - { - ScopedForwardingMessage forwarding(*this); - m_netBindComponent->GetSendServerToAuthorityRpcEvent().Signal(entityRpcMessage); + { + ScopedForwardingMessage forwarding(*this); + m_netBindComponent->GetSendServerToAuthorityRpcEvent().Signal(entityRpcMessage); + } return true; - } default: break; } diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp index 0a99f4b0d4..b3f87ea9ab 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityAuthorityTracker.cpp @@ -33,8 +33,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Removing timeout for networkEntityId %u from %s, new owner is %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Removing timeout for networkEntityId %llu from %s, new owner is %s", + aznumeric_cast(entityHandle.GetNetEntityId()), timeoutData->second.m_previousOwner.GetString().c_str(), newOwner.GetString().c_str() ); @@ -48,8 +48,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Assigning networkEntityId %u from %s to %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Assigning networkEntityId %llu from %s to %s", + aznumeric_cast(entityHandle.GetNetEntityId()), iter->second.back().GetString().c_str(), newOwner.GetString().c_str() ); @@ -59,8 +59,8 @@ namespace Multiplayer AZLOG ( NET_AuthTracker, - "AuthTracker: Assigning networkEntityId %u to %s", - aznumeric_cast(entityHandle.GetNetEntityId()), + "AuthTracker: Assigning networkEntityId %llu to %s", + aznumeric_cast(entityHandle.GetNetEntityId()), newOwner.GetString().c_str() ); } @@ -87,7 +87,7 @@ namespace Multiplayer } } - AZLOG(NET_AuthTracker, "AuthTracker: Removing networkEntityId %u from %s", aznumeric_cast(entityHandle.GetNetEntityId()), previousOwner.GetString().c_str()); + AZLOG(NET_AuthTracker, "AuthTracker: Removing networkEntityId %llu from %s", aznumeric_cast(entityHandle.GetNetEntityId()), previousOwner.GetString().c_str()); if (auto localEnt = entityHandle.GetEntity()) { if (authorityStack.empty()) @@ -114,14 +114,14 @@ namespace Multiplayer } else { - AZLOG(NET_AuthTracker, "AuthTracker: Skipping timeout for Autonomous networkEntityId %u", aznumeric_cast(entityHandle.GetNetEntityId())); + AZLOG(NET_AuthTracker, "AuthTracker: Skipping timeout for Autonomous networkEntityId %llu", aznumeric_cast(entityHandle.GetNetEntityId())); } } } } else { - AZLOG(NET_AuthTracker, "AuthTracker: Remove authority called on networkEntityId that was never added %u", aznumeric_cast(entityHandle.GetNetEntityId())); + AZLOG(NET_AuthTracker, "AuthTracker: Remove authority called on networkEntityId that was never added %llu", aznumeric_cast(entityHandle.GetNetEntityId())); AZ_Assert(false, "AuthTracker: Remove authority called on entity that was never added"); } } @@ -205,8 +205,8 @@ namespace Multiplayer { AZLOG_ERROR ( - "Timed out entity id %u during migration previous owner %s, removing it", - aznumeric_cast(entityHandle.GetNetEntityId()), + "Timed out entity id %llu during migration previous owner %s, removing it", + aznumeric_cast(entityHandle.GetNetEntityId()), timeoutData->second.m_previousOwner.GetString().c_str() ); m_networkEntityManager.MarkForRemoval(entityHandle); diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp index 7794d9026b..457395a61e 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityHandle.cpp @@ -18,21 +18,13 @@ namespace Multiplayer { ConstNetworkEntityHandle::ConstNetworkEntityHandle(AZ::Entity* entity, const NetworkEntityTracker* networkEntityTracker) : m_entity(entity) - , m_networkEntityTracker(networkEntityTracker) + , m_networkEntityTracker((networkEntityTracker != nullptr) ? networkEntityTracker : GetNetworkEntityTracker()) { - if (m_networkEntityTracker == nullptr) - { - m_networkEntityTracker = GetNetworkEntityTracker(); - } - - if (m_networkEntityTracker) - { - m_changeDirty = m_networkEntityTracker->GetChangeDirty(m_entity); - } + AZ_Assert(m_networkEntityTracker, "NetworkEntityTracker is not valid"); + m_changeDirty = m_networkEntityTracker->GetChangeDirty(m_entity); if (entity) { - AZ_Assert(m_networkEntityTracker, "NetworkEntityTracker is not valid"); m_netBindComponent = m_networkEntityTracker->GetNetBindComponent(entity); if (m_netBindComponent != nullptr) { diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp index b72ea21719..c7582af83f 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityManager.cpp @@ -48,6 +48,20 @@ namespace Multiplayer void NetworkEntityManager::Initialize(const HostId& hostId, AZStd::unique_ptr entityDomain) { m_hostId = hostId; + + // Configure our vended NetEntityIds so that no two hosts generate the same NetEntityId + { + // Needs more thought + const uint64_t addrPortion = hostId.GetAddress(AzNetworking::ByteOrder::Host); + const uint64_t portPortion = hostId.GetPort(AzNetworking::ByteOrder::Host); + const uint64_t hostIdentifier = (portPortion << 32) | addrPortion; + const AZ::HashValue32 hostHash = AZ::TypeHash32(hostIdentifier); + + NetEntityId hostEntityIdOffset = static_cast(hostHash) << 32; + m_nextEntityId &= NetEntityId{ 0x0000000000000000FFFFFFFFFFFFFFFF }; + m_nextEntityId |= hostEntityIdOffset; + } + m_entityDomain = AZStd::move(entityDomain); m_updateEntityDomainEvent.Enqueue(net_EntityDomainUpdateMs, true); m_entityDomain->ActivateTracking(m_ownedEntities); @@ -227,11 +241,19 @@ namespace Multiplayer { AZ::Entity* entity = it->second; NetBindComponent* netBindComponent = m_networkEntityTracker.GetNetBindComponent(entity); + AZ::Aabb entityBounds = AZ::Interface::Get()->GetEntityWorldBoundsUnion(entity->GetId()); + entityBounds.Expand(AZ::Vector3(0.01f)); if (netBindComponent->GetNetEntityRole() == NetEntityRole::Authority) { - const AZ::Aabb entityBounds = AZ::Interface::Get()->GetEntityWorldBoundsUnion(entity->GetId()); - debugDisplay->DrawWireBox(entityBounds.GetMin(), entityBounds.GetMax()); + debugDisplay->SetColor(AZ::Colors::Black); + debugDisplay->SetAlpha(0.5f); } + else + { + debugDisplay->SetColor(AZ::Colors::DeepSkyBlue); + debugDisplay->SetAlpha(0.25f); + } + debugDisplay->DrawWireBox(entityBounds.GetMin(), entityBounds.GetMax()); } if (m_entityDomain != nullptr) diff --git a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp index 4a2f12ce17..d635dbaf80 100644 --- a/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkEntity/NetworkEntityUpdateMessage.cpp @@ -18,7 +18,6 @@ namespace Multiplayer , m_entityId(rhs.m_entityId) , m_isDelete(rhs.m_isDelete) , m_wasMigrated(rhs.m_wasMigrated) - , m_takeOwnership(rhs.m_takeOwnership) , m_hasValidPrefabId(rhs.m_hasValidPrefabId) , m_prefabEntityId(rhs.m_prefabEntityId) , m_data(AZStd::move(rhs.m_data)) @@ -31,7 +30,6 @@ namespace Multiplayer , m_entityId(rhs.m_entityId) , m_isDelete(rhs.m_isDelete) , m_wasMigrated(rhs.m_wasMigrated) - , m_takeOwnership(rhs.m_takeOwnership) , m_hasValidPrefabId(rhs.m_hasValidPrefabId) , m_prefabEntityId(rhs.m_prefabEntityId) { @@ -58,11 +56,10 @@ namespace Multiplayer ; } - NetworkEntityUpdateMessage::NetworkEntityUpdateMessage(NetEntityId entityId, bool wasMigrated, bool takeOwnership) + NetworkEntityUpdateMessage::NetworkEntityUpdateMessage(NetEntityId entityId, bool wasMigrated) : m_entityId(entityId) , m_isDelete(true) , m_wasMigrated(wasMigrated) - , m_takeOwnership(takeOwnership) { // this is a delete entity message c-tor } @@ -73,7 +70,6 @@ namespace Multiplayer m_entityId = rhs.m_entityId; m_isDelete = rhs.m_isDelete; m_wasMigrated = rhs.m_wasMigrated; - m_takeOwnership = rhs.m_takeOwnership; m_hasValidPrefabId = rhs.m_hasValidPrefabId; m_prefabEntityId = rhs.m_prefabEntityId; m_data = AZStd::move(rhs.m_data); @@ -86,7 +82,6 @@ namespace Multiplayer m_entityId = rhs.m_entityId; m_isDelete = rhs.m_isDelete; m_wasMigrated = rhs.m_wasMigrated; - m_takeOwnership = rhs.m_takeOwnership; m_hasValidPrefabId = rhs.m_hasValidPrefabId; m_prefabEntityId = rhs.m_prefabEntityId; if (rhs.m_data != nullptr) @@ -104,7 +99,6 @@ namespace Multiplayer && (m_entityId == rhs.m_entityId) && (m_isDelete == rhs.m_isDelete) && (m_wasMigrated == rhs.m_wasMigrated) - && (m_takeOwnership == rhs.m_takeOwnership) && (m_hasValidPrefabId == rhs.m_hasValidPrefabId) && (m_prefabEntityId == rhs.m_prefabEntityId)); } @@ -160,11 +154,6 @@ namespace Multiplayer return m_wasMigrated; } - bool NetworkEntityUpdateMessage::GetTakeOwnership() const - { - return m_takeOwnership; - } - bool NetworkEntityUpdateMessage::GetHasValidPrefabId() const { return m_hasValidPrefabId; @@ -210,17 +199,15 @@ namespace Multiplayer serializer.Serialize(m_entityId, "EntityId"); // Use the upper 4 bits for boolean flags, and the lower 4 bits for the network role - uint8_t networkTypeAndFlags = (m_isDelete ? 0x80 : 0x00) - | (m_wasMigrated ? 0x40 : 0x00) - | (m_takeOwnership ? 0x20 : 0x00) + uint8_t networkTypeAndFlags = (m_isDelete ? 0x40 : 0x00) + | (m_wasMigrated ? 0x20 : 0x00) | (m_hasValidPrefabId ? 0x10 : 0x00) | static_cast(m_networkRole); if (serializer.Serialize(networkTypeAndFlags, "TypeAndFlags")) { - m_isDelete = (networkTypeAndFlags & 0x80) == 0x80; - m_wasMigrated = (networkTypeAndFlags & 0x40) == 0x40; - m_takeOwnership = (networkTypeAndFlags & 0x20) == 0x20; + m_isDelete = (networkTypeAndFlags & 0x40) == 0x40; + m_wasMigrated = (networkTypeAndFlags & 0x20) == 0x20; m_hasValidPrefabId = (networkTypeAndFlags & 0x10) == 0x10; m_networkRole = static_cast(networkTypeAndFlags & 0x0F); } diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp index 638dc9a900..9d566537c5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputArray.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include #include diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp index bea110f298..59fb62e1b5 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputChild.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp index 54813327ab..00fd9f13b8 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputHistory.cpp @@ -6,7 +6,7 @@ * */ -#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp index d95c46261f..ec57891725 100644 --- a/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp +++ b/Gems/Multiplayer/Code/Source/NetworkInput/NetworkInputMigrationVector.cpp @@ -6,7 +6,7 @@ * */ -#include +#include #include #include diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp index 856fd59f9d..deff1f640f 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.cpp @@ -92,17 +92,10 @@ namespace Multiplayer return m_isPoorConnection ? sv_MinEntitiesToReplicate : sv_MaxEntitiesToReplicate; } - bool ServerToClientReplicationWindow::IsInWindow(const ConstNetworkEntityHandle& entityHandle, NetEntityRole& outNetworkRole) const + bool ServerToClientReplicationWindow::IsInWindow([[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, NetEntityRole& outNetworkRole) const { - // TODO: Clean up this interface, this function is used for server->server migrations, and probably shouldn't be exposed in it's current setup AZ_Assert(false, "IsInWindow should not be called on the ServerToClientReplicationWindow"); outNetworkRole = NetEntityRole::InvalidRole; - auto iter = m_replicationSet.find(entityHandle); - if (iter != m_replicationSet.end()) - { - outNetworkRole = iter->second.m_netEntityRole; - return true; - } return false; } @@ -146,7 +139,7 @@ namespace Multiplayer NetworkEntityTracker* networkEntityTracker = GetNetworkEntityTracker(); IFilterEntityManager* filterEntityManager = GetMultiplayer()->GetFilterEntityManager(); - // Add all the neighbors + // Add all the neighbours for (AzFramework::VisibilityEntry* visEntry : gatheredEntries) { AZ::Entity* entity = static_cast(visEntry->m_userData); @@ -301,7 +294,6 @@ namespace Multiplayer void ServerToClientReplicationWindow::AddEntityToReplicationSet(ConstNetworkEntityHandle& entityHandle, float priority, [[maybe_unused]] float distanceSquared) { // Assumption: the entity has been checked for filtering prior to this call. - if (!sv_ReplicateServerProxies) { NetBindComponent* netBindComponent = entityHandle.GetNetBindComponent(); @@ -312,11 +304,11 @@ namespace Multiplayer } } - const bool isQueueFull = (m_candidateQueue.size() >= sv_MaxEntitiesToTrackReplication); // See if have the maximum number of entities in our set + const bool isQueueFull = (m_candidateQueue.size() >= sv_MaxEntitiesToTrackReplication); // See if have the maximum number of entities in our set const bool isInReplicationSet = m_replicationSet.find(entityHandle) != m_replicationSet.end(); if (!isInReplicationSet) { - if (isQueueFull) // if our set is full, then we need to remove the worst priority in our set + if (isQueueFull) // If our set is full, then we need to remove the worst priority in our set { ConstNetworkEntityHandle removeEnt = m_candidateQueue.top().m_entityHandle; m_candidateQueue.pop(); @@ -332,7 +324,7 @@ namespace Multiplayer INetworkEntityManager* networkEntityManager = AZ::Interface::Get(); AZ_Assert(networkEntityManager, "NetworkEntityManager must be created."); - for (const AZ::Entity* controlledEntity : hierarchyComponent.m_hierarchicalEntities) + for (const AZ::Entity* controlledEntity : hierarchyComponent.GetHierarchicalEntities()) { NetEntityId controlledNetEntitydId = networkEntityManager->GetNetEntityIdById(controlledEntity->GetId()); AZ_Assert(controlledNetEntitydId != InvalidNetEntityId, "Unable to find the hierarchy entity in Network Entity Manager"); diff --git a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h index 8816209d7b..3b4fddfe99 100644 --- a/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h +++ b/Gems/Multiplayer/Code/Source/ReplicationWindows/ServerToClientReplicationWindow.h @@ -76,8 +76,6 @@ namespace Multiplayer AZ::EntityActivatedEvent::Handler m_entityActivatedEventHandler; AZ::EntityDeactivatedEvent::Handler m_entityDeactivatedEventHandler; - //NetBindComponent* m_controlledNetBindComponent = nullptr; - AzNetworking::IConnection* m_connection = nullptr; // Cached values to detect a poor network connection diff --git a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp index badee73e04..386496045c 100644 --- a/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ClientHierarchyTests.cpp @@ -16,7 +16,7 @@ #include #include #include -#include +#include #include namespace Multiplayer @@ -213,9 +213,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -303,6 +302,36 @@ namespace Multiplayer SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); } + TEST_F(ClientSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent) + { + // disconnect and assign new connection ids + SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); + + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_child->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + const ConnectionId previousConnectionId = m_child->m_entity->FindComponent()->GetOwningConnectionId(); + + // re-attach, child's owning connection id should then be root's connection id + SetParentIdOnNetworkTransform(m_child->m_entity, RootNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, RootNetEntityId); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + // detach, the child should roll back to his previous owning connection id + SetParentIdOnNetworkTransform(m_child->m_entity, InvalidNetEntityId); + SetHierarchyRootFieldOnNetworkHierarchyChildOnClient(m_child->m_entity, InvalidNetEntityId); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + previousConnectionId + ); + } + /* * Parent -> Child -> ChildOfChild */ @@ -397,7 +426,7 @@ namespace Multiplayer using MultiplayerTest::TestMultiplayerComponentNetworkInput; auto* rootNetBind = m_root->m_entity->FindComponent(); - + NetworkInputArray inputArray(rootNetBind->GetEntityHandle()); NetworkInput& input = inputArray[0]; diff --git a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h index 5a528ed497..3c3d77e011 100644 --- a/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonBenchmarkSetup.h @@ -342,8 +342,11 @@ namespace Multiplayer void AddClientMigrationEndEventHandler([[maybe_unused]] ClientMigrationEndEvent::Handler& handler) override {} void AddNotifyClientMigrationHandler([[maybe_unused]] NotifyClientMigrationEvent::Handler& handler) override {} void AddNotifyEntityMigrationEventHandler([[maybe_unused]] NotifyEntityMigrationEvent::Handler& handler) override {} - void SendNotifyClientMigrationEvent([[maybe_unused]] const HostId& hostId, [[maybe_unused]] uint64_t userIdentifier, [[maybe_unused]] ClientInputId lastClientInputId) override {} + void SendNotifyClientMigrationEvent([[maybe_unused]] AzNetworking::ConnectionId connectionId, [[maybe_unused]] const HostId& hostId, + [[maybe_unused]] uint64_t userIdentifier, [[maybe_unused]] ClientInputId lastClientInputId, [[maybe_unused]] NetEntityId netEntityId) override {} void SendNotifyEntityMigrationEvent([[maybe_unused]] const ConstNetworkEntityHandle& entityHandle, [[maybe_unused]] const HostId& remoteHostId) override {} + void RegisterPlayerIdentifierForRejoin(uint64_t, NetEntityId) override {} + void CompleteClientMigration(uint64_t, AzNetworking::ConnectionId, const HostId&, ClientInputId) override {} void SetShouldSpawnNetworkEntities([[maybe_unused]] bool value) override {} bool GetShouldSpawnNetworkEntities() const override { return true; } @@ -535,9 +538,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(netParentId), - "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(netParentId, "parentEntityId"); // Derived from NetworkTransformComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -560,9 +562,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); diff --git a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h index 38027c11a9..249837b484 100644 --- a/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h +++ b/Gems/Multiplayer/Code/Tests/CommonHierarchySetup.h @@ -317,9 +317,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(netParentId), - "parentEntityId", /* Derived from NetworkTransformComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(netParentId, "parentEntityId"); // Derived from NetworkTransformComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); @@ -365,9 +364,8 @@ namespace Multiplayer constexpr uint32_t bufferSize = 100; AZStd::array buffer = {}; NetworkInputSerializer inSerializer(buffer.begin(), bufferSize); - inSerializer.Serialize(reinterpret_cast(value), - "hierarchyRoot", /* Derived from NetworkHierarchyChildComponent.AutoComponent.xml */ - AZStd::numeric_limits::min(), AZStd::numeric_limits::max()); + ISerializer& serializer = inSerializer; + serializer.Serialize(value, "hierarchyRoot"); // Derived from NetworkHierarchyChildComponent.AutoComponent.xml NetworkOutputSerializer outSerializer(buffer.begin(), bufferSize); diff --git a/Gems/Multiplayer/Code/Tests/MockInterfaces.h b/Gems/Multiplayer/Code/Tests/MockInterfaces.h index 527aeb51bc..8cebf280b9 100644 --- a/Gems/Multiplayer/Code/Tests/MockInterfaces.h +++ b/Gems/Multiplayer/Code/Tests/MockInterfaces.h @@ -33,7 +33,7 @@ namespace UnitTest MOCK_METHOD1(AddServerAcceptanceReceivedHandler, void(Multiplayer::ServerAcceptanceReceivedEvent::Handler&)); MOCK_METHOD1(AddSessionInitHandler, void(Multiplayer::SessionInitEvent::Handler&)); MOCK_METHOD1(AddSessionShutdownHandler, void(Multiplayer::SessionShutdownEvent::Handler&)); - MOCK_METHOD3(SendNotifyClientMigrationEvent, void(const Multiplayer::HostId&, uint64_t, Multiplayer::ClientInputId)); + MOCK_METHOD5(SendNotifyClientMigrationEvent, void(AzNetworking::ConnectionId, const Multiplayer::HostId&, uint64_t, Multiplayer::ClientInputId, Multiplayer::NetEntityId)); MOCK_METHOD2(SendNotifyEntityMigrationEvent, void(const Multiplayer::ConstNetworkEntityHandle&, const Multiplayer::HostId&)); MOCK_METHOD1(SendReadyForEntityUpdates, void(bool)); MOCK_CONST_METHOD0(GetCurrentHostTimeMs, AZ::TimeMs()); @@ -42,6 +42,8 @@ namespace UnitTest MOCK_METHOD0(GetNetworkEntityManager, Multiplayer::INetworkEntityManager* ()); MOCK_METHOD1(SetFilterEntityManager, void(Multiplayer::IFilterEntityManager*)); MOCK_METHOD0(GetFilterEntityManager, Multiplayer::IFilterEntityManager* ()); + MOCK_METHOD2(RegisterPlayerIdentifierForRejoin, void(uint64_t, Multiplayer::NetEntityId)); + MOCK_METHOD4(CompleteClientMigration, void(uint64_t, AzNetworking::ConnectionId, const Multiplayer::HostId&, Multiplayer::ClientInputId)); MOCK_METHOD1(SetShouldSpawnNetworkEntities, void(bool)); MOCK_CONST_METHOD0(GetShouldSpawnNetworkEntities, bool()); }; diff --git a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp index 64381bf195..0df0fce610 100644 --- a/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp +++ b/Gems/Multiplayer/Code/Tests/NetworkInputTests.cpp @@ -17,9 +17,9 @@ #include #include #include -#include -#include -#include +#include +#include +#include namespace Multiplayer { diff --git a/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp b/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp index 1b5aa31780..2888071508 100644 --- a/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp +++ b/Gems/Multiplayer/Code/Tests/ServerHierarchyTests.cpp @@ -195,6 +195,49 @@ namespace Multiplayer m_child->m_entity.reset(); } + TEST_F(ServerSimpleHierarchyTests, ChildPointsToRootAfterReattachment) + { + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetHierarchyRoot(), + InvalidNetEntityId + ); + + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetHierarchyRoot(), + m_root->m_entity->FindComponent()->GetNetEntityId() + ); + } + + TEST_F(ServerSimpleHierarchyTests, ChildHasOwningConnectionIdOfParent) + { + // disconnect and assign new connection ids + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_child->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + const ConnectionId previousConnectionId = m_child->m_entity->FindComponent()->GetOwningConnectionId(); + + // re-attach, child's owning connection id should then be root's connection id + m_child->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + // detach, the child should roll back to his previous owning connection id + m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child->m_entity->FindComponent()->GetOwningConnectionId(), + previousConnectionId + ); + } + /* * Parent -> Child -> ChildOfChild */ @@ -410,7 +453,7 @@ namespace Multiplayer { MockNetworkHierarchyCallbackHandler mock; EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root->m_entity->GetId())).Times(2); - + m_root->m_entity->FindComponent()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler); m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); @@ -822,6 +865,22 @@ namespace Multiplayer } } + TEST_F(ServerHierarchyOfHierarchyTests, InnerChildrenPointToInnerRootAfterDetachmentFromTopRoot) + { + m_root2->m_entity->FindComponent()->SetParent(m_root->m_entity->GetId()); + // detach + m_root2->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetHierarchyRoot(), + m_root2->m_entity->FindComponent()->GetNetEntityId() + ); + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetHierarchyRoot(), + m_root2->m_entity->FindComponent()->GetNetEntityId() + ); + } + TEST_F(ServerHierarchyOfHierarchyTests, Inner_Root_Has_Child_References_After_Detachment_From_Child_Of_Child) { m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); @@ -1005,6 +1064,59 @@ namespace Multiplayer m_console->GetCvarValue("bg_hierarchyEntityMaxLimit", currentMaxLimit); } + TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveOwningConnectionIdOfTopRoot) + { + // Assign new connection ids. + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_root2->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + // Attach then inner hierarchy's owning connection id should then be top root's connection id. + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + EXPECT_EQ( + m_root2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root->m_entity->FindComponent()->GetOwningConnectionId() + ); + } + + TEST_F(ServerHierarchyOfHierarchyTests, InnerRootAndItsChildrenHaveTheirOriginalOwningConnectionIdAfterDetachingFromTopRoot) + { + // Assign new connection ids. + m_root->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 1 }); + m_root2->m_entity->FindComponent()->SetOwningConnectionId(ConnectionId{ 2 }); + + // Attach then inner hierarchy's owning connection id should then be top root's connection id. + m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); + + // detach, inner hierarchy should roll back to his previous owning connection id + m_root2->m_entity->FindComponent()->SetParent(AZ::EntityId()); + + EXPECT_EQ( + m_root2->m_entity->FindComponent()->GetOwningConnectionId(), + ConnectionId{ 2 } + ); + + EXPECT_EQ( + m_child2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root2->m_entity->FindComponent()->GetOwningConnectionId() + ); + + EXPECT_EQ( + m_childOfChild2->m_entity->FindComponent()->GetOwningConnectionId(), + m_root2->m_entity->FindComponent()->GetOwningConnectionId() + ); + } + /* * Parent -> Child -> ChildOfChild (not marked as in a hierarchy) */ @@ -1242,15 +1354,15 @@ namespace Multiplayer ); } - TEST_F(ServerHierarchyWithThreeRoots, ReattachMiddleChildWhileLastChildGetsLeaveEventOnce) + TEST_F(ServerHierarchyWithThreeRoots, InnerRootLeftTopRootThenLastChildGetsJoinedEventOnce) { m_root2->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); m_root3->m_entity->FindComponent()->SetParent(m_childOfChild->m_entity->GetId()); MockNetworkHierarchyCallbackHandler mock; - EXPECT_CALL(mock, OnNetworkHierarchyLeave()); - - m_childOfChild3->m_entity->FindComponent()->BindNetworkHierarchyLeaveEventHandler(mock.m_leaveHandler); + EXPECT_CALL(mock, OnNetworkHierarchyUpdated(m_root3->m_entity->GetId())); + + m_childOfChild3->m_entity->FindComponent()->BindNetworkHierarchyChangedEventHandler(mock.m_changedHandler); m_child->m_entity->FindComponent()->SetParent(AZ::EntityId()); } diff --git a/Gems/Multiplayer/Code/multiplayer_files.cmake b/Gems/Multiplayer/Code/multiplayer_files.cmake index f16483e663..a799278203 100644 --- a/Gems/Multiplayer/Code/multiplayer_files.cmake +++ b/Gems/Multiplayer/Code/multiplayer_files.cmake @@ -45,6 +45,10 @@ set(FILES Include/Multiplayer/NetworkEntity/NetworkEntityUpdateMessage.h Include/Multiplayer/NetworkInput/IMultiplayerComponentInput.h Include/Multiplayer/NetworkInput/NetworkInput.h + Include/Multiplayer/NetworkInput/NetworkInputArray.h + Include/Multiplayer/NetworkInput/NetworkInputChild.h + Include/Multiplayer/NetworkInput/NetworkInputHistory.h + Include/Multiplayer/NetworkInput/NetworkInputMigrationVector.h Include/Multiplayer/NetworkTime/INetworkTime.h Include/Multiplayer/NetworkTime/RewindableArray.h Include/Multiplayer/NetworkTime/RewindableArray.inl @@ -114,13 +118,9 @@ set(FILES Source/NetworkEntity/NetworkSpawnableLibrary.h Source/NetworkInput/NetworkInput.cpp Source/NetworkInput/NetworkInputArray.cpp - Source/NetworkInput/NetworkInputArray.h Source/NetworkInput/NetworkInputChild.cpp - Source/NetworkInput/NetworkInputChild.h Source/NetworkInput/NetworkInputHistory.cpp - Source/NetworkInput/NetworkInputHistory.h Source/NetworkInput/NetworkInputMigrationVector.cpp - Source/NetworkInput/NetworkInputMigrationVector.h Source/NetworkTime/NetworkTime.cpp Source/NetworkTime/NetworkTime.h Source/Pipeline/NetworkSpawnableHolderComponent.cpp diff --git a/Gems/PhysX/Code/Editor/DebugDraw.cpp b/Gems/PhysX/Code/Editor/DebugDraw.cpp index 5936312ecf..734557205a 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.cpp +++ b/Gems/PhysX/Code/Editor/DebugDraw.cpp @@ -676,7 +676,17 @@ namespace PhysX } } - AZ::Transform Collider::GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, + void Collider::DrawHeightfield( + [[maybe_unused]] AzFramework::DebugDisplayRequests& debugDisplay, + [[maybe_unused]] const Physics::ColliderConfiguration& colliderConfig, + [[maybe_unused]] const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig, + [[maybe_unused]] const AZ::Vector3& colliderScale, + [[maybe_unused]] const bool forceUniformScaling) const + { + } + + AZ::Transform Collider::GetColliderLocalTransform( + const Physics::ColliderConfiguration& colliderConfig, const AZ::Vector3& colliderScale) const { // Apply entity world transform scale to collider offset diff --git a/Gems/PhysX/Code/Editor/DebugDraw.h b/Gems/PhysX/Code/Editor/DebugDraw.h index 774def6900..74f08aae99 100644 --- a/Gems/PhysX/Code/Editor/DebugDraw.h +++ b/Gems/PhysX/Code/Editor/DebugDraw.h @@ -104,7 +104,15 @@ namespace PhysX const AZ::Vector3& meshScale, AZ::u32 geomIndex) const; - void DrawPolygonPrism(AzFramework::DebugDisplayRequests& debugDisplay, + void DrawHeightfield( + AzFramework::DebugDisplayRequests& debugDisplay, + const Physics::ColliderConfiguration& colliderConfig, + const Physics::HeightfieldShapeConfiguration& heightfieldShapeConfig, + const AZ::Vector3& colliderScale = AZ::Vector3::CreateOne(), + const bool forceUniformScaling = false) const; + + void DrawPolygonPrism( + AzFramework::DebugDisplayRequests& debugDisplay, const Physics::ColliderConfiguration& colliderConfig, const AZStd::vector& points) const; AZ::Transform GetColliderLocalTransform(const Physics::ColliderConfiguration& colliderConfig, diff --git a/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h b/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h index f9bd2dbad5..a31a4e65b8 100644 --- a/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h +++ b/Gems/PhysX/Code/Include/PhysX/SystemComponentBus.h @@ -16,19 +16,21 @@ namespace AzPhysics { class CollisionGroup; class CollisionLayer; -} +} // namespace AzPhysics namespace physx { class PxScene; class PxSceneDesc; class PxConvexMesh; + class PxHeightField; class PxTriangleMesh; class PxShape; class PxCooking; class PxControllerManager; struct PxFilterData; -} + struct PxHeightFieldSample; +} // namespace physx namespace PhysX { @@ -63,6 +65,13 @@ namespace PhysX /// @return Pointer to the created mesh. virtual physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) = 0; + /// Creates a new heightfield. + /// @param samples Pointer to beginning of heightfield sample data. + /// @param numRows Number of rows in the heightfield. + /// @param numColumns Number of columns in the heightfield. + /// @return Pointer to the created heightfield. + virtual physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) = 0; + /// Creates PhysX collision filter data from generic collision filtering settings. /// @param layer The collision layer the object belongs to. /// @param group The set of collision layers the object will interact with. diff --git a/Gems/PhysX/Code/Source/ComponentDescriptors.cpp b/Gems/PhysX/Code/Source/ComponentDescriptors.cpp index ebe6efd310..83232edc40 100644 --- a/Gems/PhysX/Code/Source/ComponentDescriptors.cpp +++ b/Gems/PhysX/Code/Source/ComponentDescriptors.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -36,6 +37,7 @@ namespace PhysX BaseColliderComponent::CreateDescriptor(), MeshColliderComponent::CreateDescriptor(), BoxColliderComponent::CreateDescriptor(), + HeightfieldColliderComponent::CreateDescriptor(), SphereColliderComponent::CreateDescriptor(), CapsuleColliderComponent::CreateDescriptor(), ShapeColliderComponent::CreateDescriptor(), diff --git a/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp b/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp index d43d0edc23..362903f638 100644 --- a/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp +++ b/Gems/PhysX/Code/Source/EditorComponentDescriptors.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -35,6 +36,7 @@ namespace PhysX EditorColliderComponent::CreateDescriptor(), EditorFixedJointComponent::CreateDescriptor(), EditorForceRegionComponent::CreateDescriptor(), + EditorHeightfieldColliderComponent::CreateDescriptor(), EditorHingeJointComponent::CreateDescriptor(), EditorJointComponent::CreateDescriptor(), EditorRigidBodyComponent::CreateDescriptor(), diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp new file mode 100644 index 0000000000..96b1bd51f5 --- /dev/null +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.cpp @@ -0,0 +1,341 @@ +/* + * 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 + +namespace PhysX +{ + void EditorHeightfieldColliderComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("ColliderConfiguration", &EditorHeightfieldColliderComponent::m_colliderConfig) + ->Field("DebugDrawSettings", &EditorHeightfieldColliderComponent::m_colliderDebugDraw) + ->Field("ShapeConfig", &EditorHeightfieldColliderComponent::m_shapeConfig) + ; + + if (auto editContext = serializeContext->GetEditContext()) + { + editContext->Class( + "PhysX Heightfield Collider", "Creates geometry in the PhysX simulation based on an attached heightfield component") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Category, "PhysX") + ->Attribute(AZ::Edit::Attributes::Icon, "Icons/Components/PhysXCollider.svg") + ->Attribute(AZ::Edit::Attributes::ViewportIcon, "Icons/Components/Viewport/PhysXCollider.svg") + ->Attribute(AZ::Edit::Attributes::AppearsInAddComponentMenu, AZ_CRC_CE("Game")) + ->Attribute( + AZ::Edit::Attributes::HelpPageURL, "https://o3de.org/docs/user-guide/components/reference/physx/heightfield-collider/") + ->Attribute(AZ::Edit::Attributes::AutoExpand, true) + ->DataElement( + AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderConfig, "Collider configuration", + "Configuration of the collider") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::ChangeNotify, &EditorHeightfieldColliderComponent::OnConfigurationChanged) + ->DataElement( + AZ::Edit::UIHandlers::Default, &EditorHeightfieldColliderComponent::m_colliderDebugDraw, "Debug draw settings", + "Debug draw settings") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ; + } + } + } + + void EditorHeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService")); + } + + void EditorHeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void EditorHeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PhysXColliderService")); + incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + EditorHeightfieldColliderComponent::EditorHeightfieldColliderComponent() + : m_physXConfigChangedHandler( + []([[maybe_unused]] const AzPhysics::SystemConfiguration* config) + { + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues); + }) + , m_onMaterialLibraryChangedEventHandler( + [this](const AZ::Data::AssetId& defaultMaterialLibrary) + { + m_colliderConfig.m_materialSelection.OnMaterialLibraryChanged(defaultMaterialLibrary); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + + AzToolsFramework::PropertyEditorGUIMessages::Bus::Broadcast( + &AzToolsFramework::PropertyEditorGUIMessages::RequestRefresh, + AzToolsFramework::PropertyModificationRefreshLevel::Refresh_AttributesAndValues); + }) + { + } + + EditorHeightfieldColliderComponent ::~EditorHeightfieldColliderComponent() + { + ClearHeightfield(); + } + + // AZ::Component + void EditorHeightfieldColliderComponent::Activate() + { + AzToolsFramework::Components::EditorComponentBase::Activate(); + + // Heightfields don't support the following: + // - Offset: There shouldn't be a need to offset the data, since the heightfield provider is giving a physics representation + // - IsTrigger: PhysX heightfields don't support acting as triggers + // - MaterialSelection: The heightfield provider provides per-vertex material selection + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::Offset, false); + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::IsTrigger, false); + m_colliderConfig.SetPropertyVisibility(Physics::ColliderConfiguration::MaterialSelection, false); + + m_sceneInterface = AZ::Interface::Get(); + if (m_sceneInterface) + { + m_attachedSceneHandle = m_sceneInterface->GetSceneHandle(AzPhysics::EditorPhysicsSceneName); + } + + const AZ::EntityId entityId = GetEntityId(); + + AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusConnect(entityId); + + // Debug drawing + m_colliderDebugDraw.Connect(entityId); + m_colliderDebugDraw.SetDisplayCallback(this); + + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId); + PhysX::ColliderShapeRequestBus::Handler::BusConnect(entityId); + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId); + + RefreshHeightfield(); + } + + void EditorHeightfieldColliderComponent::Deactivate() + { + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); + PhysX::ColliderShapeRequestBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + + m_colliderDebugDraw.Disconnect(); + AzToolsFramework::EntitySelectionEvents::Bus::Handler::BusDisconnect(); + AzToolsFramework::Components::EditorComponentBase::Deactivate(); + + ClearHeightfield(); + } + + void EditorHeightfieldColliderComponent::BuildGameEntity(AZ::Entity* gameEntity) + { + auto* heightfieldColliderComponent = gameEntity->CreateComponent(); + heightfieldColliderComponent->SetShapeConfiguration( + { AZStd::make_shared(m_colliderConfig), m_shapeConfig }); + } + + void EditorHeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + RefreshHeightfield(); + } + + void EditorHeightfieldColliderComponent::ClearHeightfield() + { + // There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate: + // - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it + // - The shape config is also holding onto a pointer to the Heightfield + + // We remove the simulated body first, since we don't want the heightfield to exist any more. + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + m_sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + + // Now we can safely clear out the cached heightfield pointer. + m_shapeConfig->SetCachedNativeHeightfield(nullptr); + } + + void EditorHeightfieldColliderComponent::InitStaticRigidBody() + { + // Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself + // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform + // back to us than the one that's directly on that entity. + AZ::Transform transform = AZ::Transform::CreateIdentity(); + Physics::HeightfieldProviderRequestsBus::EventResult( + transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform); + + AzPhysics::StaticRigidBodyConfiguration configuration; + configuration.m_orientation = transform.GetRotation(); + configuration.m_position = transform.GetTranslation(); + configuration.m_entityId = GetEntityId(); + configuration.m_debugName = GetEntity()->GetName(); + + AzPhysics::ShapeColliderPairList colliderShapePairs; + colliderShapePairs.emplace_back(AZStd::make_shared(m_colliderConfig), m_shapeConfig); + configuration.m_colliderAndShapeData = colliderShapePairs; + + if (m_sceneInterface) + { + m_staticRigidBodyHandle = m_sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration); + } + } + + void EditorHeightfieldColliderComponent::InitHeightfieldShapeConfiguration() + { + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig); + + Utils::InitHeightfieldShapeConfiguration(GetEntityId(), configuration); + } + + void EditorHeightfieldColliderComponent::RefreshHeightfield() + { + ClearHeightfield(); + InitHeightfieldShapeConfiguration(); + InitStaticRigidBody(); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + } + + AZ::u32 EditorHeightfieldColliderComponent::OnConfigurationChanged() + { + RefreshHeightfield(); + return AZ::Edit::PropertyRefreshLevels::None; + } + + // AzToolsFramework::EntitySelectionEvents + void EditorHeightfieldColliderComponent::OnSelected() + { + if (auto* physXSystem = GetPhysXSystem()) + { + if (!m_physXConfigChangedHandler.IsConnected()) + { + physXSystem->RegisterSystemConfigurationChangedEvent(m_physXConfigChangedHandler); + } + if (!m_onMaterialLibraryChangedEventHandler.IsConnected()) + { + physXSystem->RegisterOnMaterialLibraryChangedEventHandler(m_onMaterialLibraryChangedEventHandler); + } + } + } + + // AzToolsFramework::EntitySelectionEvents + void EditorHeightfieldColliderComponent::OnDeselected() + { + m_onMaterialLibraryChangedEventHandler.Disconnect(); + m_physXConfigChangedHandler.Disconnect(); + } + + // DisplayCallback + void EditorHeightfieldColliderComponent::Display(AzFramework::DebugDisplayRequests& debugDisplay) const + { + const auto& heightfieldConfig = static_cast(*m_shapeConfig); + m_colliderDebugDraw.DrawHeightfield(debugDisplay, m_colliderConfig, heightfieldConfig); + } + + // SimulatedBodyComponentRequestsBus + void EditorHeightfieldColliderComponent::EnablePhysics() + { + if (!IsPhysicsEnabled() && m_sceneInterface) + { + m_sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + void EditorHeightfieldColliderComponent::DisablePhysics() + { + if (m_sceneInterface) + { + m_sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + bool EditorHeightfieldColliderComponent::IsPhysicsEnabled() const + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->m_simulating; + } + } + return false; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBodyHandle EditorHeightfieldColliderComponent::GetSimulatedBodyHandle() const + { + return m_staticRigidBodyHandle; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBody* EditorHeightfieldColliderComponent::GetSimulatedBody() + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body; + } + } + return nullptr; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SceneQueryHit EditorHeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request) + { + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->RayCast(request); + } + } + return AzPhysics::SceneQueryHit(); + } + + // ColliderShapeRequestBus + AZ::Aabb EditorHeightfieldColliderComponent::GetColliderShapeAabb() + { + // Get the Collider AABB directly from the heightfield provider. + AZ::Aabb colliderAabb = AZ::Aabb::CreateNull(); + Physics::HeightfieldProviderRequestsBus::EventResult( + colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb); + + return colliderAabb; + } + + // SimulatedBodyComponentRequestsBus + AZ::Aabb EditorHeightfieldColliderComponent::GetAabb() const + { + // On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider. + if (m_sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* body = m_sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->GetAabb(); + } + } + return AZ::Aabb::CreateNull(); + } + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h new file mode 100644 index 0000000000..08b3ec5801 --- /dev/null +++ b/Gems/PhysX/Code/Source/EditorHeightfieldColliderComponent.h @@ -0,0 +1,104 @@ +/* + * 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 +#include +#include + +#include + +namespace PhysX +{ + //! Editor PhysX Heightfield Collider Component. + class EditorHeightfieldColliderComponent + : public AzToolsFramework::Components::EditorComponentBase + , protected AzToolsFramework::EntitySelectionEvents::Bus::Handler + , protected DebugDraw::DisplayCallback + , protected AzPhysics::SimulatedBodyComponentRequestsBus::Handler + , protected PhysX::ColliderShapeRequestBus::Handler + , protected Physics::HeightfieldProviderNotificationBus::Handler + { + public: + AZ_EDITOR_COMPONENT( + EditorHeightfieldColliderComponent, + "{C388C3DB-8D2E-4D26-96D3-198EDC799B77}", + AzToolsFramework::Components::EditorComponentBase); + static void Reflect(AZ::ReflectContext* context); + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + EditorHeightfieldColliderComponent(); + ~EditorHeightfieldColliderComponent(); + + // AZ::Component + void Activate() override; + void Deactivate() override; + + // EditorComponentBase + void BuildGameEntity(AZ::Entity* gameEntity) override; + + protected: + + // AzToolsFramework::EntitySelectionEvents + void OnSelected() override; + void OnDeselected() override; + + // DisplayCallback + void Display(AzFramework::DebugDisplayRequests& debugDisplay) const; + + // ColliderShapeRequestBus + AZ::Aabb GetColliderShapeAabb() override; + bool IsTrigger() override + { + // PhysX Heightfields don't support triggers. + return false; + } + + // AzPhysics::SimulatedBodyComponentRequestsBus::Handler overrides ... + void EnablePhysics() override; + void DisablePhysics() override; + bool IsPhysicsEnabled() const override; + AZ::Aabb GetAabb() const override; + AzPhysics::SimulatedBody* GetSimulatedBody() override; + AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override; + AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override; + + // Physics::HeightfieldProviderNotificationBus + void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override; + + private: + AZ::u32 OnConfigurationChanged(); + + void ClearHeightfield(); + void InitHeightfieldShapeConfiguration(); + void InitStaticRigidBody(); + void RefreshHeightfield(); + + DebugDraw::Collider m_colliderDebugDraw; //!< Handles drawing the collider + AzPhysics::SceneInterface* m_sceneInterface{ nullptr }; + + AzPhysics::SystemEvents::OnConfigurationChangedEvent::Handler m_physXConfigChangedHandler; + AzPhysics::SystemEvents::OnMaterialLibraryChangedEvent::Handler m_onMaterialLibraryChangedEventHandler; + + Physics::ColliderConfiguration m_colliderConfig; //!< Stores collision layers, whether the collider is a trigger, etc. + AZStd::shared_ptr m_shapeConfig{ new Physics::HeightfieldShapeConfiguration() }; + + AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = + AzPhysics::InvalidSimulatedBodyHandle; //!< Handle to the body in the editor physics scene if there is no rigid body component. + AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; + }; + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp new file mode 100644 index 0000000000..9bc209982d --- /dev/null +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.cpp @@ -0,0 +1,365 @@ +/* + * 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 +#include + +namespace PhysX +{ + void HeightfieldColliderComponent::Reflect(AZ::ReflectContext* context) + { + if (auto serializeContext = azrtti_cast(context)) + { + serializeContext->Class() + ->Version(1) + ->Field("ShapeConfig", &HeightfieldColliderComponent::m_shapeConfig) + ; + } + } + + void HeightfieldColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided) + { + provided.push_back(AZ_CRC_CE("PhysicsWorldBodyService")); + provided.push_back(AZ_CRC_CE("PhysXColliderService")); + provided.push_back(AZ_CRC_CE("PhysXHeightfieldColliderService")); + provided.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + } + + void HeightfieldColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required) + { + required.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void HeightfieldColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible) + { + incompatible.push_back(AZ_CRC_CE("PhysXColliderService")); + incompatible.push_back(AZ_CRC_CE("PhysXStaticRigidBodyService")); + incompatible.push_back(AZ_CRC_CE("PhysXRigidBodyService")); + } + + HeightfieldColliderComponent::~HeightfieldColliderComponent() + { + ClearHeightfield(); + } + + void HeightfieldColliderComponent::Activate() + { + const AZ::EntityId entityId = GetEntityId(); + + Physics::HeightfieldProviderNotificationBus::Handler::BusConnect(entityId); + ColliderComponentRequestBus::Handler::BusConnect(entityId); + ColliderShapeRequestBus::Handler::BusConnect(entityId); + Physics::CollisionFilteringRequestBus::Handler::BusConnect(entityId); + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusConnect(entityId); + + RefreshHeightfield(); + } + + void HeightfieldColliderComponent::Deactivate() + { + AzPhysics::SimulatedBodyComponentRequestsBus::Handler::BusDisconnect(); + Physics::CollisionFilteringRequestBus::Handler::BusDisconnect(); + ColliderShapeRequestBus::Handler::BusDisconnect(); + ColliderComponentRequestBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderNotificationBus::Handler::BusDisconnect(); + + ClearHeightfield(); + } + + void HeightfieldColliderComponent::OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) + { + RefreshHeightfield(); + } + + void HeightfieldColliderComponent::ClearHeightfield() + { + // There are two references to the heightfield data, we need to clear both to make the heightfield clear out and deallocate: + // - The simulated body has a pointer to the shape, which has a GeometryHolder, which has the Heightfield inside it + // - The shape config is also holding onto a pointer to the Heightfield + + // We remove the simulated body first, since we don't want the heightfield to exist any more. + if (auto* sceneInterface = AZ::Interface::Get(); + sceneInterface && m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + sceneInterface->RemoveSimulatedBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + + // Now we can safely clear out the cached heightfield pointer. + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); + configuration.SetCachedNativeHeightfield(nullptr); + } + + void HeightfieldColliderComponent::InitStaticRigidBody() + { + // Get the transform from the HeightfieldProvider. Because rotation and scale can indirectly affect how the heightfield itself + // is computed and the size of the heightfield, it's possible that the HeightfieldProvider will provide a different transform + // back to us than the one that's directly on that entity. + AZ::Transform transform = AZ::Transform::CreateIdentity(); + Physics::HeightfieldProviderRequestsBus::EventResult( + transform, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldTransform); + + AzPhysics::StaticRigidBodyConfiguration configuration; + configuration.m_orientation = transform.GetRotation(); + configuration.m_position = transform.GetTranslation(); + configuration.m_entityId = GetEntityId(); + configuration.m_debugName = GetEntity()->GetName(); + configuration.m_colliderAndShapeData = GetShapeConfigurations(); + + if (m_attachedSceneHandle == AzPhysics::InvalidSceneHandle) + { + Physics::DefaultWorldBus::BroadcastResult(m_attachedSceneHandle, &Physics::DefaultWorldRequests::GetDefaultSceneHandle); + } + if (auto* sceneInterface = AZ::Interface::Get()) + { + m_staticRigidBodyHandle = sceneInterface->AddSimulatedBody(m_attachedSceneHandle, &configuration); + } + } + + void HeightfieldColliderComponent::InitHeightfieldShapeConfiguration() + { + Physics::HeightfieldShapeConfiguration& configuration = static_cast(*m_shapeConfig.second); + + Utils::InitHeightfieldShapeConfiguration(GetEntityId(), configuration); + } + + void HeightfieldColliderComponent::RefreshHeightfield() + { + ClearHeightfield(); + InitHeightfieldShapeConfiguration(); + InitStaticRigidBody(); + Physics::ColliderComponentEventBus::Event(GetEntityId(), &Physics::ColliderComponentEvents::OnColliderChanged); + } + + void HeightfieldColliderComponent::SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig) + { + if (GetEntity()->GetState() == AZ::Entity::State::Active) + { + AZ_Warning( + "PhysX", false, "Trying to call SetShapeConfiguration for entity \"%s\" while entity is active.", + GetEntity()->GetName().c_str()); + return; + } + m_shapeConfig = shapeConfig; + } + + // SimulatedBodyComponentRequestsBus + void HeightfieldColliderComponent::EnablePhysics() + { + if (IsPhysicsEnabled()) + { + return; + } + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->EnableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + void HeightfieldColliderComponent::DisablePhysics() + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + sceneInterface->DisableSimulationOfBody(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + } + + // SimulatedBodyComponentRequestsBus + bool HeightfieldColliderComponent::IsPhysicsEnabled() const + { + if (m_staticRigidBodyHandle != AzPhysics::InvalidSimulatedBodyHandle) + { + if (auto* sceneInterface = AZ::Interface::Get(); + sceneInterface != nullptr && sceneInterface->IsEnabled(m_attachedSceneHandle)) // check if the scene is enabled + { + if (AzPhysics::SimulatedBody* body = + sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->m_simulating; + } + } + } + return false; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBodyHandle HeightfieldColliderComponent::GetSimulatedBodyHandle() const + { + return m_staticRigidBodyHandle; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SimulatedBody* HeightfieldColliderComponent::GetSimulatedBody() + { + if (auto* sceneInterface = AZ::Interface::Get()) + { + return sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle); + } + return nullptr; + } + + // SimulatedBodyComponentRequestsBus + AzPhysics::SceneQueryHit HeightfieldColliderComponent::RayCast(const AzPhysics::RayCastRequest& request) + { + if (auto* body = azdynamic_cast(GetSimulatedBody())) + { + return body->RayCast(request); + } + return AzPhysics::SceneQueryHit(); + } + + // ColliderComponentRequestBus + AzPhysics::ShapeColliderPairList HeightfieldColliderComponent::GetShapeConfigurations() + { + AzPhysics::ShapeColliderPairList shapeConfigurationList({ m_shapeConfig }); + return shapeConfigurationList; + } + + AZStd::shared_ptr HeightfieldColliderComponent::GetHeightfieldShape() + { + if (auto* body = azdynamic_cast(GetSimulatedBody())) + { + // Heightfields should only have one shape + AZ_Assert(body->GetShapeCount() == 1, "Heightfield rigid body has the wrong number of shapes: %zu", body->GetShapeCount()); + return body->GetShape(0); + } + + return {}; + } + + // ColliderComponentRequestBus + AZStd::vector> HeightfieldColliderComponent::GetShapes() + { + return { GetHeightfieldShape() }; + } + + // PhysX::ColliderShapeBus + AZ::Aabb HeightfieldColliderComponent::GetColliderShapeAabb() + { + // Get the Collider AABB directly from the heightfield provider. + AZ::Aabb colliderAabb = AZ::Aabb::CreateNull(); + Physics::HeightfieldProviderRequestsBus::EventResult( + colliderAabb, GetEntityId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldAabb); + + return colliderAabb; + } + + // SimulatedBodyComponentRequestsBus + AZ::Aabb HeightfieldColliderComponent::GetAabb() const + { + // On the SimulatedBodyComponentRequestsBus, get the AABB from the simulated body instead of the collider. + if (auto* sceneInterface = AZ::Interface::Get()) + { + if (AzPhysics::SimulatedBody* body = sceneInterface->GetSimulatedBodyFromHandle(m_attachedSceneHandle, m_staticRigidBodyHandle)) + { + return body->GetAabb(); + } + } + return AZ::Aabb::CreateNull(); + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionLayer layer; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer); + if (success) + { + heightfield->SetCollisionLayer(layer); + } + } + } + } + + // CollisionFilteringRequestBus + AZStd::string HeightfieldColliderComponent::GetCollisionLayerName() + { + AZStd::string layerName; + if (auto heightfield = GetHeightfieldShape()) + { + Physics::CollisionRequestBus::BroadcastResult( + layerName, &Physics::CollisionRequests::GetCollisionLayerName, heightfield->GetCollisionLayer()); + } + return layerName; + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 colliderTag) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionGroup group; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionGroupByName, groupName, group); + if (success) + { + heightfield->SetCollisionGroup(group); + } + } + } + } + + // CollisionFilteringRequestBus + AZStd::string HeightfieldColliderComponent::GetCollisionGroupName() + { + AZStd::string groupName; + if (auto heightfield = GetHeightfieldShape()) + { + Physics::CollisionRequestBus::BroadcastResult( + groupName, &Physics::CollisionRequests::GetCollisionGroupName, heightfield->GetCollisionGroup()); + } + + return groupName; + } + + // CollisionFilteringRequestBus + void HeightfieldColliderComponent::ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 colliderTag, bool enabled) + { + if (auto heightfield = GetHeightfieldShape()) + { + if (Physics::Utils::FilterTag(heightfield->GetTag(), colliderTag)) + { + bool success = false; + AzPhysics::CollisionLayer layer; + Physics::CollisionRequestBus::BroadcastResult( + success, &Physics::CollisionRequests::TryGetCollisionLayerByName, layerName, layer); + if (success) + { + auto group = heightfield->GetCollisionGroup(); + group.SetLayer(layer, enabled); + heightfield->SetCollisionGroup(group); + } + } + } + } + +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h new file mode 100644 index 0000000000..3213f7a774 --- /dev/null +++ b/Gems/PhysX/Code/Source/HeightfieldColliderComponent.h @@ -0,0 +1,104 @@ +/* + * 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 +#include + +namespace AzPhysics +{ + struct SimulatedBody; +} + +namespace PhysX +{ + class StaticRigidBody; + + //! Component that provides a Heightfield Collider and associated Static Rigid Body. + //! The heightfield collider is a bit different from the other shape colliders in that it gets the heightfield data from a + //! HeightfieldProvider, which can control position, rotation, size, and even change its data at runtime. + //! + //! Due to these differences, this component directly implements both the collider and static rigid body services instead of + //! using BaseColliderComponent and StaticRigidBodyComponent. + class HeightfieldColliderComponent + : public AZ::Component + , public ColliderComponentRequestBus::Handler + , public AzPhysics::SimulatedBodyComponentRequestsBus::Handler + , protected PhysX::ColliderShapeRequestBus::Handler + , protected Physics::CollisionFilteringRequestBus::Handler + , protected Physics::HeightfieldProviderNotificationBus::Handler + { + public: + using Configuration = Physics::HeightfieldShapeConfiguration; + AZ_COMPONENT(HeightfieldColliderComponent, "{9A42672C-281A-4CE8-BFDD-EAA1E0FCED76}"); + static void Reflect(AZ::ReflectContext* context); + + HeightfieldColliderComponent() = default; + ~HeightfieldColliderComponent() override; + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& provided); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& required); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& incompatible); + + void Activate() override; + void Deactivate() override; + + void SetShapeConfiguration(const AzPhysics::ShapeColliderPair& shapeConfig); + + protected: + // ColliderComponentRequestBus + AzPhysics::ShapeColliderPairList GetShapeConfigurations() override; + AZStd::vector> GetShapes() override; + + // ColliderShapeRequestBus + AZ::Aabb GetColliderShapeAabb() override; + bool IsTrigger() override + { + // PhysX Heightfields don't support triggers. + return false; + } + + // CollisionFilteringRequestBus + void SetCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag) override; + AZStd::string GetCollisionLayerName() override; + void SetCollisionGroup(const AZStd::string& groupName, AZ::Crc32 filterTag) override; + AZStd::string GetCollisionGroupName() override; + void ToggleCollisionLayer(const AZStd::string& layerName, AZ::Crc32 filterTag, bool enabled) override; + + // SimulatedBodyComponentRequestsBus + void EnablePhysics() override; + void DisablePhysics() override; + bool IsPhysicsEnabled() const override; + AZ::Aabb GetAabb() const override; + AzPhysics::SimulatedBodyHandle GetSimulatedBodyHandle() const override; + AzPhysics::SimulatedBody* GetSimulatedBody() override; + AzPhysics::SceneQueryHit RayCast(const AzPhysics::RayCastRequest& request) override; + + // HeightfieldProviderNotificationBus + void OnHeightfieldDataChanged([[maybe_unused]] const AZ::Aabb& dirtyRegion) override; + + private: + AZStd::shared_ptr GetHeightfieldShape(); + + void ClearHeightfield(); + void InitHeightfieldShapeConfiguration(); + void InitStaticRigidBody(); + void RefreshHeightfield(); + + AzPhysics::ShapeColliderPair m_shapeConfig; + AzPhysics::SimulatedBodyHandle m_staticRigidBodyHandle = AzPhysics::InvalidSimulatedBodyHandle; + AzPhysics::SceneHandle m_attachedSceneHandle = AzPhysics::InvalidSceneHandle; + }; +} // namespace PhysX diff --git a/Gems/PhysX/Code/Source/SystemComponent.cpp b/Gems/PhysX/Code/Source/SystemComponent.cpp index 2f4d082ccb..000d69d982 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.cpp +++ b/Gems/PhysX/Code/Source/SystemComponent.cpp @@ -252,6 +252,22 @@ namespace PhysX return convex; } + physx::PxHeightField* SystemComponent::CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) + { + physx::PxHeightFieldDesc desc; + desc.format = physx::PxHeightFieldFormat::eS16_TM; + desc.nbColumns = numColumns; + desc.nbRows = numRows; + desc.samples.data = samples; + desc.samples.stride = sizeof(physx::PxHeightFieldSample); + + physx::PxHeightField* heightfield = + m_physXSystem->GetPxCooking()->createHeightField(desc, m_physXSystem->GetPxPhysics()->getPhysicsInsertionCallback()); + AZ_Error("PhysX", heightfield, "Error. Unable to create heightfield"); + + return heightfield; + } + bool SystemComponent::CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) { AZStd::vector physxData; @@ -342,6 +358,14 @@ namespace PhysX return AZStd::make_shared(materialConfiguration); } + void SystemComponent::ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) + { + if (nativeHeightfieldObject) + { + static_cast(nativeHeightfieldObject)->release(); + } + } + void SystemComponent::ReleaseNativeMeshObject(void* nativeMeshObject) { if (nativeMeshObject) diff --git a/Gems/PhysX/Code/Source/SystemComponent.h b/Gems/PhysX/Code/Source/SystemComponent.h index 3661655bb3..d581af7e67 100644 --- a/Gems/PhysX/Code/Source/SystemComponent.h +++ b/Gems/PhysX/Code/Source/SystemComponent.h @@ -76,6 +76,7 @@ namespace PhysX physx::PxConvexMesh* CreateConvexMesh(const void* vertices, AZ::u32 vertexNum, AZ::u32 vertexStride) override; // should we use AZ::Vector3* or physx::PxVec3 here? physx::PxConvexMesh* CreateConvexMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override; physx::PxTriangleMesh* CreateTriangleMeshFromCooked(const void* cookedMeshData, AZ::u32 bufferSize) override; + physx::PxHeightField* CreateHeightField(const physx::PxHeightFieldSample* samples, AZ::u32 numRows, AZ::u32 numColumns) override; bool CookConvexMeshToFile(const AZStd::string& filePath, const AZ::Vector3* vertices, AZ::u32 vertexCount) override; @@ -112,6 +113,7 @@ namespace PhysX AZStd::shared_ptr CreateMaterial(const Physics::MaterialConfiguration& materialConfiguration) override; void ReleaseNativeMeshObject(void* nativeMeshObject) override; + void ReleaseNativeHeightfieldObject(void* nativeHeightfieldObject) override; // Assets related data AZStd::vector> m_assetHandlers; diff --git a/Gems/PhysX/Code/Source/Utils.cpp b/Gems/PhysX/Code/Source/Utils.cpp index 8a76d6d986..72e17aff70 100644 --- a/Gems/PhysX/Code/Source/Utils.cpp +++ b/Gems/PhysX/Code/Source/Utils.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include @@ -64,6 +66,136 @@ namespace PhysX } } + void CreatePxGeometryFromHeightfield( + const Physics::HeightfieldShapeConfiguration& heightfieldConfig, physx::PxGeometryHolder& pxGeometry) + { + physx::PxHeightField* heightfield = nullptr; + + const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution(); + + const int32_t numCols = heightfieldConfig.GetNumColumns(); + const int32_t numRows = heightfieldConfig.GetNumRows(); + + const float rowScale = gridSpacing.GetX(); + const float colScale = gridSpacing.GetY(); + + const float minHeightBounds = heightfieldConfig.GetMinHeightBounds(); + const float maxHeightBounds = heightfieldConfig.GetMaxHeightBounds(); + const float halfBounds{ (maxHeightBounds - minHeightBounds) / 2.0f }; + + // We're making the assumption right now that the min/max bounds are centered around 0. + // If we ever want to allow off-center bounds, we'll need to fix up the float-to-int16 height math below + // to account for it. + AZ_Assert( + AZ::IsClose(-halfBounds, minHeightBounds) && AZ::IsClose(halfBounds, maxHeightBounds), + "Min/Max height bounds aren't centered around 0, the height conversions below will be incorrect."); + + AZ_Assert( + maxHeightBounds >= minHeightBounds, + "Max height bounds is less than min height bounds, the height conversions below will be incorrect."); + + // To convert our floating-point heights to fixed-point representation inside of an int16, we need a scale factor + // for the conversion. The scale factor is used to map the most important bits of our floating-point height to the + // full 16-bit range. + // Note that the scaleFactor choice here affects overall precision. For each bit that the integer part of our max + // height uses, that's one less bit for the fractional part. + const float scaleFactor = (maxHeightBounds <= minHeightBounds) ? 1.0f : AZStd::numeric_limits::max() / halfBounds; + const float heightScale{ 1.0f / scaleFactor }; + + const 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); + + const AZStd::vector& samples = heightfieldConfig.GetSamples(); + AZ_Assert(samples.size() == numRows * numCols, "GetHeightsAndMaterials returned wrong sized heightfield"); + + if (!samples.empty()) + { + AZStd::vector physxSamples(samples.size()); + + for (int32_t row = 0; row < numRows; row++) + { + const bool lastRowIndex = (row == (numRows - 1)); + + for (int32_t col = 0; col < numCols; col++) + { + const bool lastColumnIndex = (col == (numCols - 1)); + + auto GetIndex = [numCols](int32_t row, int32_t col) + { + return (row * numCols) + col; + }; + + const int32_t sampleIndex = GetIndex(row, col); + + const Physics::HeightMaterialPoint& currentSample = samples[sampleIndex]; + physx::PxHeightFieldSample& currentPhysxSample = physxSamples[sampleIndex]; + AZ_Assert(currentSample.m_materialIndex < physxMaximumMaterialIndex, "MaterialIndex must be less than 128"); + currentPhysxSample.height = azlossy_cast( + AZ::GetClamp(currentSample.m_height, minHeightBounds, maxHeightBounds) * scaleFactor); + if (lastRowIndex || lastColumnIndex) + { + // In PhysX, the material indices refer to the quad down and to the right of the sample. + // If we're in the last row or last column, there aren't any quads down or to the right, + // so just clear these out. + currentPhysxSample.materialIndex0 = 0; + currentPhysxSample.materialIndex1 = 0; + } + else + { + // Our source data is providing one material index per vertex, but PhysX wants one material index + // per triangle. The heuristic that we'll go with for selecting the material index is to choose + // the material for the vertex that's not on the diagonal of each triangle. + // Ex: A *---* B + // | / | For this, we'll use A for index0 and D for index1. + // C *---* D + // + // Ex: A *---* B + // | \ | For this, we'll use C for index0 and B for index1. + // C *---* D + // + // This is a pretty arbitrary choice, so the heuristic might need to be revisited over time if this + // causes incorrect or unpredictable physics material mappings. + + switch (currentSample.m_quadMeshType) + { + case Physics::QuadMeshType::SubdivideUpperLeftToBottomRight: + currentPhysxSample.materialIndex0 = samples[GetIndex(row + 1, col)].m_materialIndex; + currentPhysxSample.materialIndex1 = samples[GetIndex(row, col + 1)].m_materialIndex; + // Set the tesselation flag to say that we need to go from UL to BR + currentPhysxSample.materialIndex0.setBit(); + break; + case Physics::QuadMeshType::SubdivideBottomLeftToUpperRight: + currentPhysxSample.materialIndex0 = currentSample.m_materialIndex; + currentPhysxSample.materialIndex1 = samples[GetIndex(row + 1, col + 1)].m_materialIndex; + break; + case Physics::QuadMeshType::Hole: + currentPhysxSample.materialIndex0 = physx::PxHeightFieldMaterial::eHOLE; + currentPhysxSample.materialIndex1 = physx::PxHeightFieldMaterial::eHOLE; + break; + default: + AZ_Warning("PhysX Heightfield", false, "Unhandled case in CreatePxGeometryFromConfig"); + currentPhysxSample.materialIndex0 = 0; + currentPhysxSample.materialIndex1 = 0; + break; + } + } + } + } + + SystemRequestsBus::BroadcastResult(heightfield, &SystemRequests::CreateHeightField, physxSamples.data(), numRows, numCols); + } + if (heightfield) + { + heightfieldConfig.SetCachedNativeHeightfield(heightfield); + + physx::PxHeightFieldGeometry hfGeom(heightfield, physx::PxMeshGeometryFlags(), heightScale, rowScale, colScale); + + pxGeometry.storeAny(hfGeom); + } + } + bool CreatePxGeometryFromConfig(const Physics::ShapeConfiguration& shapeConfiguration, physx::PxGeometryHolder& pxGeometry) { if (!shapeConfiguration.m_scale.IsGreaterThan(AZ::Vector3::CreateZero())) @@ -170,6 +302,14 @@ namespace PhysX "Please iterate over m_colliderShapes in the asset and call this function for each of them."); return false; } + case Physics::ShapeType::Heightfield: + { + const Physics::HeightfieldShapeConfiguration& heightfieldConfig = + static_cast(shapeConfiguration); + + CreatePxGeometryFromHeightfield(heightfieldConfig, pxGeometry); + break; + } default: AZ_Warning("PhysX Rigid Body", false, "Shape not supported in PhysX. Shape Type: %d", shapeType); return false; @@ -219,6 +359,26 @@ namespace PhysX physx::PxQuat pxQuat(AZ::Constants::HalfPi, physx::PxVec3(0.0f, 1.0f, 0.0f)); shape->setLocalPose(physx::PxTransform(pxQuat)); } + else if (pxGeomHolder.getType() == physx::PxGeometryType::eHEIGHTFIELD) + { + const Physics::HeightfieldShapeConfiguration& heightfieldConfig = + static_cast(shapeConfiguration); + + // PhysX heightfields have the origin at the corner, not the center, so add an offset to the passed-in transform + // to account for this difference. + const AZ::Vector2 gridSpacing = heightfieldConfig.GetGridResolution(); + AZ::Vector3 offset( + -(gridSpacing.GetX() * heightfieldConfig.GetNumColumns() / 2.0f), + -(gridSpacing.GetY() * heightfieldConfig.GetNumRows() / 2.0f), + 0.0f); + + // PhysX heightfields are always defined to have the height in the Y direction, not the Z direction, so we need + // to provide additional rotations to make it Z-up. + physx::PxQuat pxQuat = PxMathConvert( + AZ::Quaternion::CreateFromEulerAnglesRadians(AZ::Vector3(AZ::Constants::HalfPi, AZ::Constants::HalfPi, 0.0f))); + physx::PxTransform pxHeightfieldTransform = physx::PxTransform(PxMathConvert(offset), pxQuat); + shape->setLocalPose(pxHeightfieldTransform); + } // Handle a possible misconfiguration when a shape is set to be both simulated & trigger. This is illegal in PhysX. shape->setFlag(physx::PxShapeFlag::eSIMULATION_SHAPE, colliderConfiguration.m_isSimulated && !colliderConfiguration.m_isTrigger); @@ -1357,6 +1517,39 @@ namespace PhysX return entityWorldTransformWithoutScale * jointLocalTransformWithoutScale; } + + void InitHeightfieldShapeConfiguration(AZ::EntityId entityId, Physics::HeightfieldShapeConfiguration& configuration) + { + configuration = Physics::HeightfieldShapeConfiguration(); + + AZ::Vector2 gridSpacing(1.0f); + Physics::HeightfieldProviderRequestsBus::EventResult( + gridSpacing, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSpacing); + + configuration.SetGridResolution(gridSpacing); + + int32_t numRows = 0; + int32_t numColumns = 0; + Physics::HeightfieldProviderRequestsBus::Event( + entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, numColumns, numRows); + + configuration.SetNumRows(numRows); + configuration.SetNumColumns(numColumns); + + float minHeightBounds = 0.0f; + float maxHeightBounds = 0.0f; + Physics::HeightfieldProviderRequestsBus::Event( + entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldHeightBounds, minHeightBounds, maxHeightBounds); + + configuration.SetMinHeightBounds(minHeightBounds); + configuration.SetMaxHeightBounds(maxHeightBounds); + + AZStd::vector samples; + Physics::HeightfieldProviderRequestsBus::EventResult( + samples, entityId, &Physics::HeightfieldProviderRequestsBus::Events::GetHeightsAndMaterials); + + configuration.SetSamples(samples); + } } // namespace Utils namespace ReflectionUtils diff --git a/Gems/PhysX/Code/Source/Utils.h b/Gems/PhysX/Code/Source/Utils.h index ed63605bc8..54a81bb28d 100644 --- a/Gems/PhysX/Code/Source/Utils.h +++ b/Gems/PhysX/Code/Source/Utils.h @@ -188,6 +188,8 @@ namespace PhysX //! Returns defaultValue if the input is infinite or NaN, otherwise returns the input unchanged. const AZ::Vector3& Sanitize(const AZ::Vector3& input, const AZ::Vector3& defaultValue = AZ::Vector3::CreateZero()); + void InitHeightfieldShapeConfiguration(AZ::EntityId entityId, Physics::HeightfieldShapeConfiguration& configuration); + namespace Geometry { using PointList = AZStd::vector; diff --git a/Gems/PhysX/Code/physx_editor_files.cmake b/Gems/PhysX/Code/physx_editor_files.cmake index 5c0c038976..1b22c0ce99 100644 --- a/Gems/PhysX/Code/physx_editor_files.cmake +++ b/Gems/PhysX/Code/physx_editor_files.cmake @@ -27,6 +27,8 @@ set(FILES Source/EditorFixedJointComponent.h Source/EditorHingeJointComponent.cpp Source/EditorHingeJointComponent.h + Source/EditorHeightfieldColliderComponent.cpp + Source/EditorHeightfieldColliderComponent.h Source/EditorJointComponent.cpp Source/EditorJointComponent.h Source/Pipeline/MeshExporter.cpp diff --git a/Gems/PhysX/Code/physx_files.cmake b/Gems/PhysX/Code/physx_files.cmake index 654f1d9767..efefaf3105 100644 --- a/Gems/PhysX/Code/physx_files.cmake +++ b/Gems/PhysX/Code/physx_files.cmake @@ -35,6 +35,8 @@ set(FILES Source/MeshColliderComponent.h Source/BoxColliderComponent.h Source/BoxColliderComponent.cpp + Source/HeightfieldColliderComponent.h + Source/HeightfieldColliderComponent.cpp Source/SphereColliderComponent.h Source/SphereColliderComponent.cpp Source/CapsuleColliderComponent.h diff --git a/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py b/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py index 6b9fbc8f08..758cc539d7 100755 --- a/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py +++ b/Gems/QtForPython/Editor/Scripts/az_qt_helpers.py @@ -18,6 +18,14 @@ from shiboken2 import wrapInstance, getCppPointer view_pane_handlers = {} registration_handlers = {} +# Helper method for retrieving the Editor QMainWindow instance +def get_editor_main_window(): + params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters") + editor_id = QtWidgets.QWidget.find(params.mainWindowId) + editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow) + + return editor_main_window + # Helper method for registering a Python widget as a tool/view pane with the Editor def register_view_pane(name, widget_type, options=editor.ViewPaneOptions()): global view_pane_handlers @@ -29,9 +37,7 @@ def register_view_pane(name, widget_type, options=editor.ViewPaneOptions()): # This method will be invoked by the ViewPaneCallbackBus::CreateViewPaneWidget # when our view pane needs to be created def on_create_view_pane_widget(parameters): - params = azlmbr.qt.QtForPythonRequestBus(azlmbr.bus.Broadcast, "GetQtBootstrapParameters") - editor_id = QtWidgets.QWidget.find(params.mainWindowId) - editor_main_window = wrapInstance(int(getCppPointer(editor_id)[0]), QtWidgets.QMainWindow) + editor_main_window = get_editor_main_window() dock_main_window = editor_main_window.findChild(QtWidgets.QMainWindow) # Create the view pane widget parented to the Editor QMainWindow, so it can be found diff --git a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype index ec04412fe6..01b862c4f8 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/PbrTerrain.materialtype @@ -1,7 +1,7 @@ { "description": "A material for rendering terrain with a physically-based rendering (PBR) material shading model.", + "version": 1, "propertyLayout": { - "version": 1, "groups": [ { "id": "settings", diff --git a/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype b/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype index 3cdab8da10..17769ffb92 100644 --- a/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype +++ b/Gems/Terrain/Assets/Materials/Terrain/TerrainMacroMaterial.materialtype @@ -1,7 +1,7 @@ { "description": "A material for providing terrain with low-fidelity color and normals. This material will get blended with surface detail materials.", + "version": 1, "propertyLayout": { - "version": 1, "groups": [ { "name": "baseColor", diff --git a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl index 91e367500b..750cd2fb29 100644 --- a/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl +++ b/Gems/Terrain/Assets/Shaders/Terrain/TerrainPBR_ForwardPass.azsl @@ -59,6 +59,7 @@ VSOutput TerrainPBR_MainPassVS(VertexInput IN) DirectionalLightShadow::GetShadowCoords( shadowIndex, worldPosition, + OUT.m_normal, OUT.m_shadowCoords); } diff --git a/Gems/Terrain/Code/CMakeLists.txt b/Gems/Terrain/Code/CMakeLists.txt index 442093a885..4b2f32e172 100644 --- a/Gems/Terrain/Code/CMakeLists.txt +++ b/Gems/Terrain/Code/CMakeLists.txt @@ -111,6 +111,7 @@ if(PAL_TRAIT_BUILD_TESTS_SUPPORTED) AZ::AzTest AZ::AzFramework Gem::LmbrCentral.Mocks + Gem::GradientSignal.Mocks Gem::Terrain.Mocks Gem::Terrain.Static ) diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h index ea0413be9a..bb1fa5683a 100644 --- a/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrain.h @@ -10,11 +10,12 @@ #include #include +#include #include +#include namespace UnitTest { - class MockTerrainSystemService : private Terrain::TerrainSystemServiceRequestBus::Handler { public: @@ -69,11 +70,7 @@ namespace UnitTest Terrain::TerrainAreaHeightRequestBus::Handler::BusDisconnect(); } - MOCK_METHOD3(GetHeight, void( - const AZ::Vector3& inPosition, - AZ::Vector3& outPosition, - bool& terrainExists)); - + MOCK_METHOD3(GetHeight, void(const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists)); }; class MockTerrainSpawnerRequests : public Terrain::TerrainSpawnerRequestBus::Handler @@ -92,4 +89,35 @@ namespace UnitTest MOCK_METHOD2(GetPriority, void(AZ::u32& outLayer, AZ::u32& outPriority)); MOCK_METHOD0(GetUseGroundPlane, bool()); }; -} + + class MockTerrainDataRequests : public AzFramework::Terrain::TerrainDataRequestBus::Handler + { + public: + MockTerrainDataRequests() + { + AzFramework::Terrain::TerrainDataRequestBus::Handler::BusConnect(); + } + + ~MockTerrainDataRequests() + { + AzFramework::Terrain::TerrainDataRequestBus::Handler::BusDisconnect(); + } + + MOCK_CONST_METHOD0(GetTerrainHeightQueryResolution, AZ::Vector2()); + MOCK_METHOD1(SetTerrainHeightQueryResolution, void(AZ::Vector2)); + MOCK_CONST_METHOD0(GetTerrainAabb, AZ::Aabb()); + MOCK_METHOD1(SetTerrainAabb, void(const AZ::Aabb&)); + MOCK_CONST_METHOD3(GetHeight, float(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD4(GetHeightFromFloats, float(float, float, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceWeight, AzFramework::SurfaceData::SurfaceTagWeight(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceWeightFromVector2, AzFramework::SurfaceData::SurfaceTagWeight(const AZ::Vector2&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetMaxSurfaceWeightFromFloats, AzFramework::SurfaceData::SurfaceTagWeight(float, float, Sampler, bool*)); + MOCK_CONST_METHOD4(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*)); + MOCK_CONST_METHOD4(GetSurfaceWeightsFromVector2, void(const AZ::Vector2&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*)); + MOCK_CONST_METHOD5(GetSurfaceWeightsFromFloats, void(float, float, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&, Sampler, bool*)); + MOCK_CONST_METHOD3(GetMaxSurfaceName, const char*(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD3(GetIsHoleFromFloats, bool(float, float, Sampler)); + MOCK_CONST_METHOD3(GetNormal, AZ::Vector3(AZ::Vector3, Sampler, bool*)); + MOCK_CONST_METHOD4(GetNormalFromFloats, AZ::Vector3(float, float, Sampler, bool*)); + }; +} // namespace UnitTest diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h new file mode 100644 index 0000000000..c5a04b4265 --- /dev/null +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h @@ -0,0 +1,34 @@ +/* + * 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 + +namespace UnitTest +{ + class MockTerrainAreaSurfaceRequestBus : public Terrain::TerrainAreaSurfaceRequestBus::Handler + { + public: + MockTerrainAreaSurfaceRequestBus(AZ::EntityId entityId) + { + Terrain::TerrainAreaSurfaceRequestBus::Handler::BusConnect(entityId); + } + + ~MockTerrainAreaSurfaceRequestBus() + { + Terrain::TerrainAreaSurfaceRequestBus::Handler::BusDisconnect(); + } + + MOCK_METHOD0(Activate, void()); + MOCK_METHOD0(Deactivate, void()); + MOCK_CONST_METHOD2(GetSurfaceWeights, void(const AZ::Vector3&, AzFramework::SurfaceData::OrderedSurfaceTagWeightSet&)); + }; + +} // namespace UnitTest diff --git a/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h new file mode 100644 index 0000000000..d0551d0881 --- /dev/null +++ b/Gems/Terrain/Code/Mocks/Terrain/MockTerrainLayerSpawner.h @@ -0,0 +1,39 @@ +/* + * 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 UnitTest +{ + class MockTerrainLayerSpawnerComponent + : public AZ::Component + { + public: + AZ_COMPONENT(MockTerrainLayerSpawnerComponent, "{9F27C980-9826-4063-86D8-E981C1E842A3}"); + + static void Reflect([[maybe_unused]] AZ::ReflectContext* context) + { + } + + void Activate() override + { + } + + void Deactivate() override + { + } + + private: + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("TerrainAreaService")); + } + }; + +} //namespace UnitTest diff --git a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp index 4ea16018d1..01ad0bb77f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainHeightGradientListComponent.cpp @@ -65,7 +65,7 @@ namespace Terrain void TerrainHeightGradientListComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) { services.push_back(AZ_CRC_CE("TerrainAreaService")); - services.push_back(AZ_CRC_CE("BoxShapeService")); + services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); } void TerrainHeightGradientListComponent::Reflect(AZ::ReflectContext* context) diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp index 245c98ccac..00ed9c004f 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.cpp @@ -102,7 +102,6 @@ namespace Terrain void TerrainLayerSpawnerComponent::Activate() { - AZ::TransformNotificationBus::Handler::BusConnect(GetEntityId()); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(GetEntityId()); TerrainSpawnerRequestBus::Handler::BusConnect(GetEntityId()); @@ -114,8 +113,6 @@ namespace Terrain TerrainSystemServiceRequestBus::Broadcast(&TerrainSystemServiceRequestBus::Events::UnregisterArea, GetEntityId()); TerrainSpawnerRequestBus::Handler::BusDisconnect(); LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); - AZ::TransformNotificationBus::Handler::BusDisconnect(); - } bool TerrainLayerSpawnerComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) @@ -138,13 +135,12 @@ namespace Terrain return false; } - void TerrainLayerSpawnerComponent::OnTransformChanged([[maybe_unused]] const AZ::Transform& local, [[maybe_unused]] const AZ::Transform& world) - { - RefreshArea(); - } - void TerrainLayerSpawnerComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason) { + // This will notify us of both shape changes and transform changes. + // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee + // the shape has received the transform change message and updated its internal state before passing it along to us. + RefreshArea(); } diff --git a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h index c7398bf93e..683f9e9f06 100644 --- a/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h +++ b/Gems/Terrain/Code/Source/Components/TerrainLayerSpawnerComponent.h @@ -18,7 +18,6 @@ #include #include -#include #include #include @@ -56,7 +55,6 @@ namespace Terrain class TerrainLayerSpawnerComponent : public AZ::Component - , private AZ::TransformNotificationBus::Handler , private LmbrCentral::ShapeComponentNotificationsBus::Handler , private Terrain::TerrainSpawnerRequestBus::Handler { @@ -81,10 +79,6 @@ namespace Terrain bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; protected: - ////////////////////////////////////////////////////////////////////////// - // AZ::TransformNotificationBus::Handler - void OnTransformChanged(const AZ::Transform& local, const AZ::Transform& world) override; - // ShapeComponentNotificationsBus void OnShapeChanged(ShapeChangeReasons changeReason) override; diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp new file mode 100644 index 0000000000..e9c235c1bd --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.cpp @@ -0,0 +1,321 @@ +/* + * 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 + +namespace Terrain +{ + void TerrainPhysicsColliderConfig::Reflect(AZ::ReflectContext* context) + { + if (auto serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(1) + ; + + if (auto edit = serialize->GetEditContext()) + { + edit->Class( + "Terrain Physics Collider Component", + "Provides terrain data to a physics collider with configurable surface mappings.") + ->ClassElement(AZ::Edit::ClassElements::EditorData, "") + ->Attribute(AZ::Edit::Attributes::Visibility, AZ::Edit::PropertyVisibility::ShowChildrenOnly) + ->Attribute(AZ::Edit::Attributes::AutoExpand, true); + } + } + } + + void TerrainPhysicsColliderComponent::GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void TerrainPhysicsColliderComponent::GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("PhysicsHeightfieldProviderService")); + } + + void TerrainPhysicsColliderComponent::GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services) + { + services.push_back(AZ_CRC_CE("AxisAlignedBoxShapeService")); + } + + void TerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context) + { + TerrainPhysicsColliderConfig::Reflect(context); + + if (auto serialize = azrtti_cast(context)) + { + serialize->Class() + ->Version(0) + ->Field("Configuration", &TerrainPhysicsColliderComponent::m_configuration) + ; + } + } + + TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration) + : m_configuration(configuration) + { + } + + TerrainPhysicsColliderComponent::TerrainPhysicsColliderComponent() + { + + } + + void TerrainPhysicsColliderComponent::Activate() + { + const auto entityId = GetEntityId(); + LmbrCentral::ShapeComponentNotificationsBus::Handler::BusConnect(entityId); + Physics::HeightfieldProviderRequestsBus::Handler::BusConnect(entityId); + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusConnect(); + + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::Deactivate() + { + AzFramework::Terrain::TerrainDataNotificationBus::Handler::BusDisconnect(); + Physics::HeightfieldProviderRequestsBus::Handler ::BusDisconnect(); + LmbrCentral::ShapeComponentNotificationsBus::Handler::BusDisconnect(); + } + + bool TerrainPhysicsColliderComponent::ReadInConfig(const AZ::ComponentConfig* baseConfig) + { + if (auto config = azrtti_cast(baseConfig)) + { + m_configuration = *config; + return true; + } + return false; + } + + bool TerrainPhysicsColliderComponent::WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const + { + if (auto config = azrtti_cast(outBaseConfig)) + { + *config = m_configuration; + return true; + } + return false; + } + + void TerrainPhysicsColliderComponent::NotifyListenersOfHeightfieldDataChange() + { + AZ::Aabb worldSize = AZ::Aabb::CreateNull(); + + LmbrCentral::ShapeComponentRequestsBus::EventResult( + worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + + Physics::HeightfieldProviderNotificationBus::Broadcast( + &Physics::HeightfieldProviderNotificationBus::Events::OnHeightfieldDataChanged, worldSize); + } + + void TerrainPhysicsColliderComponent::OnShapeChanged([[maybe_unused]] ShapeChangeReasons changeReason) + { + // This will notify us of both shape changes and transform changes. + // It's important to use this event for transform changes instead of listening to OnTransformChanged, because we need to guarantee + // the shape has received the transform change message and updated its internal state before passing it along to us. + + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataCreateEnd() + { + // The terrain system has finished creating itself, so we should now have data for creating a heightfield. + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataDestroyBegin() + { + // The terrain system is starting to destroy itself, so notify listeners of a change since the heightfield + // will no longer have any valid data. + NotifyListenersOfHeightfieldDataChange(); + } + + void TerrainPhysicsColliderComponent::OnTerrainDataChanged( + [[maybe_unused]] const AZ::Aabb& dirtyRegion, [[maybe_unused]] TerrainDataChangedMask dataChangedMask) + { + NotifyListenersOfHeightfieldDataChange(); + } + + AZ::Aabb TerrainPhysicsColliderComponent::GetHeightfieldAabb() const + { + AZ::Aabb worldSize = AZ::Aabb::CreateNull(); + + LmbrCentral::ShapeComponentRequestsBus::EventResult( + worldSize, GetEntityId(), &LmbrCentral::ShapeComponentRequestsBus::Events::GetEncompassingAabb); + + auto vector2Floor = [](const AZ::Vector2& in) + { + return AZ::Vector2(floor(in.GetX()), floor(in.GetY())); + }; + auto vector2Ceil = [](const AZ::Vector2& in) + { + return AZ::Vector2(ceil(in.GetX()), ceil(in.GetY())); + }; + + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + const AZ::Vector3 boundsMin = worldSize.GetMin(); + const AZ::Vector3 boundsMax = worldSize.GetMax(); + + const AZ::Vector2 gridMinBoundLower = vector2Floor(AZ::Vector2(boundsMin) / gridResolution) * gridResolution; + const AZ::Vector2 gridMaxBoundUpper = vector2Ceil(AZ::Vector2(boundsMax) / gridResolution) * gridResolution; + + return AZ::Aabb::CreateFromMinMaxValues( + gridMinBoundLower.GetX(), gridMinBoundLower.GetY(), boundsMin.GetZ(), + gridMaxBoundUpper.GetX(), gridMaxBoundUpper.GetY(), boundsMax.GetZ() + ); + } + + void TerrainPhysicsColliderComponent::GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const + { + AZ::Aabb heightfieldAabb = GetHeightfieldAabb(); + + // Because our terrain heights are relative to the center of the bounding box, the min and max allowable heights are also + // relative to the center. They are also clamped to the size of the bounding box. + minHeightBounds = -(heightfieldAabb.GetZExtent() / 2.0f); + maxHeightBounds = heightfieldAabb.GetZExtent() / 2.0f; + } + + AZ::Transform TerrainPhysicsColliderComponent::GetHeightfieldTransform() const + { + // We currently don't support rotation of terrain heightfields. + AZ::Vector3 translate; + AZ::TransformBus::EventResult(translate, GetEntityId(), &AZ::TransformBus::Events::GetWorldTranslation); + + AZ::Transform transform = AZ::Transform::CreateTranslation(translate); + + return transform; + } + + void TerrainPhysicsColliderComponent::GenerateHeightsInBounds(AZStd::vector& heights) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + + AZ::Aabb worldSize = GetHeightfieldAabb(); + + const float worldCenterZ = worldSize.GetCenter().GetZ(); + + int32_t gridWidth, gridHeight; + GetHeightfieldGridSize(gridWidth, gridHeight); + + heights.clear(); + heights.reserve(gridWidth * gridHeight); + + for (int32_t row = 0; row < gridHeight; row++) + { + const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY(); + for (int32_t col = 0; col < gridWidth; col++) + { + const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX(); + float height = 0.0f; + + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, + AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, nullptr); + + heights.emplace_back(height - worldCenterZ); + } + } + } + + void TerrainPhysicsColliderComponent::GenerateHeightsAndMaterialsInBounds( + AZStd::vector& heightMaterials) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + + AZ::Aabb worldSize = GetHeightfieldAabb(); + + const float worldCenterZ = worldSize.GetCenter().GetZ(); + const float worldHeightBoundsMin = worldSize.GetMin().GetZ(); + const float worldHeightBoundsMax = worldSize.GetMax().GetZ(); + + int32_t gridWidth, gridHeight; + GetHeightfieldGridSize(gridWidth, gridHeight); + + heightMaterials.clear(); + heightMaterials.reserve(gridWidth * gridHeight); + + for (int32_t row = 0; row < gridHeight; row++) + { + const float y = row * gridResolution.GetY() + worldSize.GetMin().GetY(); + for (int32_t col = 0; col < gridWidth; col++) + { + const float x = col * gridResolution.GetX() + worldSize.GetMin().GetX(); + float height = 0.0f; + + bool terrainExists = true; + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + height, &AzFramework::Terrain::TerrainDataRequests::GetHeightFromFloats, x, y, + AzFramework::Terrain::TerrainDataRequests::Sampler::DEFAULT, &terrainExists); + + // Any heights that fall outside the range of our bounding box will get turned into holes. + if ((height < worldHeightBoundsMin) || (height > worldHeightBoundsMax)) + { + height = worldHeightBoundsMin; + terrainExists = false; + } + + Physics::HeightMaterialPoint point; + point.m_height = height - worldCenterZ; + point.m_quadMeshType = terrainExists ? Physics::QuadMeshType::SubdivideUpperLeftToBottomRight : Physics::QuadMeshType::Hole; + heightMaterials.emplace_back(point); + } + } + } + + AZ::Vector2 TerrainPhysicsColliderComponent::GetHeightfieldGridSpacing() const + { + AZ::Vector2 gridResolution = AZ::Vector2(1.0f); + AzFramework::Terrain::TerrainDataRequestBus::BroadcastResult( + gridResolution, &AzFramework::Terrain::TerrainDataRequests::GetTerrainHeightQueryResolution); + + return gridResolution; + } + + void TerrainPhysicsColliderComponent::GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const + { + const AZ::Vector2 gridResolution = GetHeightfieldGridSpacing(); + const AZ::Aabb bounds = GetHeightfieldAabb(); + + numColumns = aznumeric_cast((bounds.GetMax().GetX() - bounds.GetMin().GetX()) / gridResolution.GetX()); + numRows = aznumeric_cast((bounds.GetMax().GetY() - bounds.GetMin().GetY()) / gridResolution.GetY()); + } + + AZStd::vector TerrainPhysicsColliderComponent::GetMaterialList() const + { + return AZStd::vector(); + } + + AZStd::vector TerrainPhysicsColliderComponent::GetHeights() const + { + AZStd::vector heights; + GenerateHeightsInBounds(heights); + + return heights; + } + + AZStd::vector TerrainPhysicsColliderComponent::GetHeightsAndMaterials() const + { + AZStd::vector heightMaterials; + GenerateHeightsAndMaterialsInBounds(heightMaterials); + + return heightMaterials; + } +} diff --git a/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h new file mode 100644 index 0000000000..e268223689 --- /dev/null +++ b/Gems/Terrain/Code/Source/Components/TerrainPhysicsColliderComponent.h @@ -0,0 +1,91 @@ +/* + * 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 LmbrCentral +{ + template + class EditorWrappedComponentBase; +} + +namespace Terrain +{ + class TerrainPhysicsColliderConfig + : public AZ::ComponentConfig + { + public: + AZ_CLASS_ALLOCATOR(TerrainPhysicsColliderConfig, AZ::SystemAllocator, 0); + AZ_RTTI(TerrainPhysicsColliderConfig, "{E9EADB8F-C3A5-4B9C-A62D-2DBC86B4CE59}", AZ::ComponentConfig); + static void Reflect(AZ::ReflectContext* context); + + }; + + + class TerrainPhysicsColliderComponent + : public AZ::Component + , public Physics::HeightfieldProviderRequestsBus::Handler + , protected LmbrCentral::ShapeComponentNotificationsBus::Handler + , protected AzFramework::Terrain::TerrainDataNotificationBus::Handler + { + public: + template + friend class LmbrCentral::EditorWrappedComponentBase; + AZ_COMPONENT(TerrainPhysicsColliderComponent, "{33C20287-1D37-44D0-96A0-2C3766E23624}"); + + static void GetProvidedServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetIncompatibleServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void GetRequiredServices(AZ::ComponentDescriptor::DependencyArrayType& services); + static void Reflect(AZ::ReflectContext* context); + + TerrainPhysicsColliderComponent(const TerrainPhysicsColliderConfig& configuration); + TerrainPhysicsColliderComponent(); + ~TerrainPhysicsColliderComponent() = default; + + // HeightfieldProviderRequestsBus + AZ::Vector2 GetHeightfieldGridSpacing() const override; + void GetHeightfieldGridSize(int32_t& numColumns, int32_t& numRows) const override; + void GetHeightfieldHeightBounds(float& minHeightBounds, float& maxHeightBounds) const override; + AZ::Aabb GetHeightfieldAabb() const override; + AZ::Transform GetHeightfieldTransform() const override; + AZStd::vector GetMaterialList() const override; + AZStd::vector GetHeights() const override; + AZStd::vector GetHeightsAndMaterials() const override; + + protected: + ////////////////////////////////////////////////////////////////////////// + // AZ::Component interface implementation + void Activate() override; + void Deactivate() override; + bool ReadInConfig(const AZ::ComponentConfig* baseConfig) override; + bool WriteOutConfig(AZ::ComponentConfig* outBaseConfig) const override; + + void GenerateHeightsInBounds(AZStd::vector& heights) const; + void GenerateHeightsAndMaterialsInBounds(AZStd::vector& heightMaterials) const; + + void NotifyListenersOfHeightfieldDataChange(); + + // ShapeComponentNotificationsBus + void OnShapeChanged(ShapeChangeReasons changeReason) override; + + void OnTerrainDataCreateEnd() override; + void OnTerrainDataDestroyBegin() override; + void OnTerrainDataChanged(const AZ::Aabb& dirtyRegion, TerrainDataChangedMask dataChangedMask) override; + + private: + TerrainPhysicsColliderConfig m_configuration; + }; +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp new file mode 100644 index 0000000000..6df6c3b440 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp @@ -0,0 +1,24 @@ +/* + * Copyright (c) Contributors to the Open 3D Engine Project. + * For complete copyright and license terms please see the LICENSE at the root of this distribution. + * + * SPDX-License-Identifier: Apache-2.0 OR MIT + * + */ + +#include +#include +#include + +namespace Terrain +{ + void EditorTerrainPhysicsColliderComponent::Reflect(AZ::ReflectContext* context) + { + // Call ReflectSubClass in EditorWrappedComponentBase to handle all the boilerplate reflection. + BaseClassType::ReflectSubClass( + context, 1, + &LmbrCentral::EditorWrappedComponentBaseVersionConverter + ); + } +} diff --git a/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h new file mode 100644 index 0000000000..d2254161a0 --- /dev/null +++ b/Gems/Terrain/Code/Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h @@ -0,0 +1,32 @@ +/* + * 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 + +namespace Terrain +{ + class EditorTerrainPhysicsColliderComponent + : public LmbrCentral::EditorWrappedComponentBase + { + public: + using BaseClassType = LmbrCentral::EditorWrappedComponentBase; + AZ_EDITOR_COMPONENT(EditorTerrainPhysicsColliderComponent, "{C43FAB8F-3968-46A6-920E-E84AEDED3DF5}", BaseClassType); + static void Reflect(AZ::ReflectContext* context); + + static constexpr auto s_categoryName = "Terrain"; + static constexpr auto s_componentName = "Terrain Physics Heightfield Collider"; + static constexpr auto s_componentDescription = "Provides terrain data to a physics collider in the form of a heightfield and surface->material mapping."; + static constexpr auto s_icon = "Editor/Icons/Components/TerrainLayerSpawner.svg"; + static constexpr auto s_viewportIcon = "Editor/Icons/Components/Viewport/TerrainLayerSpawner.svg"; + static constexpr auto s_helpUrl = ""; + }; +} diff --git a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp index 6fd8979e4a..0ea822e8b5 100644 --- a/Gems/Terrain/Code/Source/EditorTerrainModule.cpp +++ b/Gems/Terrain/Code/Source/EditorTerrainModule.cpp @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -32,6 +33,7 @@ namespace Terrain Terrain::EditorTerrainSurfaceMaterialsListComponent::CreateDescriptor(), Terrain::EditorTerrainWorldComponent::CreateDescriptor(), Terrain::EditorTerrainWorldDebuggerComponent::CreateDescriptor(), + Terrain::EditorTerrainPhysicsColliderComponent::CreateDescriptor(), Terrain::EditorTerrainWorldRendererComponent::CreateDescriptor(), }); diff --git a/Gems/Terrain/Code/Source/TerrainModule.cpp b/Gems/Terrain/Code/Source/TerrainModule.cpp index c3a7890565..2524f3684c 100644 --- a/Gems/Terrain/Code/Source/TerrainModule.cpp +++ b/Gems/Terrain/Code/Source/TerrainModule.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include #include @@ -37,6 +38,7 @@ namespace Terrain TerrainMacroMaterialComponent::CreateDescriptor(), TerrainSurfaceGradientListComponent::CreateDescriptor(), TerrainSurfaceDataSystemComponent::CreateDescriptor(), + TerrainPhysicsColliderComponent::CreateDescriptor() }); } diff --git a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp index ee4b18e2fb..0ca5e7d99a 100644 --- a/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp +++ b/Gems/Terrain/Code/Tests/LayerSpawnerTests.cpp @@ -7,6 +7,7 @@ */ #include +#include #include #include @@ -195,8 +196,10 @@ TEST_F(LayerSpawnerComponentTest, LayerSpawnerTransformChangedUpdatesTerrainSyst m_entity->Activate(); - AZ::TransformNotificationBus::Event( - m_entity->GetId(), &AZ::TransformNotificationBus::Events::OnTransformChanged, AZ::Transform(), AZ::Transform()); + // The component gets transform change notifications via the shape bus. + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); m_entity->Deactivate(); } diff --git a/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp new file mode 100644 index 0000000000..c314e4e968 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainHeightGradientListTests.cpp @@ -0,0 +1,146 @@ +/* + * 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 + +using ::testing::_; +using ::testing::AtLeast; +using ::testing::Mock; +using ::testing::NiceMock; +using ::testing::Return; + +class TerrainHeightGradientListComponentTest : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + // Create the required box component. + UnitTest::MockAxisAlignedBoxShapeComponent* boxComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(boxComponent->CreateDescriptor()); + + // Create the TerrainHeightGradientListComponent with an entity in its configuration. + Terrain::TerrainHeightGradientListConfig config; + config.m_gradientEntities.push_back(m_entity->GetId()); + + Terrain::TerrainHeightGradientListComponent* heightGradientListComponent = m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(heightGradientListComponent->CreateDescriptor()); + + // Create a MockTerrainLayerSpawnerComponent to provide the required TerrainAreaService. + UnitTest::MockTerrainLayerSpawnerComponent* layerSpawner = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(layerSpawner->CreateDescriptor()); + + m_entity->Init(); + } +}; + +TEST_F(TerrainHeightGradientListComponentTest, ActivateEntityActivateSuccess) +{ + // Check that the entity activates. + CreateEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + m_entity.reset(); +} + +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientRefreshesTerrainSystem) +{ + // Check that the HeightGradientListComponent informs the TerrainSystem when the composition changes. + CreateEntity(); + + m_entity->Activate(); + + NiceMock terrainSystem; + + // 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); + + LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + // Stop the EXPECT_CALL check now, as OnCompositionChanged will get called twice again during the reset. + Mock::VerifyAndClearExpectations(&terrainSystem); + + m_entity.reset(); +} + +TEST_F(TerrainHeightGradientListComponentTest, TerrainHeightGradientListReturnsHeights) +{ + // Check that the HeightGradientListComponent returns expected height values. + CreateEntity(); + + NiceMock heightfieldRequestBus(m_entity->GetId()); + + m_entity->Activate(); + + const float mockGradientValue = 0.25f; + NiceMock gradientRequests(m_entity->GetId()); + ON_CALL(gradientRequests, GetValue).WillByDefault(Return(mockGradientValue)); + + // Setup a mock to provide the encompassing Aabb to the HeightGradientListComponent. + const float min = 0.0f; + const float max = 1000.0f; + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(max)); + NiceMock mockShapeRequests(m_entity->GetId()); + ON_CALL(mockShapeRequests, GetEncompassingAabb).WillByDefault(Return(aabb)); + + const float worldMax = 10000.0f; + const AZ::Aabb worldAabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3(min), AZ::Vector3(worldMax)); + NiceMock mockterrainDataRequests; + ON_CALL(mockterrainDataRequests, GetTerrainHeightQueryResolution).WillByDefault(Return(AZ::Vector2(1.0f))); + ON_CALL(mockterrainDataRequests, GetTerrainAabb).WillByDefault(Return(worldAabb)); + + // Ensure the cached values in the HeightGradientListComponent are up to date. + LmbrCentral::DependencyNotificationBus::Event(m_entity->GetId(), &LmbrCentral::DependencyNotificationBus::Events::OnCompositionChanged); + + const AZ::Vector3 inPosition = AZ::Vector3::CreateZero(); + AZ::Vector3 outPosition = AZ::Vector3::CreateZero(); + bool terrainExists = false; + Terrain::TerrainAreaHeightRequestBus::Event(m_entity->GetId(), &Terrain::TerrainAreaHeightRequestBus::Events::GetHeight, inPosition, outPosition, terrainExists); + + const float height = outPosition.GetZ(); + + EXPECT_NEAR(height, mockGradientValue * max, 0.01f); + + m_entity.reset(); +} + diff --git a/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp b/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp new file mode 100644 index 0000000000..d1deca897c --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainPhysicsColliderTests.cpp @@ -0,0 +1,292 @@ +/* + * 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 + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; +using ::testing::Return; + +class TerrainPhysicsColliderComponentTest + : public ::testing::Test +{ +protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + Terrain::TerrainPhysicsColliderComponent* m_colliderComponent; + UnitTest::MockAxisAlignedBoxShapeComponent* m_boxComponent; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_app.Destroy(); + } + + void CreateEntity() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + m_entity->Init(); + } + + void AddTerrainPhysicsColliderAndShapeComponentToEntity() + { + m_boxComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_boxComponent->CreateDescriptor()); + + m_colliderComponent = m_entity->CreateComponent(Terrain::TerrainPhysicsColliderConfig()); + m_app.RegisterComponentDescriptor(m_colliderComponent->CreateDescriptor()); + } +}; + +TEST_F(TerrainPhysicsColliderComponentTest, ActivateEntityActivateSuccess) +{ + // Check that the entity activates with a collider and the required shape attached. + CreateEntity(); + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + EXPECT_EQ(m_entity->GetState(), AZ::Entity::State::Active); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderTransformChangedNotifiesHeightfieldBus) +{ + // Check that the HeightfieldBus is notified when the transform of the entity changes. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + NiceMock heightfieldListener(m_entity->GetId()); + EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1); + + // The component gets transform change notifications via the shape bus. + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::TransformChanged); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderShapeChangedNotifiesHeightfieldBus) +{ + // Check that the Heightfield bus is notified when the shape component changes. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + NiceMock heightfieldListener(m_entity->GetId()); + EXPECT_CALL(heightfieldListener, OnHeightfieldDataChanged(_)).Times(1); + + LmbrCentral::ShapeComponentNotificationsBus::Event( + m_entity->GetId(), &LmbrCentral::ShapeComponentNotificationsBus::Events::OnShapeChanged, + LmbrCentral::ShapeComponentNotifications::ShapeChangeReasons::ShapeChanged); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsAlignedRowBoundsCorrectly) +{ + // Check that the heightfield grid size is correct when the shape bounds match the grid resolution. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + const AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // With the bounds set at 0-1024 and a resolution of 1.0, the heightfield grid should be 1024x1024. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMinBoundsCorrectly) +{ + // Check that the heightfield grid is correctly expanded if the minimum value of the bounds needs expanding + // to correctly encompass it. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.1f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // If the heightfield is not expanded to ensure it encompasses the shape bounds, + // the values returned would be 1023. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderExpandsMaxBoundsCorrectly) +{ + // Check that the heightfield grid is correctly expanded if the maximum value of the bounds needs expanding + // to correctly encompass it. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1023.5f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + // If the heightfield is not expanded to ensure it encompasses the shape bounds, + // the values returned would be 1023. + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderGetHeightsReturnsHeights) +{ + // Check that the TerrainPhysicsCollider returns a heightfield of the expected size. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const float boundsMin = 0.0f; + const float boundsMax = 1024.0f; + + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + NiceMock terrainListener; + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + int32_t cols, rows; + Physics::HeightfieldProviderRequestsBus::Event( + m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeightfieldGridSize, cols, rows); + + AZStd::vector heights; + + Physics::HeightfieldProviderRequestsBus::EventResult( + heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights); + + EXPECT_EQ(cols, 1024); + EXPECT_EQ(rows, 1024); + EXPECT_EQ(heights.size(), cols * rows); + + m_entity.reset(); +} + +TEST_F(TerrainPhysicsColliderComponentTest, TerrainPhysicsColliderReturnsRelativeHeightsCorrectly) +{ + // Check that the values stored in the heightfield returned by the TerrainPhysicsCollider are correct. + CreateEntity(); + + AddTerrainPhysicsColliderAndShapeComponentToEntity(); + + m_entity->Activate(); + + const AZ::Vector3 boundsMin = AZ::Vector3(0.0f); + const AZ::Vector3 boundsMax = AZ::Vector3(256.0f, 256.0f, 32768.0f); + + const float mockHeight = 32768.0f; + AZ::Vector2 mockHeightResolution = AZ::Vector2(1.0f); + + NiceMock terrainListener; + ON_CALL(terrainListener, GetHeightFromFloats).WillByDefault(Return(mockHeight)); + ON_CALL(terrainListener, GetTerrainHeightQueryResolution).WillByDefault(Return(mockHeightResolution)); + + // Just return the bounds as setup. This is equivalent to the box being at the origin. + NiceMock boxShape(m_entity->GetId()); + const AZ::Aabb bounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(boundsMin), AZ::Vector3(boundsMax)); + ON_CALL(boxShape, GetEncompassingAabb).WillByDefault(Return(bounds)); + + AZStd::vector heights; + + Physics::HeightfieldProviderRequestsBus::EventResult(heights, m_entity->GetId(), &Physics::HeightfieldProviderRequestsBus::Events::GetHeights); + + ASSERT_FALSE(heights.empty()); + + const float expectedHeightValue = 16384.0f; + EXPECT_NEAR(heights[0], expectedHeightValue, 0.01f); + + m_entity->Reset(); +} diff --git a/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp new file mode 100644 index 0000000000..2a645b3f94 --- /dev/null +++ b/Gems/Terrain/Code/Tests/TerrainSurfaceGradientListTests.cpp @@ -0,0 +1,129 @@ +/* + * 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 + +using ::testing::NiceMock; +using ::testing::AtLeast; +using ::testing::_; +using ::testing::Return; + +namespace UnitTest +{ + class TerrainSurfaceGradientListTest : public ::testing::Test + { + protected: + AZ::ComponentApplication m_app; + + AZStd::unique_ptr m_entity; + UnitTest::MockTerrainLayerSpawnerComponent* m_layerSpawnerComponent = nullptr; + AZStd::unique_ptr m_gradientEntity1, m_gradientEntity2; + + const AZStd::string surfaceTag1 = "testtag1"; + const AZStd::string surfaceTag2 = "testtag2"; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + + CreateEntities(); + } + + void TearDown() override + { + m_gradientEntity2.reset(); + m_gradientEntity1.reset(); + m_entity.reset(); + + m_app.Destroy(); + } + + void CreateEntities() + { + m_entity = AZStd::make_unique(); + ASSERT_TRUE(m_entity); + + m_entity->Init(); + + m_gradientEntity1 = AZStd::make_unique(); + ASSERT_TRUE(m_gradientEntity1); + + m_gradientEntity1->Init(); + + m_gradientEntity2 = AZStd::make_unique(); + ASSERT_TRUE(m_gradientEntity2); + + m_gradientEntity2->Init(); + } + + void AddSurfaceGradientListToEntities() + { + m_layerSpawnerComponent = m_entity->CreateComponent(); + m_app.RegisterComponentDescriptor(m_layerSpawnerComponent->CreateDescriptor()); + + Terrain::TerrainSurfaceGradientListConfig config; + + Terrain::TerrainSurfaceGradientMapping mapping1; + mapping1.m_gradientEntityId = m_gradientEntity1->GetId(); + mapping1.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag1); + config.m_gradientSurfaceMappings.emplace_back(mapping1); + + Terrain::TerrainSurfaceGradientMapping mapping2; + mapping2.m_gradientEntityId = m_gradientEntity2->GetId(); + mapping2.m_surfaceTag = SurfaceData::SurfaceTag(surfaceTag2); + config.m_gradientSurfaceMappings.emplace_back(mapping2); + + Terrain::TerrainSurfaceGradientListComponent* terrainSurfaceGradientListComponent = + m_entity->CreateComponent(config); + m_app.RegisterComponentDescriptor(terrainSurfaceGradientListComponent->CreateDescriptor()); + } + }; + + TEST_F(TerrainSurfaceGradientListTest, SurfaceGradientReturnsSurfaceWeightsInOrder) + { + // When there is more that one surface/weight defined and added to the component, they should all + // be returned in descending weight order. + AddSurfaceGradientListToEntities(); + + m_entity->Activate(); + m_gradientEntity1->Activate(); + m_gradientEntity2->Activate(); + + const float gradient1Value = 0.3f; + NiceMock mockGradientRequests1(m_gradientEntity1->GetId()); + ON_CALL(mockGradientRequests1, GetValue).WillByDefault(Return(gradient1Value)); + + const float gradient2Value = 1.0f; + NiceMock mockGradientRequests2(m_gradientEntity2->GetId()); + ON_CALL(mockGradientRequests2, GetValue).WillByDefault(Return(gradient2Value)); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet weightSet; + Terrain::TerrainAreaSurfaceRequestBus::Event( + m_entity->GetId(), &Terrain::TerrainAreaSurfaceRequestBus::Events::GetSurfaceWeights, AZ::Vector3::CreateZero(), weightSet); + + AZ::Crc32 expectedCrcList[] = { AZ::Crc32(surfaceTag2), AZ::Crc32(surfaceTag1) }; + const float expectedWeightList[] = { gradient2Value, gradient1Value }; + + int index = 0; + for (const auto& surfaceWeight : weightSet) + { + EXPECT_EQ(surfaceWeight.m_surfaceType, expectedCrcList[index]); + EXPECT_NEAR(surfaceWeight.m_weight, expectedWeightList[index], 0.01f); + index++; + } + } +} // namespace UnitTest + + diff --git a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp index e4fb0f348e..f273a85e26 100644 --- a/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp +++ b/Gems/Terrain/Code/Tests/TerrainSystemTest.cpp @@ -13,9 +13,10 @@ #include #include -#include +#include #include +#include #include using ::testing::AtLeast; @@ -25,284 +26,284 @@ using ::testing::IsFalse; using ::testing::Ne; using ::testing::NiceMock; using ::testing::Return; +using ::testing::SetArgReferee; -class TerrainSystemTest : public ::testing::Test +namespace UnitTest { -protected: - // Defines a structure for defining both an XY position and the expected height for that position. - struct HeightTestPoint + class TerrainSystemTest : public ::testing::Test { - AZ::Vector2 m_testLocation; - float m_expectedHeight; + protected: + // Defines a structure for defining both an XY position and the expected height for that position. + struct HeightTestPoint + { + AZ::Vector2 m_testLocation = AZ::Vector2::CreateZero(); + float m_expectedHeight = 0.0f; + }; + + AZ::ComponentApplication m_app; + AZStd::unique_ptr m_terrainSystem; + + AZStd::unique_ptr> m_boxShapeRequests; + AZStd::unique_ptr> m_shapeRequests; + AZStd::unique_ptr> m_terrainAreaHeightRequests; + + void SetUp() override + { + AZ::ComponentApplication::Descriptor appDesc; + appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; + appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; + appDesc.m_stackRecordLevels = 20; + + m_app.Create(appDesc); + } + + void TearDown() override + { + m_terrainSystem.reset(); + m_boxShapeRequests.reset(); + m_shapeRequests.reset(); + m_terrainAreaHeightRequests.reset(); + m_app.Destroy(); + } + + AZStd::unique_ptr CreateEntity() + { + return AZStd::make_unique(); + } + + void ActivateEntity(AZ::Entity* entity) + { + entity->Init(); + EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); + + entity->Activate(); + EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); + } + + template + AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(config); + } + + template + AZ::Component* CreateComponent(AZ::Entity* entity) + { + m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); + return entity->CreateComponent(); + } + + // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults + // on a test-by-test basis. + void CreateAndActivateTerrainSystem( + AZ::Vector2 queryResolution = AZ::Vector2(1.0f), + AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) + { + // Create the terrain system and give it one tick to fully initialize itself. + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->SetTerrainAabb(worldBounds); + m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); + m_terrainSystem->Activate(); + AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + } + + AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( + const AZ::Aabb& spawnerBox, const AZStd::function& mockHeights) + { + // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. + auto entity = CreateEntity(); + CreateComponent(entity.get()); + CreateComponent(entity.get()); + + m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); + m_shapeRequests = AZStd::make_unique>(entity->GetId()); + + // Set up the box shape to return whatever spawnerBox was passed in. + ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); + + // Set up a mock height provider to use the passed-in mock height function to generate a height. + m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); + ON_CALL(*m_terrainAreaHeightRequests, GetHeight) + .WillByDefault( + [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) + { + // By default, set the outPosition to the input position and terrain to always exist. + outPosition = inPosition; + terrainExists = true; + // Let the test function modify these values based on the needs of the specific test. + mockHeights(outPosition, terrainExists); + }); + + ActivateEntity(entity.get()); + return entity; + } }; - AZ::ComponentApplication m_app; - AZStd::unique_ptr m_terrainSystem; - - AZStd::unique_ptr> m_boxShapeRequests; - AZStd::unique_ptr> m_shapeRequests; - AZStd::unique_ptr> m_terrainAreaHeightRequests; - - - void SetUp() override + TEST_F(TerrainSystemTest, TrivialCreateDestroy) { - AZ::ComponentApplication::Descriptor appDesc; - appDesc.m_memoryBlocksByteSize = 20 * 1024 * 1024; - appDesc.m_recordingMode = AZ::Debug::AllocationRecords::RECORD_NO_RECORDS; - appDesc.m_stackRecordLevels = 20; + // Trivially verify that the terrain system can successfully be constructed and destructed without errors. + + m_terrainSystem = AZStd::make_unique(); + } + + TEST_F(TerrainSystemTest, TrivialActivateDeactivate) + { + // Verify that the terrain system can be activated and deactivated without errors. - m_app.Create(appDesc); - } - - void TearDown() override - { - m_terrainSystem.reset(); - m_boxShapeRequests.reset(); - m_shapeRequests.reset(); - m_terrainAreaHeightRequests.reset(); - m_app.Destroy(); - } - - AZStd::unique_ptr CreateEntity() - { - return AZStd::make_unique(); - } - - void ActivateEntity(AZ::Entity* entity) - { - entity->Init(); - EXPECT_EQ(AZ::Entity::State::Init, entity->GetState()); - - entity->Activate(); - EXPECT_EQ(AZ::Entity::State::Active, entity->GetState()); - } - - template - AZ::Component* CreateComponent(AZ::Entity* entity, const Configuration& config) - { - m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); - return entity->CreateComponent(config); - } - - template - AZ::Component* CreateComponent(AZ::Entity* entity) - { - m_app.RegisterComponentDescriptor(Component::CreateDescriptor()); - return entity->CreateComponent(); - } - - // Create a terrain system with reasonable defaults for testing, but with the ability to override the defaults - // on a test-by-test basis. - void CreateAndActivateTerrainSystem( - AZ::Vector2 queryResolution = AZ::Vector2(1.0f), - AZ::Aabb worldBounds = AZ::Aabb::CreateFromMinMax(AZ::Vector3(-128.0f), AZ::Vector3(128.0f))) - { - // Create the terrain system and give it one tick to fully initialize itself. m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->SetTerrainAabb(worldBounds); - m_terrainSystem->SetTerrainHeightQueryResolution(queryResolution); m_terrainSystem->Activate(); - AZ::TickBus::Broadcast(&AZ::TickBus::Events::OnTick, 0.f, AZ::ScriptTimePoint{}); + m_terrainSystem->Deactivate(); } - - AZStd::unique_ptr CreateAndActivateMockTerrainLayerSpawner( - const AZ::Aabb& spawnerBox, - const AZStd::function& mockHeights) + TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) { - // Create the base entity with a mock box shape, Terrain Layer Spawner, and height provider. - auto entity = CreateEntity(); - CreateComponent(entity.get()); - CreateComponent(entity.get()); + // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. - m_boxShapeRequests = AZStd::make_unique>(entity->GetId()); - m_shapeRequests = AZStd::make_unique>(entity->GetId()); + NiceMock mockTerrainListener; + EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); + EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); - // Set up the box shape to return whatever spawnerBox was passed in. - ON_CALL(*m_shapeRequests, GetEncompassingAabb).WillByDefault(Return(spawnerBox)); - - // Set up a mock height provider to use the passed-in mock height function to generate a height. - m_terrainAreaHeightRequests = AZStd::make_unique>(entity->GetId()); - ON_CALL(*m_terrainAreaHeightRequests, GetHeight) - .WillByDefault( - [mockHeights](const AZ::Vector3& inPosition, AZ::Vector3& outPosition, bool& terrainExists) - { - // By default, set the outPosition to the input position and terrain to always exist. - outPosition = inPosition; - terrainExists = true; - // Let the test function modify these values based on the needs of the specific test. - mockHeights(outPosition, terrainExists); - }); - - ActivateEntity(entity.get()); - return entity; + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); } -}; -TEST_F(TerrainSystemTest, TrivialCreateDestroy) -{ - // Trivially verify that the terrain system can successfully be constructed and destructed without errors. - - m_terrainSystem = AZStd::make_unique(); -} - -TEST_F(TerrainSystemTest, TrivialActivateDeactivate) -{ - // Verify that the terrain system can be activated and deactivated without errors. - - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - m_terrainSystem->Deactivate(); -} - -TEST_F(TerrainSystemTest, CreateEventsCalledOnActivation) -{ - // Verify that when the terrain system is activated, the OnTerrainDataCreate* ebus notifications are generated. - - NiceMock mockTerrainListener; - EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateBegin()).Times(AtLeast(1)); - EXPECT_CALL(mockTerrainListener, OnTerrainDataCreateEnd()).Times(AtLeast(1)); - - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); -} - -TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) -{ - // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. - - NiceMock mockTerrainListener; - EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); - EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); - - m_terrainSystem = AZStd::make_unique(); - m_terrainSystem->Activate(); - m_terrainSystem->Deactivate(); -} - -TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) -{ - // For the terrain system, terrain should only exist where terrain layer spawners are present. - - // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point - // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns - // a normal facing up the Z axis. - - // Create and activate the terrain system with our testing defaults for world bounds and query resolution. - CreateAndActivateTerrainSystem(); - - AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); - - // Loop through several points within the world bounds, including on the edges, and verify that they all return false for - // terrainExists with default heights and normals. - for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) + TEST_F(TerrainSystemTest, DestroyEventsCalledOnDeactivation) { - for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) - { - AZ::Vector3 position(x, y, 0.0f); - bool terrainExists = true; - float height = m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - EXPECT_FALSE(terrainExists); - EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); + // Verify that when the terrain system is deactivated, the OnTerrainDataDestroy* ebus notifications are generated. - terrainExists = true; - AZ::Vector3 normal = m_terrainSystem->GetNormal( - position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); - EXPECT_FALSE(terrainExists); - EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); + NiceMock mockTerrainListener; + EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyBegin()).Times(AtLeast(1)); + EXPECT_CALL(mockTerrainListener, OnTerrainDataDestroyEnd()).Times(AtLeast(1)); - bool isHole = m_terrainSystem->GetIsHoleFromFloats( - position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); - EXPECT_TRUE(isHole); - } + m_terrainSystem = AZStd::make_unique(); + m_terrainSystem->Activate(); + m_terrainSystem->Deactivate(); } -} -TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) -{ - // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the TerrainLayerSpawner - // is defined. - - // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the - // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. - - // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. - constexpr float spawnerHeight = 5.0f; - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) - { - position.SetZ(spawnerHeight); - terrainExists = true; - }); - - // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. - - // Create and activate the terrain system with our testing defaults for world bounds and query resolution. - CreateAndActivateTerrainSystem(); - - // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain - // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. - const AZ::Aabb encompassingBox = - AZ::Aabb::CreateFromMinMax(spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), - spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); - - for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) + TEST_F(TerrainSystemTest, TerrainDoesNotExistWhenNoTerrainLayerSpawnersAreRegistered) { - for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) - { - AZ::Vector3 position(x, y, 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); - bool isHole = m_terrainSystem->GetIsHoleFromFloats( - position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + // For the terrain system, terrain should only exist where terrain layer spawners are present. - if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) + // Verify that in the active terrain system, if there are no terrain layer spawners, any arbitrary point + // will return false for terrainExists, returns a height equal to the min world bounds of the terrain system, and returns + // a normal facing up the Z axis. + + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); + + AZ::Aabb worldBounds = m_terrainSystem->GetTerrainAabb(); + + // Loop through several points within the world bounds, including on the edges, and verify that they all return false for + // terrainExists with default heights and normals. + for (float y = worldBounds.GetMin().GetY(); y <= worldBounds.GetMax().GetY(); y += (worldBounds.GetExtents().GetY() / 4.0f)) + { + for (float x = worldBounds.GetMin().GetX(); x <= worldBounds.GetMax().GetX(); x += (worldBounds.GetExtents().GetX() / 4.0f)) { - EXPECT_TRUE(heightQueryTerrainExists); - EXPECT_FALSE(isHole); - EXPECT_FLOAT_EQ(height, spawnerHeight); - } - else - { - EXPECT_FALSE(heightQueryTerrainExists); + AZ::Vector3 position(x, y, 0.0f); + bool terrainExists = true; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_FLOAT_EQ(height, worldBounds.GetMin().GetZ()); + + terrainExists = true; + AZ::Vector3 normal = + m_terrainSystem->GetNormal(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &terrainExists); + EXPECT_FALSE(terrainExists); + EXPECT_EQ(normal, AZ::Vector3::CreateAxisZ()); + + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); EXPECT_TRUE(isHole); } } } -} -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) -{ - // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact - // requested location, instead of the position being quantized to the height query grid. - - // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave - // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 - // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. - constexpr float amplitudeMeters = 10.0f; - constexpr float frequencyMeters = 1.0f; - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) - { - position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); - terrainExists = true; - }); - - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches - // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. - const AZ::Vector2 queryResolution(frequencyMeters); - CreateAndActivateTerrainSystem(queryResolution); - - // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the - // query resolution, or with the 0 points on the sine wave. - const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; - for (auto& nonZeroPoint : nonZeroPoints) + TEST_F(TerrainSystemTest, TerrainExistsOnlyWithinTerrainLayerSpawnerBounds) { + // Verify that the presence of a TerrainLayerSpawner causes terrain to exist in (and *only* in) the box where the + // TerrainLayerSpawner is defined. + + // The terrain system should only query Heights from the TerrainAreaHeightRequest bus within the + // TerrainLayerSpawner region, and so those values should only get returned from GetHeight for queries inside that region. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and always returns a height of 5. + constexpr float spawnerHeight = 5.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(spawnerHeight); + terrainExists = true; + }); + + // Verify that terrain exists within the layer spawner bounds, and doesn't exist outside of it. + + // Create and activate the terrain system with our testing defaults for world bounds and query resolution. + CreateAndActivateTerrainSystem(); + + // Create a box that's twice as big as the layer spawner box. Loop through it and verify that points within the layer box contain + // terrain and the expected height & normal values, and points outside the layer box don't contain terrain. + const AZ::Aabb encompassingBox = AZ::Aabb::CreateFromMinMax( + spawnerBox.GetMin() - (spawnerBox.GetExtents() / 2.0f), spawnerBox.GetMax() + (spawnerBox.GetExtents() / 2.0f)); + + for (float y = encompassingBox.GetMin().GetY(); y < encompassingBox.GetMax().GetY(); y += 1.0f) + { + for (float x = encompassingBox.GetMin().GetX(); x < encompassingBox.GetMax().GetX(); x += 1.0f) + { + AZ::Vector3 position(x, y, 0.0f); + bool heightQueryTerrainExists = false; + float height = m_terrainSystem->GetHeight( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); + bool isHole = m_terrainSystem->GetIsHoleFromFloats( + position.GetX(), position.GetY(), AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT); + + if (spawnerBox.Contains(AZ::Vector3(position.GetX(), position.GetY(), spawnerBox.GetMin().GetZ()))) + { + EXPECT_TRUE(heightQueryTerrainExists); + EXPECT_FALSE(isHole); + EXPECT_FLOAT_EQ(height, spawnerHeight); + } + else + { + EXPECT_FALSE(heightQueryTerrainExists); + EXPECT_TRUE(isHole); + } + } + } + } + + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) + { + // Verify that when using the "EXACT" height sampler, the returned heights come directly from the height provider at the exact + // requested location, instead of the position being quantized to the height query grid. + + // Create a mock terrain layer spawner that uses a box of (0,0,5) - (10,10,15) and generates a height based on a sine wave + // using a frequency of 1m and an amplitude of 10m. i.e. Heights will range between -10 to 10 meters, but will have a value of 0 + // every 0.5 meters. The sine wave value is based on the absolute X position only, for simplicity. + constexpr float amplitudeMeters = 10.0f; + constexpr float frequencyMeters = 1.0f; + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(0.0f, 0.0f, 5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(amplitudeMeters * sin(AZ::Constants::TwoPi * (position.GetX() / frequencyMeters))); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution that exactly matches + // the frequency of our sine wave. If our height queries rely on the query resolution, we should always get a value of 0. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test an arbitrary set of points that should all produce non-zero heights with the EXACT sampler. They're not aligned with the + // query resolution, or with the 0 points on the sine wave. + const AZ::Vector2 nonZeroPoints[] = { AZ::Vector2(0.3f), AZ::Vector2(2.8f), AZ::Vector2(5.9f), AZ::Vector2(7.7f) }; + for (auto& nonZeroPoint : nonZeroPoints) + { AZ::Vector3 position(nonZeroPoint.GetX(), nonZeroPoint.GetY(), 0.0f); bool heightQueryTerrainExists = false; float height = @@ -311,165 +312,253 @@ TEST_F(TerrainSystemTest, TerrainHeightQueriesWithExactSamplersIgnoreQueryGrid) // We've chosen a bunch of places on the sine wave that should return a non-zero positive or negative value. constexpr float epsilon = 0.0001f; EXPECT_GT(fabsf(height), epsilon); - } + } - // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on the - // sine wave, regardless of whether or not they align to the query resolution. - const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; - for (auto& zeroPoint : zeroPoints) - { - AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); - - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, 0.0f, epsilon); - } -} - -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) -{ - // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching - // the height. - - // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal - // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [](AZ::Vector3& position, bool& terrainExists) + // Test an arbitrary set of points that should all produce zero heights with the EXACT sampler, since they align with 0 points on + // the sine wave, regardless of whether or not they align to the query resolution. + const AZ::Vector2 zeroPoints[] = { AZ::Vector2(0.5f), AZ::Vector2(1.0f), AZ::Vector2(5.0f), AZ::Vector2(7.5f) }; + for (auto& zeroPoint : zeroPoints) { - position.SetZ(position.GetX() + position.GetY()); - terrainExists = true; - }); + AZ::Vector3 position(zeroPoint.GetX(), zeroPoint.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::EXACT, &heightQueryTerrainExists); - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter intervals. - const AZ::Vector2 queryResolution(0.25f); - CreateAndActivateTerrainSystem(queryResolution); - - // Test some points and verify that the results always go "downward", whether they're in positive or negative space. - // (Z contains the the expected result for convenience). - const HeightTestPoint testPoints[] = - { - { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 - { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 - { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 - { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 - { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 - - { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 - { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 - { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 - { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 - }; - for (auto& testPoint : testPoints) - { - const float expectedHeight = testPoint.m_expectedHeight; - - AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); - - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, expectedHeight, epsilon); + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, 0.0f, epsilon); + } } -} -TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) -{ - // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithClampSamplersUseQueryGrid) + { + // Verify that when using the "CLAMP" height sampler, the requested location is quantized to the height query grid before fetching + // the height. - // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal - // to the X + Y position, so we'll have heights that look like this on our grid: - // 0 *---* 1 - // | | - // 1 *---* 2 - // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid - // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so if either one doesn't get clamped we'll get an unexpected result. + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(position.GetX() + position.GetY()); + terrainExists = true; + }); - const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); - const float amplitudeMeters = 10.0f; - const float frequencyMeters = 1.0f; - auto entity = CreateAndActivateMockTerrainLayerSpawner( - spawnerBox, - [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 0.25 meter + // intervals. + const AZ::Vector2 queryResolution(0.25f); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results always go "downward", whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0.00 + 0.00 + { AZ::Vector2(0.3f, 0.3f), 0.5f }, // Should return a height of 0.25 + 0.25 + { AZ::Vector2(2.8f, 2.8f), 5.5f }, // Should return a height of 2.75 + 2.75 + { AZ::Vector2(5.5f, 5.5f), 11.0f }, // Should return a height of 5.50 + 5.50 + { AZ::Vector2(7.7f, 7.7f), 15.0f }, // Should return a height of 7.50 + 7.50 + + { AZ::Vector2(-0.3f, -0.3f), -1.0f }, // Should return a height of -0.50 + -0.50 + { AZ::Vector2(-2.8f, -2.8f), -6.0f }, // Should return a height of -3.00 + -3.00 + { AZ::Vector2(-5.5f, -5.5f), -11.0f }, // Should return a height of -5.50 + -5.50 + { AZ::Vector2(-7.7f, -7.7f), -15.5f } // Should return a height of -7.75 + -7.75 + }; + for (auto& testPoint : testPoints) { - // Our generated height will be X + Y. - float expectedHeight = position.GetX() + position.GetY(); + const float expectedHeight = testPoint.m_expectedHeight; - // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. - // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. - float unexpectedVariance = amplitudeMeters * - (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); - position.SetZ(expectedHeight + unexpectedVariance); - terrainExists = true; - }); + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = + m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::CLAMP, &heightQueryTerrainExists); - // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. - const AZ::Vector2 queryResolution(frequencyMeters); - CreateAndActivateTerrainSystem(queryResolution); - - // Test some points and verify that the results are the expected bilinear filtered result, - // whether they're in positive or negative space. - // (Z contains the the expected result for convenience). - const HeightTestPoint testPoints[] = { - - // Queries directly on grid points. These should return values of X + Y. - { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 - { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 - { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 - { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 - { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 - - { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 - { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 - { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 - { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 - - // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). - // These should just be a linear interpolation of the points, so it should still be X + Y. - - { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 - { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 - { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 - { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 - - { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 - { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 - - { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 - { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 - { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 - { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 - - { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 - { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 - - // Queries inside a grid square (both axes are in-between grid points) - // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values - // should *still* be X + Y assuming the points were sampled correctly from the grid points. - - { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 - { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 - - { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 - { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 - }; - - // Loop through every test point and validate it. - for (auto& testPoint : testPoints) - { - const float expectedHeight = testPoint.m_expectedHeight; - - AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); - bool heightQueryTerrainExists = false; - float height = - m_terrainSystem->GetHeight(position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); - - // Verify that our height query returned the bilinear filtered result we expect. - constexpr float epsilon = 0.0001f; - EXPECT_NEAR(height, expectedHeight, epsilon); + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } } -} + + TEST_F(TerrainSystemTest, TerrainHeightQueriesWithBilinearSamplersUseQueryGridToInterpolate) + { + // Verify that when using the "BILINEAR" height sampler, the heights are interpolated from points sampled from the query grid. + + // Create a mock terrain layer spawner that uses a box of (-10,-10,-5) - (10,10,15) and generates a height equal + // to the X + Y position, so we'll have heights that look like this on our grid: + // 0 *---* 1 + // | | + // 1 *---* 2 + // However, everywhere inside the grid box, we'll generate heights much larger than X + Y. It will have no effect on exact grid + // points, but it will noticeably affect the expected height values if any points get sampled in-between grid points. + + const AZ::Aabb spawnerBox = AZ::Aabb::CreateFromMinMaxValues(-10.0f, -10.0f, -5.0f, 10.0f, 10.0f, 15.0f); + const float amplitudeMeters = 10.0f; + const float frequencyMeters = 1.0f; + auto entity = CreateAndActivateMockTerrainLayerSpawner( + spawnerBox, + [amplitudeMeters, frequencyMeters](AZ::Vector3& position, bool& terrainExists) + { + // Our generated height will be X + Y. + float expectedHeight = position.GetX() + position.GetY(); + + // If either X or Y aren't evenly divisible by the query frequency, add a scaled value to our generated height. + // This will show up as an unexpected height "spike" if it gets used in any bilinear filter queries. + float unexpectedVariance = + amplitudeMeters * (fmodf(position.GetX(), frequencyMeters) + fmodf(position.GetY(), frequencyMeters)); + position.SetZ(expectedHeight + unexpectedVariance); + terrainExists = true; + }); + + // Create and activate the terrain system with our testing defaults for world bounds, and a query resolution at 1 meter intervals. + const AZ::Vector2 queryResolution(frequencyMeters); + CreateAndActivateTerrainSystem(queryResolution); + + // Test some points and verify that the results are the expected bilinear filtered result, + // whether they're in positive or negative space. + // (Z contains the the expected result for convenience). + const HeightTestPoint testPoints[] = { + + // Queries directly on grid points. These should return values of X + Y. + { AZ::Vector2(0.0f, 0.0f), 0.0f }, // Should return a height of 0 + 0 + { AZ::Vector2(1.0f, 0.0f), 1.0f }, // Should return a height of 1 + 0 + { AZ::Vector2(0.0f, 1.0f), 1.0f }, // Should return a height of 0 + 1 + { AZ::Vector2(1.0f, 1.0f), 2.0f }, // Should return a height of 1 + 1 + { AZ::Vector2(3.0f, 5.0f), 8.0f }, // Should return a height of 3 + 5 + + { AZ::Vector2(-1.0f, 0.0f), -1.0f }, // Should return a height of -1 + 0 + { AZ::Vector2(0.0f, -1.0f), -1.0f }, // Should return a height of 0 + -1 + { AZ::Vector2(-1.0f, -1.0f), -2.0f }, // Should return a height of -1 + -1 + { AZ::Vector2(-3.0f, -5.0f), -8.0f }, // Should return a height of -3 + -5 + + // Queries that are on a grid edge (one axis on the grid, the other somewhere in-between). + // These should just be a linear interpolation of the points, so it should still be X + Y. + + { AZ::Vector2(0.25f, 0.0f), 0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(3.75f, 0.0f), 3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, 0.25f), 0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, 3.75f), 3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(2.0f, 3.75f), 5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(2.25f, 4.0f), 6.25f }, // Should return a height of -2.25 + -4 + + { AZ::Vector2(-0.25f, 0.0f), -0.25f }, // Should return a height of -0.25 + 0 + { AZ::Vector2(-3.75f, 0.0f), -3.75f }, // Should return a height of -3.75 + 0 + { AZ::Vector2(0.0f, -0.25f), -0.25f }, // Should return a height of 0 + -0.25 + { AZ::Vector2(0.0f, -3.75f), -3.75f }, // Should return a height of 0 + -3.75 + + { AZ::Vector2(-2.0f, -3.75f), -5.75f }, // Should return a height of -2 + -3.75 + { AZ::Vector2(-2.25f, -4.0f), -6.25f }, // Should return a height of -2.25 + -4 + + // Queries inside a grid square (both axes are in-between grid points) + // This is a full bilinear interpolation, but because we're using X + Y for our heights, the interpolated values + // should *still* be X + Y assuming the points were sampled correctly from the grid points. + + { AZ::Vector2(3.25f, 5.25f), 8.5f }, // Should return a height of 3.25 + 5.25 + { AZ::Vector2(7.71f, 9.74f), 17.45f }, // Should return a height of 7.71 + 9.74 + + { AZ::Vector2(-3.25f, -5.25f), -8.5f }, // Should return a height of -3.25 + -5.25 + { AZ::Vector2(-7.71f, -9.74f), -17.45f }, // Should return a height of -7.71 + -9.74 + }; + + // Loop through every test point and validate it. + for (auto& testPoint : testPoints) + { + const float expectedHeight = testPoint.m_expectedHeight; + + AZ::Vector3 position(testPoint.m_testLocation.GetX(), testPoint.m_testLocation.GetY(), 0.0f); + bool heightQueryTerrainExists = false; + float height = m_terrainSystem->GetHeight( + position, AzFramework::Terrain::TerrainDataRequests::Sampler::BILINEAR, &heightQueryTerrainExists); + + // Verify that our height query returned the bilinear filtered result we expect. + constexpr float epsilon = 0.0001f; + EXPECT_NEAR(height, expectedHeight, epsilon); + } + } + + TEST_F(TerrainSystemTest, GetSurfaceWeightsReturnsAllValidSurfaceWeights) + { + CreateAndActivateTerrainSystem(); + + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne()); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + aabb, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(1.0f); + terrainExists = true; + }); + + const AZ::Crc32 tag1("tag1"); + const AZ::Crc32 tag2("tag2"); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights; + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight1; + tagWeight1.m_surfaceType = tag1; + tagWeight1.m_weight = 1.0f; + orderedSurfaceWeights.emplace(tagWeight1); + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight2; + tagWeight2.m_surfaceType = tag2; + tagWeight2.m_weight = 0.8f; + orderedSurfaceWeights.emplace(tagWeight2); + + NiceMock mockSurfaceRequests(entity->GetId()); + ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights)); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet outSurfaceWeights; + + // Asking for values outside the layer spawner bounds, should result in no results. + m_terrainSystem->GetSurfaceWeights(aabb.GetMax() + AZ::Vector3::CreateOne(), outSurfaceWeights); + EXPECT_TRUE(outSurfaceWeights.empty()); + + // Inside the layer spawner box should give us both the added surface weights. + m_terrainSystem->GetSurfaceWeights(aabb.GetCenter(), outSurfaceWeights); + + EXPECT_EQ(outSurfaceWeights.size(), 2); + } + + TEST_F(TerrainSystemTest, GetMaxSurfaceWeightsReturnsBiggestValidSurfaceWeight) + { + CreateAndActivateTerrainSystem(); + + const AZ::Aabb aabb = AZ::Aabb::CreateFromMinMax(AZ::Vector3::CreateZero(), AZ::Vector3::CreateOne()); + auto entity = CreateAndActivateMockTerrainLayerSpawner( + aabb, + [](AZ::Vector3& position, bool& terrainExists) + { + position.SetZ(1.0f); + terrainExists = true; + }); + + const AZ::Crc32 tag1("tag1"); + const AZ::Crc32 tag2("tag2"); + + AzFramework::SurfaceData::OrderedSurfaceTagWeightSet orderedSurfaceWeights; + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight1; + tagWeight1.m_surfaceType = tag1; + tagWeight1.m_weight = 1.0f; + orderedSurfaceWeights.emplace(tagWeight1); + + AzFramework::SurfaceData::SurfaceTagWeight tagWeight2; + tagWeight2.m_surfaceType = tag2; + tagWeight2.m_weight = 0.8f; + orderedSurfaceWeights.emplace(tagWeight2); + + NiceMock mockSurfaceRequests(entity->GetId()); + ON_CALL(mockSurfaceRequests, GetSurfaceWeights).WillByDefault(SetArgReferee<1>(orderedSurfaceWeights)); + + // Asking for values outside the layer spawner bounds, should result in an invalid result. + AzFramework::SurfaceData::SurfaceTagWeight tagWeight = + m_terrainSystem->GetMaxSurfaceWeight(aabb.GetMax() + AZ::Vector3::CreateOne()); + + EXPECT_EQ(tagWeight.m_surfaceType, AZ::Crc32(AzFramework::SurfaceData::Constants::s_unassignedTagName)); + + // Inside the layer spawner box should give us the highest weighted tag (tag1). + tagWeight = m_terrainSystem->GetMaxSurfaceWeight(aabb.GetCenter()); + + EXPECT_EQ(tagWeight.m_surfaceType, tagWeight1.m_surfaceType); + EXPECT_NEAR(tagWeight.m_weight, tagWeight1.m_weight, 0.01f); + } +} // namespace UnitTest diff --git a/Gems/Terrain/Code/terrain_editor_shared_files.cmake b/Gems/Terrain/Code/terrain_editor_shared_files.cmake index 334c89ec73..09724751a9 100644 --- a/Gems/Terrain/Code/terrain_editor_shared_files.cmake +++ b/Gems/Terrain/Code/terrain_editor_shared_files.cmake @@ -11,6 +11,8 @@ set(FILES Source/EditorComponents/EditorTerrainHeightGradientListComponent.h Source/EditorComponents/EditorTerrainLayerSpawnerComponent.cpp Source/EditorComponents/EditorTerrainLayerSpawnerComponent.h + Source/EditorComponents/EditorTerrainPhysicsColliderComponent.cpp + Source/EditorComponents/EditorTerrainPhysicsColliderComponent.h Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.cpp Source/EditorComponents/EditorTerrainSurfaceGradientListComponent.h Source/EditorComponents/EditorTerrainWorldComponent.cpp diff --git a/Gems/Terrain/Code/terrain_files.cmake b/Gems/Terrain/Code/terrain_files.cmake index 780dd1bdb0..893477e10d 100644 --- a/Gems/Terrain/Code/terrain_files.cmake +++ b/Gems/Terrain/Code/terrain_files.cmake @@ -12,6 +12,8 @@ set(FILES Source/Components/TerrainHeightGradientListComponent.h Source/Components/TerrainLayerSpawnerComponent.cpp Source/Components/TerrainLayerSpawnerComponent.h + Source/Components/TerrainPhysicsColliderComponent.cpp + Source/Components/TerrainPhysicsColliderComponent.h Source/Components/TerrainSurfaceDataSystemComponent.cpp Source/Components/TerrainSurfaceDataSystemComponent.h Source/Components/TerrainSurfaceGradientListComponent.cpp diff --git a/Gems/Terrain/Code/terrain_mocks_files.cmake b/Gems/Terrain/Code/terrain_mocks_files.cmake index 2aedd1c5d8..874e17f028 100644 --- a/Gems/Terrain/Code/terrain_mocks_files.cmake +++ b/Gems/Terrain/Code/terrain_mocks_files.cmake @@ -8,4 +8,6 @@ set(FILES Mocks/Terrain/MockTerrain.h + Mocks/Terrain/MockTerrainLayerSpawner.h + Mocks/Terrain/MockTerrainAreaSurfaceRequestBus.h ) diff --git a/Gems/Terrain/Code/terrain_tests_files.cmake b/Gems/Terrain/Code/terrain_tests_files.cmake index 793bc01ac5..3e37e509a7 100644 --- a/Gems/Terrain/Code/terrain_tests_files.cmake +++ b/Gems/Terrain/Code/terrain_tests_files.cmake @@ -10,6 +10,9 @@ set(FILES Tests/TerrainTest.cpp Tests/TerrainSystemTest.cpp Tests/LayerSpawnerTests.cpp + Tests/TerrainPhysicsColliderTests.cpp Tests/SurfaceMaterialsListTest.cpp Tests/MockAxisAlignedBoxShapeComponent.h + Tests/TerrainHeightGradientListTests.cpp + Tests/TerrainSurfaceGradientListTests.cpp ) diff --git a/Tools/LyTestTools/setup.py b/Tools/LyTestTools/setup.py index e23f914dab..a523cbc2dd 100755 --- a/Tools/LyTestTools/setup.py +++ b/Tools/LyTestTools/setup.py @@ -48,7 +48,8 @@ if __name__ == '__main__': 'ly_test_tools=ly_test_tools._internal.pytest_plugin.test_tools_fixtures', 'testrail_filter=ly_test_tools._internal.pytest_plugin.case_id', 'terminal_report=ly_test_tools._internal.pytest_plugin.terminal_report', - 'editor_test=ly_test_tools._internal.pytest_plugin.editor_test' + 'editor_test=ly_test_tools._internal.pytest_plugin.editor_test', + 'pytester=_pytest.pytester' ], }, ) diff --git a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake index d6fdc5cd9b..6210b9cc18 100644 --- a/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake +++ b/cmake/3rdParty/Platform/Linux/BuiltInPackages_linux.cmake @@ -39,7 +39,7 @@ ly_associate_package(PACKAGE_NAME libsamplerate-0.2.1-rev2-linux ly_associate_package(PACKAGE_NAME OpenSSL-1.1.1b-rev2-linux TARGETS OpenSSL PACKAGE_HASH b779426d1e9c5ddf71160d5ae2e639c3b956e0fb5e9fcaf9ce97c4526024e3bc) ly_associate_package(PACKAGE_NAME DirectXShaderCompilerDxc-1.6.2104-o3de-rev3-linux TARGETS DirectXShaderCompilerDxc PACKAGE_HASH 88c4a359325d749bc34090b9ac466424847f3b71ba0de15045cf355c17c07099) ly_associate_package(PACKAGE_NAME SPIRVCross-2021.04.29-rev1-linux TARGETS SPIRVCross PACKAGE_HASH 7889ee5460a688e9b910c0168b31445c0079d363affa07b25d4c8aeb608a0b80) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev2-linux TARGETS azslc PACKAGE_HASH 1ba84d8321a566d35a1e9aa7400211ba8e6d1c11c08e4be3c93e6e74b8f7aef1) +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-linux TARGETS azslc PACKAGE_HASH 6d7dc671936c34ff70d2632196107ca1b8b2b41acdd021bfbc69a9fd56215c22) ly_associate_package(PACKAGE_NAME zlib-1.2.11-rev5-linux TARGETS ZLIB PACKAGE_HASH 9be5ea85722fc27a8645a9c8a812669d107c68e6baa2ca0740872eaeb6a8b0fc) ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-linux TARGETS squish-ccr PACKAGE_HASH 85fecafbddc6a41a27c5f59ed4a5dfb123a94cb4666782cf26e63c0a4724c530) ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-linux TARGETS astc-encoder PACKAGE_HASH 2ba97a06474d609945f0ab4419af1f6bbffdd294ca6b869f5fcebec75c573c0f) diff --git a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake index 41df718b71..d054ba22e1 100644 --- a/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake +++ b/cmake/3rdParty/Platform/Mac/BuiltInPackages_mac.cmake @@ -17,7 +17,6 @@ ly_associate_package(PACKAGE_NAME cityhash-1.1-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev1-multiplatform TARGETS azslc PACKAGE_HASH 664439954bad54cc43731c684adbc1249d971ad7379fcd83ca8bba5e1cc4a2d0) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) @@ -43,4 +42,4 @@ ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-mac ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-mac TARGETS astc-encoder PACKAGE_HASH 96f6ea8c3e45ec7fe525230c7c53ca665c8300d8e28456cc19bb3159ce6f8dcc) ly_associate_package(PACKAGE_NAME ISPCTexComp-36b80aa-rev1-mac TARGETS ISPCTexComp PACKAGE_HASH 8a4e93277b8face6ea2fd57c6d017bdb55643ed3d6387110bc5f6b3b884dd169) ly_associate_package(PACKAGE_NAME lz4-1.9.3-vcpkg-rev4-mac TARGETS lz4 PACKAGE_HASH 891ff630bf34f7ab1d8eaee2ea0a8f1fca89dbdc63fca41ee592703dd488a73b) - +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-mac TARGETS azslc PACKAGE_HASH a9d81946b42ffa55c0d14d6a9249b3340e59a8fb8835e7a96c31df80f14723bc) diff --git a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake index cccd2591e8..fce2229771 100644 --- a/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake +++ b/cmake/3rdParty/Platform/Windows/BuiltInPackages_windows.cmake @@ -17,7 +17,6 @@ ly_associate_package(PACKAGE_NAME cityhash-1.1-multiplatform ly_associate_package(PACKAGE_NAME expat-2.1.0-multiplatform TARGETS expat PACKAGE_HASH 452256acd1fd699cef24162575b3524fccfb712f5321c83f1df1ce878de5b418) ly_associate_package(PACKAGE_NAME zstd-1.35-multiplatform TARGETS zstd PACKAGE_HASH 45d466c435f1095898578eedde85acf1fd27190e7ea99aeaa9acfd2f09e12665) ly_associate_package(PACKAGE_NAME SQLite-3.32.2-rev3-multiplatform TARGETS SQLite PACKAGE_HASH dd4d3de6cbb4ce3d15fc504ba0ae0587e515dc89a25228037035fc0aef4831f4) -ly_associate_package(PACKAGE_NAME azslc-1.7.23-rev1-multiplatform TARGETS azslc PACKAGE_HASH 664439954bad54cc43731c684adbc1249d971ad7379fcd83ca8bba5e1cc4a2d0) ly_associate_package(PACKAGE_NAME glad-2.0.0-beta-rev2-multiplatform TARGETS glad PACKAGE_HASH ff97ee9664e97d0854b52a3734c2289329d9f2b4cd69478df6d0ca1f1c9392ee) ly_associate_package(PACKAGE_NAME lux_core-2.2-rev5-multiplatform TARGETS lux_core PACKAGE_HASH c8c13cf7bc351643e1abd294d0841b24dee60e51647dff13db7aec396ad1e0b5) ly_associate_package(PACKAGE_NAME xxhash-0.7.4-rev1-multiplatform TARGETS xxhash PACKAGE_HASH e81f3e6c4065975833996dd1fcffe46c3cf0f9e3a4207ec5f4a1b564ba75861e) @@ -50,3 +49,4 @@ ly_associate_package(PACKAGE_NAME squish-ccr-deb557d-rev1-windows ly_associate_package(PACKAGE_NAME astc-encoder-3.2-rev1-windows TARGETS astc-encoder PACKAGE_HASH 3addc6fc1a7eb0d6b7f3d530e962af967e6d92b3825ef485da243346357cf78e) ly_associate_package(PACKAGE_NAME ISPCTexComp-36b80aa-rev1-windows TARGETS ISPCTexComp PACKAGE_HASH b6fa6ea28a2808a9a5524c72c37789c525925e435770f2d94eb2d387360fa2d0) ly_associate_package(PACKAGE_NAME lz4-1.9.3-vcpkg-rev4-windows TARGETS lz4 PACKAGE_HASH 4ea457b833cd8cfaf8e8e06ed6df601d3e6783b606bdbc44a677f77e19e0db16) +ly_associate_package(PACKAGE_NAME azslc-1.7.34-rev1-windows TARGETS azslc PACKAGE_HASH 44eb2e0fc4b0f1c75d0fb6f24c93a5753655b84dbc3e6ad45389ed3b9cf7a4b0) diff --git a/cmake/LYTestWrappers.cmake b/cmake/LYTestWrappers.cmake index 87649c5be6..9b139645fb 100644 --- a/cmake/LYTestWrappers.cmake +++ b/cmake/LYTestWrappers.cmake @@ -17,7 +17,7 @@ set(LY_GOOGLETEST_EXTRA_PARAMS CACHE STRING "Allows injection of additional opti find_package(Python REQUIRED MODULE) -ly_set(LY_PYTEST_EXECUTABLE ${LY_PYTHON_CMD} -B -m pytest -v --tb=short --show-capture=log -c ${LY_ROOT_FOLDER}/ctest_pytest.ini --build-directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") +ly_set(LY_PYTEST_EXECUTABLE ${LY_PYTHON_CMD} -B -m pytest -v --tb=short --show-capture=log -c ${LY_ROOT_FOLDER}/pytest.ini --build-directory "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/$") ly_set(LY_TEST_GLOBAL_KNOWN_SUITE_NAMES "smoke" "main" "periodic" "benchmark" "sandbox" "awsi") ly_set(LY_TEST_GLOBAL_KNOWN_REQUIREMENTS "gpu") diff --git a/cmake/Platform/Common/Install_common.cmake b/cmake/Platform/Common/Install_common.cmake index c453fadd2e..44cdeb1994 100644 --- a/cmake/Platform/Common/Install_common.cmake +++ b/cmake/Platform/Common/Install_common.cmake @@ -364,7 +364,7 @@ function(ly_setup_o3de_install) # Misc install(FILES - ${LY_ROOT_FOLDER}/ctest_pytest.ini + ${LY_ROOT_FOLDER}/pytest.ini ${LY_ROOT_FOLDER}/LICENSE.txt ${LY_ROOT_FOLDER}/README.md DESTINATION . diff --git a/ctest_pytest.ini b/pytest.ini similarity index 91% rename from ctest_pytest.ini rename to pytest.ini index 93310a522d..65c93e0eb2 100644 --- a/ctest_pytest.ini +++ b/pytest.ini @@ -7,10 +7,11 @@ # [pytest] -python_files = 'test_*.py' , '*_test.py' , '*_tests.py' +python_files = 'test_*.py' , '*_test.py' , '*_tests.py', 'TestSuite_*.py' norecursedirs = Python/2.7.* Python/3.7.5 BinTemp Cache SDKs JenkinsScripts cmake junit_family=legacy log_format=%(asctime)s [%(levelname)8s] %(message)s (%(filename)s:%(lineno)s) +addopts='--tb=short' '--show-capture=log' # primary suite markers which should appear on every filterable test and be mutually exclusive: markers = SUITE_smoke: Tiny, quick tests of fundamental operation (tests with no suite marker will also execute here in CI) diff --git a/python/get_python_path.bat b/python/get_python_path.bat new file mode 100644 index 0000000000..66942efd70 --- /dev/null +++ b/python/get_python_path.bat @@ -0,0 +1,33 @@ +@echo off +REM +REM Copyright (c) Contributors to the Open 3D Engine Project. +REM For complete copyright and license terms please see the LICENSE at the root of this distribution. +REM +REM SPDX-License-Identifier: Apache-2.0 OR MIT +REM +REM + +:: Retreives the path to the O3DE python executable(s) + +:: Skip initialization if already completed +IF "%O3DE_PYTHONHOME_INIT%"=="1" GOTO :END_OF_FILE + +SET CMD_DIR=%~dp0 + +:: Note, many DCC tools (like Maya) include thier own versioned python interpretter. +:: Some apps may not operate correctly if PYTHONHOME is set/propogated. +:: This is definitely the case with Maya, doing so causes Maya to not boot. +FOR /F "tokens=* USEBACKQ" %%F IN (`%CMD_DIR%\python.cmd %CMD_DIR%\get_python_path.py`) DO (SET O3DE_PYTHONHOME=%%F) +echo O3DE_PYTHONHOME - is now the folder containing O3DE python executable +echo O3DE_PYTHONHOME = %O3DE_PYTHONHOME% + +SET PYTHON=%O3DE_PYTHONHOME%\python.exe + +:: Set flag so we don't initialize dccsi environment twice +SET O3DE_PYTHONHOME_INIT=1 +GOTO END_OF_FILE + +:: Return to starting directory +POPD + +:END_OF_FILE diff --git a/python/get_python_path.py b/python/get_python_path.py new file mode 100644 index 0000000000..fc503ae4ab --- /dev/null +++ b/python/get_python_path.py @@ -0,0 +1,16 @@ +# coding:utf-8 +#!/usr/bin/python +# +# 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 +# +# +# ------------------------------------------------------------------------- +"""retreive the O3DE python path""" +import sys +from pathlib import Path +py_exe = Path(sys.executable) +py_dir = py_exe.parents[0] +print(py_dir.resolve()) \ No newline at end of file diff --git a/scripts/o3de/o3de/register.py b/scripts/o3de/o3de/register.py index c3d201f23c..0de161177c 100644 --- a/scripts/o3de/o3de/register.py +++ b/scripts/o3de/o3de/register.py @@ -467,7 +467,7 @@ def register_restricted_path(json_data: dict, def register_repo(json_data: dict, - repo_uri: str or pathlib.Path, + repo_uri: str, remove: bool = False) -> int: if not repo_uri: logger.error(f'Repo URI cannot be empty.') @@ -480,9 +480,9 @@ def register_repo(json_data: dict, while repo_uri in json_data['repos']: json_data['repos'].remove(repo_uri) else: - repo_uri = pathlib.Path(repo_uri).resolve() - while repo_uri.as_posix() in json_data['repos']: - json_data['repos'].remove(repo_uri.as_posix()) + repo_uri = pathlib.Path(repo_uri).resolve().as_posix() + while repo_uri in json_data['repos']: + json_data['repos'].remove(repo_uri) if remove: logger.warn(f'Removing repo uri {repo_uri}.') @@ -566,7 +566,7 @@ def register(engine_path: pathlib.Path = None, external_subdir_path: pathlib.Path = None, template_path: pathlib.Path = None, restricted_path: pathlib.Path = None, - repo_uri: str or pathlib.Path = None, + repo_uri: str = None, default_engines_folder: pathlib.Path = None, default_projects_folder: pathlib.Path = None, default_gems_folder: pathlib.Path = None, @@ -641,7 +641,7 @@ def register(engine_path: pathlib.Path = None, return 1 result = result or register_restricted_path(json_data, restricted_path, remove, project_path, engine_path) - if isinstance(repo_uri, str) or isinstance(repo_uri, pathlib.PurePath): + if isinstance(repo_uri, str): if not repo_uri: logger.error(f'Repo URI cannot be empty.') return 1