hydra_editor_utils.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  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 add_component_of_type(componentTypeId, entityId):
  79. typeIdsList = [componentTypeId]
  80. componentOutcome = editor.EditorComponentAPIBus(
  81. azlmbr.bus.Broadcast, 'AddComponentsOfType', entityId, typeIdsList)
  82. return componentOutcome.GetValue()[0]
  83. def remove_component(component_name, entity_id):
  84. """
  85. Removes the specified component from the specified entity.
  86. :param component_name: String of component name to remove.
  87. :param entity_id: Entity to remove component from.
  88. :return: EntityComponentIdPair if removal was successful, else None.
  89. """
  90. type_ids_list = [get_component_type_id(component_name)]
  91. outcome = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentOfType', entity_id, type_ids_list[0])
  92. if outcome.IsSuccess():
  93. component_entity_pair = outcome.GetValue()
  94. editor.EditorComponentAPIBus(bus.Broadcast, 'RemoveComponents', [component_entity_pair])
  95. has_component = editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entity_id, type_ids_list[0])
  96. if has_component:
  97. print(f"Failed to remove {component_name}")
  98. return None
  99. else:
  100. print(f"{component_name} was successfully removed")
  101. return component_entity_pair
  102. else:
  103. print(f"{component_name} not found on entity")
  104. return None
  105. def get_component_property_value(component, component_propertyPath):
  106. """
  107. Given a component name and component property path, outputs the property's value.
  108. :param component: Component object to act on.
  109. :param componentPropertyPath: String of component property. (e.g. 'Settings|Visible')
  110. :return: Value set in given componentPropertyPath
  111. """
  112. componentPropertyObj = editor.EditorComponentAPIBus(bus.Broadcast, 'GetComponentProperty', component,
  113. component_propertyPath)
  114. if componentPropertyObj.IsSuccess():
  115. componentProperty = componentPropertyObj.GetValue()
  116. print(f'{component_propertyPath} set to {componentProperty}')
  117. return componentProperty
  118. else:
  119. print(f'FAILURE: Could not get value from {component_propertyPath}')
  120. return None
  121. def get_property_tree(component):
  122. """
  123. Given a configured component object, prints the property tree info from that component
  124. :param component: Component object to act on.
  125. """
  126. pteObj = editor.EditorComponentAPIBus(bus.Broadcast, 'BuildComponentPropertyTreeEditor', component)
  127. pte = pteObj.GetValue()
  128. print(pte.build_paths_list())
  129. return pte
  130. def compare_values(first_object: object, second_object: object, name: str) -> bool:
  131. # Quick case - can we just directly compare the two objects successfully?
  132. if (first_object == second_object):
  133. result = True
  134. # No, so get a lot more specific
  135. elif isinstance(first_object, collections.abc.Container):
  136. # If they aren't both containers, they're different
  137. if not isinstance(second_object, collections.abc.Container):
  138. result = False
  139. # If they have different lengths, they're different
  140. elif len(first_object) != len (second_object):
  141. result = False
  142. # If they're different strings, they're containers but they failed the == check so
  143. # we know they're different
  144. elif isinstance(first_object, str):
  145. result = False
  146. else:
  147. # It's a collection of values, so iterate through them all...
  148. collection_idx = 0
  149. result = True
  150. for val1, val2 in zip(first_object, second_object):
  151. result = result and compare_values(val1, val2, f"{name} (index [{collection_idx}])")
  152. collection_idx = collection_idx + 1
  153. else:
  154. # Do approximate comparisons for floats
  155. if isinstance(first_object, float) and isclose(first_object, second_object, rel_tol=0.001):
  156. result = True
  157. # We currently don't have a generic way to compare PythonProxyObject contents, so return a
  158. # false positive result for now.
  159. elif isinstance(first_object, azlmbr.object.PythonProxyObject):
  160. print(f"{name}: validation inconclusive, the two objects cannot be directly compared.")
  161. result = True
  162. else:
  163. result = False
  164. if not result:
  165. print(f"compare_values failed: {first_object} ({type(first_object)}) vs {second_object} ({type(second_object)})")
  166. print(f"{name}: {'SUCCESS' if result else 'FAILURE'}")
  167. return result
  168. class Entity:
  169. """
  170. Entity class used to create entity objects
  171. :param name: String for the name of the Entity
  172. :param id: The ID of the entity
  173. """
  174. def __init__(self, name: str, id: object = entity.EntityId()):
  175. self.name: str = name
  176. self.id: object = id
  177. self.components: List[object] = None
  178. self.parent_id = None
  179. self.parent_name = None
  180. def create_entity(self, entity_position, components, parent_id=entity.EntityId()):
  181. self.id = editor.ToolsApplicationRequestBus(
  182. bus.Broadcast, "CreateNewEntityAtPosition", entity_position, parent_id
  183. )
  184. if self.id.IsValid():
  185. print(f"{self.name} Entity successfully created")
  186. editor.EditorEntityAPIBus(bus.Event, 'SetName', self.id, self.name)
  187. self.components = []
  188. for component in components:
  189. self.add_component(component)
  190. def add_component(self, component):
  191. new_component = add_component(component, self.id)
  192. self.components.append(new_component)
  193. def add_component_of_type(self, componentTypeId):
  194. new_component = add_component_of_type(componentTypeId, self.id)
  195. self.components.append(new_component)
  196. def remove_component(self, component):
  197. removed_component = remove_component(component, self.id)
  198. if removed_component is not None:
  199. self.components.remove(removed_component)
  200. def get_parent_info(self):
  201. """
  202. Sets the value for parent_id and parent_name on the entity (self)
  203. Prints the string for papertrail
  204. :return: None
  205. """
  206. self.parent_id = editor.EditorEntityInfoRequestBus(bus.Event, "GetParent", self.id)
  207. self.parent_name = editor.EditorEntityInfoRequestBus(bus.Event, "GetName", self.parent_id)
  208. print(f"The parent entity of {self.name} is {self.parent_name}")
  209. def set_test_parent_entity(self, parent_entity_obj):
  210. editor.EditorEntityAPIBus(bus.Event, "SetParent", self.id, parent_entity_obj.id)
  211. self.get_parent_info()
  212. def get_set_test(self, component_index: int, path: str, value: object, expected_result: object = None) -> bool:
  213. """
  214. Used to set and validate changes in component values
  215. :param component_index: Index location in the self.components list
  216. :param path: asset path in the component
  217. :param value: new value for the variable being changed in the component
  218. :param expected_result: (optional) check the result against a specific expected value
  219. """
  220. if expected_result is None:
  221. expected_result = value
  222. # Test Get/Set (get old value, set new value, check that new value was set correctly)
  223. print(f"Entity {self.name} Path {path} Component Index {component_index} ")
  224. component = self.components[component_index]
  225. old_value = get_component_property_value(component, path)
  226. if old_value is not None:
  227. print(f"SUCCESS: Retrieved property Value for {self.name}")
  228. else:
  229. print(f"FAILURE: Failed to find value in {self.name} {path}")
  230. return False
  231. if old_value == expected_result:
  232. print((f"WARNING: get_set_test on {self.name} is setting the same value that already exists ({old_value})."
  233. "The set results will be inconclusive."))
  234. editor.EditorComponentAPIBus(bus.Broadcast, "SetComponentProperty", component, path, value)
  235. new_value = get_component_property_value(self.components[component_index], path)
  236. if new_value is not None:
  237. print(f"SUCCESS: Retrieved new property Value for {self.name}")
  238. else:
  239. print(f"FAILURE: Failed to find new value in {self.name}")
  240. return False
  241. return compare_values(new_value, expected_result, f"{self.name} {path}")
  242. def get_set_test(entity: object, component_index: int, path: str, value: object) -> bool:
  243. """
  244. Used to set and validate changes in component values
  245. :param component_index: Index location in the entity.components list
  246. :param path: asset path in the component
  247. :param value: new value for the variable being changed in the component
  248. """
  249. return entity.get_set_test(component_index, path, value)
  250. def get_set_property_test(ly_object: object, attribute_name: str, value: object, expected_result: object = None) -> bool:
  251. """
  252. Used to set and validate BehaviorContext property changes in Lumberyard objects
  253. :param ly_object: The lumberyard object to test
  254. :param attribute_name: property (attribute) name in the BehaviorContext
  255. :param value: new value for the variable being changed in the component
  256. :param expected_result: (optional) check the result against a specific expected value other than the one set
  257. """
  258. if expected_result is None:
  259. expected_result = value
  260. # Test Get/Set (get old value, set new value, check that new value was set correctly)
  261. print(f"Attempting to set {ly_object.typename}.{attribute_name} = {value} (expected result is {expected_result})")
  262. if hasattr(ly_object, attribute_name):
  263. print(f"SUCCESS: Located attribute {attribute_name} for {ly_object.typename}")
  264. else:
  265. print(f"FAILURE: Failed to find attribute {attribute_name} in {ly_object.typename}")
  266. return False
  267. old_value = getattr(ly_object, attribute_name)
  268. if old_value is not None:
  269. print(f"SUCCESS: Retrieved existing value {old_value} for {attribute_name} in {ly_object.typename}")
  270. else:
  271. print(f"FAILURE: Failed to retrieve value for {attribute_name} in {ly_object.typename}")
  272. return False
  273. if old_value == expected_result:
  274. print((f"WARNING: get_set_test on {attribute_name} is setting the same value that already exists ({old_value})."
  275. "The 'set' result for the test will be inconclusive."))
  276. setattr(ly_object, attribute_name, expected_result)
  277. new_value = getattr(ly_object, attribute_name)
  278. if new_value is not None:
  279. print(f"SUCCESS: Retrieved new value {new_value} for {attribute_name} in {ly_object.typename}")
  280. else:
  281. print(f"FAILURE: Failed to retrieve value for {attribute_name} in {ly_object.typename}")
  282. return False
  283. return compare_values(new_value, expected_result, f"{ly_object.typename}.{attribute_name}")
  284. def has_components(entity_id: object, component_list: list) -> bool:
  285. """
  286. Used to verify if a given entity has all the components of components_list. Returns True if all the
  287. components are present, else False
  288. :param entity_id: entity id of the entity
  289. :param component_list: list of component names to be verified
  290. """
  291. typeIdsList = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', component_list,
  292. entity.EntityType().Game)
  293. for type_id in typeIdsList:
  294. if not editor.EditorComponentAPIBus(bus.Broadcast, 'HasComponentOfType', entity_id, type_id):
  295. return False
  296. return True
  297. class PathNotFoundError(Exception):
  298. def __init__(self, path):
  299. self.path = path
  300. def __str__(self):
  301. return f"Path \"{self.path}\" not found in Editor Settings"
  302. def get_editor_settings_path_list():
  303. """
  304. Get the list of Editor Settings paths
  305. """
  306. paths = editor.EditorSettingsAPIBus(bus.Broadcast, 'BuildSettingsList')
  307. return paths
  308. def get_editor_settings_by_path(path):
  309. """
  310. Get the value of Editor Settings based on the path.
  311. :param path: path to the Editor Settings to get the value
  312. """
  313. if path not in get_editor_settings_path_list():
  314. raise PathNotFoundError(path)
  315. outcome = editor.EditorSettingsAPIBus(bus.Broadcast, 'GetValue', path)
  316. if outcome.isSuccess():
  317. return outcome.GetValue()
  318. raise RuntimeError(f"GetValue for path '{path}' failed")
  319. def set_editor_settings_by_path(path, value, is_bool = False):
  320. """
  321. Set the value of Editor Settings based on the path.
  322. # NOTE: Some Editor Settings may need an Editor restart to apply.
  323. # Ex: Enabling or disabling New Viewport Interaction Model
  324. :param path: path to the Editor Settings to get the value
  325. :param value: value to be set
  326. :param is_bool: True for Boolean settings (enable/disable), False for other settings
  327. """
  328. if path not in get_editor_settings_path_list():
  329. raise PathNotFoundError(path)
  330. if is_bool and not isinstance(value, bool):
  331. def ParseBoolValue(value):
  332. if(value == "0"):
  333. return False
  334. return True
  335. value = ParseBoolValue(value)
  336. outcome = editor.EditorSettingsAPIBus(bus.Broadcast, 'SetValue', path, value)
  337. if not outcome.isSuccess():
  338. raise RuntimeError(f"SetValue for path '{path}' failed")
  339. print(f"Value for path '{path}' is set to {value}")
  340. def get_component_type_id_map(component_name_list):
  341. """
  342. Given a list of component names, returns a map of component name -> component type id
  343. :param component_name_list: The lumberyard object to test
  344. :return: Dictionary of component name -> component type id pairs
  345. """
  346. # Remove any duplicates so we don't have to query for the same TypeId
  347. component_names = list(set(component_name_list))
  348. type_ids_by_component = {}
  349. type_ids = editor.EditorComponentAPIBus(bus.Broadcast, 'FindComponentTypeIdsByEntityType', component_names,
  350. entity.EntityType().Game)
  351. for i, typeId in enumerate(type_ids):
  352. type_ids_by_component[component_names[i]] = typeId
  353. return type_ids_by_component