text: """
  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
  """
  
  
  # Test case ID : C4976210
  # Test Case Title : Verify that when Compute COM is disabled, the user gets an option to add the co-ordinates of
  #                       the COM and the COM gets implemented at those co-ordinates.
  
  
  
  import os
  import sys
  
  
  # fmt: off
  class Tests:
      enter_game_mode         = ("Entered game mode",                                       "Failed to enter game mode")
      boxes_validated         = ("All box entities were found and validated",               "Couldn't find or validate at least one box")
      test_objects_validated  = ("All test entities were found and validated",              "Not all test entities could be found and validated")
      sphere_com_set          = ("The Test Sphere's COM was successfully set",              "The Test Sphere's COM WAS NOT successfully set")
      capsule_com_set         = ("The Test Capsule's COM was successfully set",             "The Test Capsule's COM WAS NOT successfully set")
      box_com_set             = ("The Test Box's COM was successfully set",                 "The Test Box's COM WAS NOT successfully set")
      sphere_confirm          = ("The Test Sphere rolled uphill due to COM offset",         "The Test Sphere rolled downhill-- COM did not work as expected")
      capsule_confirm         = ("The Test Capsule fell against gravity due to COM offset", "The Test Capsule fell with gravity-- COM did not work as expected")
      box_confirm             = ("The Test Box fell against gravity due to COM offset",     "The Test Box fell with gravity-- COM did not work as expected")
      exit_game_mode          = ("Exited game mode",                                        "Couldn't exit game mode")
  # fmt: on
  
  
  def C4976210_COM_ManualSetting():
      # type: () -> None
      """
      Summary:
      Tests that when an entity has "Compute COM" disabled, a user can set a custom center of mass and that
      the set center of mass behaves as expected.
  
      Level Description:
      Three test objects (Box, Capsule and Sphere) are positioned on a platform that is rotated at an acute angle. Two
      plates (Pass and Fail) are perpendicular to the platform so that the Fail Plate is underneath the test objects, and
      the Pass Plate is above. Gravity is enabled for all test objects. Stationary objects (plates, platform... etc)
      have gravity disabled and have masses set for "very large numbers" (10,000 kg) to resist the test objects'
      movements. Test objects have their "compute COM" property disabled, but in the editor have a computed COM of
      (0.0, 0.0, 0.0) and gravity enabled.
  
      Expected Behavior:
      When game mode starts, the script manually sets each test objects center of mass in a way where that entity
      will "fall against gravity". The box and capsule should tilt against gravity into the Pass Plate, and the sphere
      should "roll up hill" to the Pass Plate.
  
      Test Steps:
      0) Define data and functions
      1) Open level, enter game mode
      2) Find and validate entities
      3) set and verify center of mass (COM)
      4) Wait for results or timeout
      5) Log results
      6) Close the editor
  
      Note:
      - This test file must be called from the Open 3D Engine Editor command terminal
      - Any passed and failed tests are written to the Editor.log file.
              Parsing the file or running a log_monitor are required to observe the test results.
  
      :return: None
      """
  
      # internal editor imports
      import ImportPathHelper as imports
  
      imports.init()
  
      from editor_python_test_tools.utils import Report
      from editor_python_test_tools.utils import TestHelper as helper
      import azlmbr.legacy.general as general
      import azlmbr.bus
      import azlmbr
      import azlmbr.math as azmath
  
      # 0) Set up data and functions used in the test
  
      # EntityData class for storing and organizing useful test information/results
      class EntityData:
          def __init__(self, name):
              self.name = name
              self.id = None
              self.init_pos = None
              self.current_pos = None
              self.init_rot = None
              self.current_rot = None
              self.result = None
              self.fail_info = None
  
      # ***** Global variables ****
  
      # Constants
      TIME_OUT = 1.5
      CAPSULE_BOX_OFFSET = azmath.Vector3(3.0, 0.0, 0.0)
      SPHERE_OFFSET = azmath.Vector3(0.5, 0.0, 0.0)
      PASS = "pass"
      FAIL = "fail"
  
      # Test entities
      test_box = EntityData("Test_Box")
      test_capsule = EntityData("Test_Capsule")
      test_sphere = EntityData("Test_Sphere")
      test_entities = [test_box, test_capsule, test_sphere]
  
      # Plate entities
      pass_plate = EntityData("Pass_Plate")
      fail_plate = EntityData("Fail_Plate")
  
      # All non-moving entities
      non_movers = [EntityData("Platform"), EntityData("Pass_Buffer"), EntityData("Fail_Buffer"), pass_plate, fail_plate]
  
      # Full entity list
      all_entities = non_movers + test_entities
  
      # ******** Helper Functions ********
  
      # Attempts to set COM, and manually computes expected COM to verify it was applied correctly
      def set_and_validate_COM(entity, com_offset):
          # type: (EntityData, Vector3) -> None
          CLOSE_ENOUGH_THRESHOLD = 0.01
          # Set COM offset
          azlmbr.physics.RigidBodyRequestBus(azlmbr.bus.Event, "SetCenterOfMassOffset", entity.id, com_offset)
          entity_world_com_pos = azlmbr.physics.RigidBodyRequestBus(azlmbr.bus.Event, "GetCenterOfMassWorld", entity.id)
          # Get world rotation matrix
          tm_matrix = azmath.Matrix3x3_CreateFromTransform(
              azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTM", entity.id)
          )
          # Calculate expected world COM
          world_pos = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", entity.id)
          calculated_com = tm_matrix.MultiplyVector3(com_offset).Add(world_pos)
          # If expected COM and actual COM differ, log data collected
          if not entity_world_com_pos.IsClose(calculated_com, CLOSE_ENOUGH_THRESHOLD):
              Report.info("COM not applied to entity: {}".format(entity.name))
              Report.info_vector3(world_pos, " Entity position:")
              Report.info_vector3(com_offset, " COM offset:")
              Report.info_vector3(entity_world_com_pos, " Entity world COM:")
              Report.info_vector3(calculated_com, " Expected COM")
              return False
          return True
  
      # ** Entity batch operation helpers **
  
      # Attempts to validate entities' IDs and initial positions and initial rotations.
      #   Returns True if all entities were successfully validated.
      #   Print to the log if there are any problems retrieving vital information
      def validate_entities(entity_list):
          # type: ([EntityData]) -> int
          count = 0
          for entity in entity_list:
              valid = True
              entity.id = general.find_game_entity(entity.name)
              if not entity.id.IsValid():
                  valid = False
                  Report.info("Entity: {} could not be validated".format(entity.name))
              entity.init_pos = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", entity.id)
              entity.current_pos = entity.init_pos
              if entity.init_pos is None or entity.init_pos.IsZero():
                  valid = False
                  Report.info("Entity: {}'s initial position could not be found".format(entity.name))
              entity.init_rot = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldRotation", entity.id)
              entity.current_rot = entity.init_rot
              if entity.init_rot is None:
                  valid = False
                  Report.info("Entity: {}'s initial rotation could not be found".format(entity.name))
              if valid:
                  count += 1
          return count == len(entity_list)
  
      # Updates entities' current position
      def update_pos_and_rot(entity_list):
          # type: ([EntityData]) -> None
          for entity in entity_list:
              entity.current_pos = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldTranslation", entity.id)
              entity.current_rot = azlmbr.components.TransformBus(azlmbr.bus.Event, "GetWorldRotation", entity.id)
  
      # Checks for unexpected movement in stationary objects
      #   Prints to the log if there is a difference between initial position and current position
      def check_for_unexpected_movement(entity_list):
          # type: ([EntityData]) -> None
          CLOSE_ENOUGH_THRESHOLD = 0.1
          for entity in entity_list:
              if not entity.init_pos.IsClose(entity.current_pos, CLOSE_ENOUGH_THRESHOLD):
                  entity.result = FAIL
                  entity.fail_info = "Unexpected movement detected"
  
      # Checks if we have enough information to end the test
      def done_collecting_results(entity_list, num_results):
          # type: ([EntityData], int) -> bool
          result_count = 0
          for entity in entity_list:
              if entity.result is not None:
                  result_count += 1
  
          # when all spheres have a result we are done
          if result_count == num_results:
              return True
          return False
  
      # ****** single update function to pass ******
      #        to helper.wait_for_condition()
  
      # Should be called every frame:
      #  Updates all entities positions and rotations
      #  Returns True if exit conditions for test are met
      #    see @done_collecting_results(..) for exit conditions
      def is_done_updating():
          # type: () -> bool
          update_pos_and_rot(all_entities)
          check_for_unexpected_movement(non_movers)
          return done_collecting_results(test_entities, len(test_entities))
  
      # ******** Event Handlers ********
  
      # Fail A Box collision event handler
      def on_collision_begin_fail(args):
          # type: ([EntityId, ...]) -> None
          collider_id = args[0]
          for entity in test_entities:
              if entity.id.Equal(collider_id) and entity.result is None:
                  entity.result = FAIL
                  entity.fail_info = "Collide with fail plate: COM did not apply properly"
                  return
  
      # Pass Box Collision Event Handler
      def on_collision_begin_pass(args):
          # type: ([EntityId, ...]) -> None
          collider_id = args[0]
          for entity in test_entities:
              if entity.id.Equal(collider_id) and entity.result is None:
                  Report.info("Entity: {} collided with the Pass Plate".format(entity.name))
                  entity.result = PASS
                  return
          # it wasn't a test entity that collided, something went wrong
          if collider_id.IsValid():
              entity_name = azlmbr.entity.GameEntityContextRequestBus(azlmbr.bus.Broadcast, "GetEntityName", collider_id)
              Report.info("Pass Plate collided with unexpected entity: {}".format(entity_name))
  
      # ******** Execution Code *********
  
      # 1) Open level and start game mode
      helper.init_idle()
      helper.open_level("Physics", "C4976210_COM_ManualSetting")
      helper.enter_game_mode(Tests.enter_game_mode)
  
      # 2) Find and validate test entities
      Report.critical_result(Tests.test_objects_validated, validate_entities(test_entities))
      Report.critical_result(Tests.boxes_validated, validate_entities(non_movers))
  
      # 3) Set and verify COM offsets
      Report.critical_result(Tests.box_com_set, set_and_validate_COM(test_box, CAPSULE_BOX_OFFSET))
      Report.critical_result(Tests.capsule_com_set, set_and_validate_COM(test_capsule, CAPSULE_BOX_OFFSET))
      Report.critical_result(Tests.sphere_com_set, set_and_validate_COM(test_sphere, SPHERE_OFFSET))
  
      # Assign handlers
      pass_handler = azlmbr.physics.CollisionNotificationBusHandler()
      pass_handler.connect(pass_plate.id)
      pass_handler.add_callback("OnCollisionBegin", on_collision_begin_pass)
  
      fail_handler = azlmbr.physics.CollisionNotificationBusHandler()
      fail_handler.connect(fail_plate.id)
      fail_handler.add_callback("OnCollisionBegin", on_collision_begin_fail)
  
      # 4) wait for either time out or for the test to complete
      if not helper.wait_for_condition(is_done_updating, TIME_OUT):
          Report.info("The test timed out, check log for possible solutions or adjust the time out time")
  
      # 5) Log results
  
      # verify entities results
      Report.result(Tests.box_confirm, test_box.result is not None and test_box.result == PASS)
      Report.result(Tests.capsule_confirm, test_capsule.result is not None and test_capsule.result == PASS)
      Report.result(Tests.sphere_confirm, test_sphere.result is not None and test_sphere.result == PASS)
  
      # Data dump at bottom of log
      Report.info("******** Collected Data *********")
      for entity in all_entities:
          Report.info("Entity: {}".format(entity.name))
          Report.info_vector3(entity.init_pos, " Initial position:")
          Report.info_vector3(entity.current_pos, " Final position:")
          Report.info_vector3(entity.init_rot, " Initial rotation:")
          Report.info_vector3(entity.current_rot, " Final rotation:")
          Report.info(" Result: {}".format(entity.result))
          if entity.fail_info is not None:
              Report.info("   Fail info: {}".format(entity.fail_info))
          Report.info("********************************")
  
      # 6) Exit Game mode
      helper.exit_game_mode(Tests.exit_game_mode)
  
      Report.info("*** FINISHED TEST ***")
  
  
  if __name__ == "__main__":
      import ImportPathHelper as imports
      imports.init()
  
      from editor_python_test_tools.utils import Report
      Report.start_test(C4976210_COM_ManualSetting)
  