hydra_editor_utils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. """
  2. All or portions of this file Copyright (c) Amazon.com, Inc. or its affiliates or
  3. its licensors.
  4. For complete copyright and license terms please see the LICENSE at the root of this
  5. distribution (the "License"). All use of this software is governed by the License,
  6. or, if provided, by the license below or the license accompanying this file. Do not
  7. remove or modify any license notices. This file is distributed on an "AS IS" BASIS,
  8. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  9. """
  10. import azlmbr.bus as bus
  11. import azlmbr.editor as editor
  12. import azlmbr.entity as entity
  13. import azlmbr.object
  14. from typing import List
  15. from math import isclose
  16. import collections.abc
  17. def find_entity_by_name(entity_name):
  18. """
  19. Gets an entity ID from the entity with the given entity_name
  20. :param entity_name: String of entity name to search for
  21. :return entity ID
  22. """
  23. search_filter = entity.SearchFilter()
  24. search_filter.names = [entity_name]
  25. matching_entity_list = entity.SearchBus(bus.Broadcast, 'SearchEntities', search_filter)
  26. if matching_entity_list:
  27. matching_entity = matching_entity_list[0]
  28. if matching_entity.IsValid():
  29. print(f'{entity_name} entity found with ID {matching_entity.ToString()}')
  30. return matching_entity
  31. else:
  32. return matching_entity_list
  33. def get_component_type_id(component_name):
  34. """
  35. Gets the component_type_id from a given component name
  36. :param component_name: String of component name to search for
  37. :return component type ID
  38. """
  39. type_ids_list = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', [component_name],
  40. entity.EntityType().Game)
  41. component_type_id = type_ids_list[0]
  42. return component_type_id
  43. def add_level_component(component_name):
  44. """
  45. Adds the specified component to the Level Inspector
  46. :param component_name: String of component name to search for
  47. :return Component object.
  48. """
  49. level_component_list = [component_name]
  50. level_component_type_ids_list = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType',
  51. level_component_list, entity.EntityType().Level)
  52. level_component_outcome = editor.EditorLevelComponentAPIBus(bus.Broadcast, 'AddComponentsOfType',
  53. [level_component_type_ids_list[0]])
  54. level_component = level_component_outcome.GetValue()[0]
  55. return level_component
  56. def add_component(componentName, entityId):
  57. """
  58. Given a component name, finds component TypeId, adds to given entity, and verifies successful add/active state.
  59. :param componentName: String of component name to add.
  60. :param entityId: Entity to add component to.
  61. :return: Component object.
  62. """
  63. typeIdsList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', [componentName],
  64. entity.EntityType().Game)
  65. typeNamesList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeNames', typeIdsList)
  66. componentOutcome = editor.EditorComponentAPIBus(bus.Broadcast, 'AddComponentsOfType', entityId, typeIdsList)
  67. isActive = editor.EditorComponentAPIBus(bus.Broadcast, 'IsComponentEnabled', componentOutcome.GetValue()[0])
  68. hasComponent = editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entityId, typeIdsList[0])
  69. if componentOutcome.IsSuccess() and isActive:
  70. print('{} component was added to entity'.format(typeNamesList[0]))
  71. elif componentOutcome.IsSuccess() and not isActive:
  72. print('{} component was added to entity, but the component is disabled'.format(typeNamesList[0]))
  73. elif not componentOutcome.IsSuccess():
  74. print('Failed to add {} component to entity'.format(typeNamesList[0]))
  75. if hasComponent:
  76. print('Entity has a {} component'.format(typeNamesList[0]))
  77. return componentOutcome.GetValue()[0]
  78. def remove_component(component_name, entity_id):
  79. """
  80. Removes the specified component from the specified entity.
  81. :param component_name: String of component name to remove.
  82. :param entity_id: Entity to remove component from.
  83. :return: EntityComponentIdPair if removal was successful, else None.
  84. """
  85. type_ids_list = [get_component_type_id(component_name)]
  86. outcome = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentOfType', entity_id, type_ids_list[0])
  87. if outcome.IsSuccess():
  88. component_entity_pair = outcome.GetValue()
  89. editor.EditorComponentAPIBus(bus.Broadcast, 'RemoveComponents', [component_entity_pair])
  90. has_component = editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entity_id, type_ids_list[0])
  91. if has_component:
  92. print(f"Failed to remove {component_name}")
  93. return None
  94. else:
  95. print(f"{component_name} was successfully removed")
  96. return component_entity_pair
  97. else:
  98. print(f"{component_name} not found on entity")
  99. return None
  100. def get_component_property_value(component, component_propertyPath):
  101. """
  102. Given a component name and component property path, outputs the property's value.
  103. :param component: Component object to act on.
  104. :param componentPropertyPath: String of component property. (e.g. 'Settings|Visible')
  105. :return: Value set in given componentPropertyPath
  106. """
  107. componentPropertyObj = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentProperty', component,
  108. component_propertyPath)
  109. if componentPropertyObj.IsSuccess():
  110. componentProperty = componentPropertyObj.GetValue()
  111. print(f'{component_propertyPath} set to {componentProperty}')
  112. return componentProperty
  113. else:
  114. print(f'FAILURE: Could not get value from {component_propertyPath}')
  115. return None
  116. def get_property_tree(component):
  117. """
  118. Given a configured component object, prints the property tree info from that component
  119. :param component: Component object to act on.
  120. """
  121. pteObj = editor.EditorComponentAPIBus(bus.Broadcast, 'BuildComponentPropertyTreeEditor', component)
  122. pte = pteObj.GetValue()
  123. print(pte.build_paths_list())
  124. return pte
  125. def compare_values(first_object: object, second_object: object, name: str) -> bool:
  126. # Quick case - can we just directly compare the two objects successfully?
  127. if (first_object == second_object):
  128. result = True
  129. # No, so get a lot more specific
  130. elif isinstance(first_object, collections.abc.Container):
  131. # If they aren't both containers, they're different
  132. if not isinstance(second_object, collections.abc.Container):
  133. result = False
  134. # If they have different lengths, they're different
  135. elif len(first_object) != len (second_object):
  136. result = False
  137. # If they're different strings, they're containers but they failed the == check so
  138. # we know they're different
  139. elif isinstance(first_object, str):
  140. result = False
  141. else:
  142. # It's a collection of values, so iterate through them all...
  143. collection_idx = 0
  144. result = True
  145. for val1, val2 in zip(first_object, second_object):
  146. result = result and compare_values(val1, val2, f"{name} (index [{collection_idx}])")
  147. collection_idx = collection_idx + 1
  148. else:
  149. # Do approximate comparisons for floats
  150. if isinstance(first_object, float) and isclose(first_object, second_object, rel_tol=0.001):
  151. result = True
  152. # We currently don't have a generic way to compare PythonProxyObject contents, so return a
  153. # false positive result for now.
  154. elif isinstance(first_object, azlmbr.object.PythonProxyObject):
  155. print(f"{name}: validation inconclusive, the two objects cannot be directly compared.")
  156. result = True
  157. else:
  158. result = False
  159. if not result:
  160. print(f"compare_values failed: {first_object} ({type(first_object)}) vs {second_object} ({type(second_object)})")
  161. print(f"{name}: {'SUCCESS' if result else 'FAILURE'}")
  162. return result
  163. class Entity:
  164. """
  165. Entity class used to create entity objects
  166. :param name: String for the name of the Entity
  167. :param id: The ID of the entity
  168. """
  169. def __init__(self, name: str, id: object = entity.EntityId()):
  170. self.name: str = name
  171. self.id: object = id
  172. self.components: List[object] = None
  173. self.parent_id = None
  174. self.parent_name = None
  175. def create_entity(self, entity_position, components, parent_id=entity.EntityId()):
  176. self.id = editor.ToolsApplicationRequestBus(
  177. bus.Broadcast, "CreateNewEntityAtPosition", entity_position, parent_id
  178. )
  179. if self.id.IsValid():
  180. print(f"{self.name} Entity successfully created")
  181. editor.EditorEntityAPIBus(bus.Event, 'SetName', self.id, self.name)
  182. self.components = []
  183. for component in components:
  184. self.add_component(component)
  185. def add_component(self, component):
  186. new_component = add_component(component, self.id)
  187. self.components.append(new_component)
  188. def remove_component(self, component):
  189. removed_component = remove_component(component, self.id)
  190. if removed_component is not None:
  191. self.components.remove(removed_component)
  192. def get_parent_info(self):
  193. """
  194. Sets the value for parent_id and parent_name on the entity (self)
  195. Prints the string for papertrail
  196. :return: None
  197. """
  198. self.parent_id = editor.EditorEntityInfoRequestBus(bus.Event, "GetParent", self.id)
  199. self.parent_name = editor.EditorEntityInfoRequestBus(bus.Event, "GetName", self.parent_id)
  200. print(f"The parent entity of {self.name} is {self.parent_name}")
  201. def set_test_parent_entity(self, parent_entity_obj):
  202. editor.EditorEntityAPIBus(bus.Event, "SetParent", self.id, parent_entity_obj.id)
  203. self.get_parent_info()
  204. def get_set_test(self, component_index: int, path: str, value: object, expected_result: object = None) -> bool:
  205. """
  206. Used to set and validate changes in component values
  207. :param component_index: Index location in the self.components list
  208. :param path: asset path in the component
  209. :param value: new value for the variable being changed in the component
  210. :param expected_result: (optional) check the result against a specific expected value
  211. """
  212. if expected_result is None:
  213. expected_result = value
  214. # Test Get/Set (get old value, set new value, check that new value was set correctly)
  215. print(f"Entity {self.name} Path {path} Component Index {component_index} ")
  216. component = self.components[component_index]
  217. old_value = get_component_property_value(component, path)
  218. if old_value is not None:
  219. print(f"SUCCESS: Retrieved property Value for {self.name}")
  220. else:
  221. print(f"FAILURE: Failed to find value in {self.name} {path}")
  222. return False
  223. if old_value == expected_result:
  224. print((f"WARNING: get_set_test on {self.name} is setting the same value that already exists ({old_value})."
  225. "The set results will be inconclusive."))
  226. editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, value)
  227. new_value = get_component_property_value(self.components[component_index], path)
  228. if new_value is not None:
  229. print(f"SUCCESS: Retrieved new property Value for {self.name}")
  230. else:
  231. print(f"FAILURE: Failed to find new value in {self.name}")
  232. return False
  233. return compare_values(new_value, expected_result, f"{self.name} {path}")
  234. def get_set_test(entity: object, component_index: int, path: str, value: object) -> bool:
  235. """
  236. Used to set and validate changes in component values
  237. :param component_index: Index location in the entity.components list
  238. :param path: asset path in the component
  239. :param value: new value for the variable being changed in the component
  240. """
  241. return entity.get_set_test(component_index, path, value)
  242. def get_set_property_test(ly_object: object, attribute_name: str, value: object, expected_result: object = None) -> bool:
  243. """
  244. Used to set and validate BehaviorContext property changes in Lumberyard objects
  245. :param ly_object: The lumberyard object to test
  246. :param attribute_name: property (attribute) name in the BehaviorContext
  247. :param value: new value for the variable being changed in the component
  248. :param expected_result: (optional) check the result against a specific expected value other than the one set
  249. """
  250. if expected_result is None:
  251. expected_result = value
  252. # Test Get/Set (get old value, set new value, check that new value was set correctly)
  253. print(f"Attempting to set {ly_object.typename}.{attribute_name} = {value} (expected result is {expected_result})")
  254. if hasattr(ly_object, attribute_name):
  255. print(f"SUCCESS: Located attribute {attribute_name} for {ly_object.typename}")
  256. else:
  257. print(f"FAILURE: Failed to find attribute {attribute_name} in {ly_object.typename}")
  258. return False
  259. old_value = getattr(ly_object, attribute_name)
  260. if old_value is not None:
  261. print(f"SUCCESS: Retrieved existing value {old_value} for {attribute_name} in {ly_object.typename}")
  262. else:
  263. print(f"FAILURE: Failed to retrieve value for {attribute_name} in {ly_object.typename}")
  264. return False
  265. if old_value == expected_result:
  266. print((f"WARNING: get_set_test on {attribute_name} is setting the same value that already exists ({old_value})."
  267. "The 'set' result for the test will be inconclusive."))
  268. setattr(ly_object, attribute_name, expected_result)
  269. new_value = getattr(ly_object, attribute_name)
  270. if new_value is not None:
  271. print(f"SUCCESS: Retrieved new value {new_value} for {attribute_name} in {ly_object.typename}")
  272. else:
  273. print(f"FAILURE: Failed to retrieve value for {attribute_name} in {ly_object.typename}")
  274. return False
  275. return compare_values(new_value, expected_result, f"{ly_object.typename}.{attribute_name}")
  276. def has_components(entity_id: object, component_list: list) -> bool:
  277. """
  278. Used to verify if a given entity has all the components of components_list. Returns True if all the
  279. components are present, else False
  280. :param entity_id: entity id of the entity
  281. :param component_list: list of component names to be verified
  282. """
  283. typeIdsList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', component_list,
  284. entity.EntityType().Game)
  285. for type_id in typeIdsList:
  286. if not editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entity_id, type_id):
  287. return False
  288. return True
  289. class PathNotFoundError(Exception):
  290. def __init__(self, path):
  291. self.path = path
  292. def __str__(self):
  293. return f"Path \"{self.path}\" not found in Editor Settings"
  294. def get_editor_settings_path_list():
  295. """
  296. Get the list of Editor Settings paths
  297. """
  298. paths = editor.EditorSettingsAPIBus(bus.Broadcast, 'BuildSettingsList')
  299. return paths
  300. def get_editor_settings_by_path(path):
  301. """
  302. Get the value of Editor Settings based on the path.
  303. :param path: path to the Editor Settings to get the value
  304. """
  305. if path not in get_editor_settings_path_list():
  306. raise PathNotFoundError(path)
  307. outcome = editor.EditorSettingsAPIBus(bus.Broadcast, 'GetValue', path)
  308. if outcome.isSuccess():
  309. return outcome.GetValue()
  310. raise RuntimeError(f"GetValue for path '{path}' failed")
  311. def set_editor_settings_by_path(path, value, is_bool = False):
  312. """
  313. Set the value of Editor Settings based on the path.
  314. # NOTE: Some Editor Settings may need an Editor restart to apply.
  315. # Ex: Enabling or disabling New Viewport Interaction Model
  316. :param path: path to the Editor Settings to get the value
  317. :param value: value to be set
  318. :param is_bool: True for Boolean settings (enable/disable), False for other settings
  319. """
  320. if path not in get_editor_settings_path_list():
  321. raise PathNotFoundError(path)
  322. if is_bool and not isinstance(value, bool):
  323. def ParseBoolValue(value):
  324. if(value == "0"):
  325. return False
  326. return True
  327. value = ParseBoolValue(value)
  328. outcome = editor.EditorSettingsAPIBus(bus.Broadcast, 'SetValue', path, value)
  329. if not outcome.isSuccess():
  330. raise RuntimeError(f"SetValue for path '{path}' failed")
  331. print(f"Value for path '{path}' is set to {value}")
  332. def get_component_type_id_map(component_name_list):
  333. """
  334. Given a list of component names, returns a map of component name -> component type id
  335. :param component_name_list: The lumberyard object to test
  336. :return: Dictionary of component name -> component type id pairs
  337. """
  338. # Remove any duplicates so we don't have to query for the same TypeId
  339. component_names = list(set(component_name_list))
  340. type_ids_by_component = {}
  341. type_ids = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', component_names,
  342. entity.EntityType().Game)
  343. for i, typeId in enumerate(type_ids):
  344. type_ids_by_component[component_names[i]] = typeId
  345. return type_ids_by_component