Physmaterial_Editor.py 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  1. """
  2. Copyright (c) Contributors to the Open 3D Engine Project.
  3. For complete copyright and license terms please see the LICENSE at the root of this distribution.
  4. SPDX-License-Identifier: Apache-2.0 OR MIT
  5. """
  6. import os
  7. import azlmbr.legacy.general as general
  8. from xml.etree import ElementTree
  9. class Physmaterial_Editor:
  10. """
  11. This class is used to adjust physmaterial files for use with Open 3D Engine.
  12. NOTEWORTHY:
  13. - Must use save_changes() for library modifications to take affect
  14. - Once file is overwritten there is a small lag before the editor applies these changes. Tests
  15. must be set up to allow time for this lag.
  16. - You can use parse() to overwrite the Physmaterial_Editor object with a new file
  17. Methods:
  18. - __init__ (self, document_filename = None): Sets up Physmaterial Instance
  19. - document_filename (type: string): the full path of your physmaterial file
  20. - parse_file (self): Loads the material library into memory and creates and indexable root object.
  21. - save_changes (self): Overwrites the contents of the input file with the modified library. Unless
  22. this is called no changes will occur
  23. - modify_material (self, material, attribute, value): Modifies a given material. Adjusts values
  24. if possible, throws errors if not
  25. - material (type: string): The name of the material, must be exact
  26. - attribute (type: string): Name of the attribute, must be exact. Restrictions outlined below
  27. - value (type: string, int, or float): New value for the given attribute. Restrictions
  28. outlined below
  29. - delete_material (self, material): Deletes given material from the library.
  30. - material (type: string): The name of the material, must be exact
  31. Properties:
  32. - number_of_materials: Number of materials in the material library
  33. Input Restrictions:
  34. - Attribute: Can only be one of the five following values
  35. - 'Dynamic Friction'
  36. - 'Static Friction'
  37. - 'Restitution'
  38. - 'Friction Combine'
  39. - 'Restitution Combine'
  40. - Friction Values: Must be a number either int or float
  41. - Restitution Values: Must be a number either int or float between 0 and 1
  42. - Combine Values: Can only be one of the four following values
  43. - 'Average'
  44. - 'Minimum'
  45. - 'Maximum'
  46. - 'Multiply'
  47. notes:
  48. - Due to the setup of material libraries root has a lot of indices that must be used to get to the
  49. actual library portion. There does not seem to be an easy way to remedy this issue as it makes
  50. for a difficult rewrite process
  51. - parse_file must only be called if the file path is not given during initialization.
  52. """
  53. def __init__(self, document=None):
  54. self.document_filename = document
  55. self.project_folder = general.get_game_folder()
  56. self._set_path()
  57. self.parse_file()
  58. def parse_file(self):
  59. # type: (str) -> None
  60. # See if a file exists at the given path
  61. if not os.path.exists(self.document_filename):
  62. raise ValueError("Given file, {} ,does not exist".format(self.document_filename))
  63. # Brings Material Library contents into memory
  64. try:
  65. self.dom = ElementTree.parse(self.document_filename)
  66. except Exception as e:
  67. print(e)
  68. raise ValueError('{} not valid'.format(self.document_filename))
  69. # Turn parsed xml into usable form
  70. self.root = self.dom.getroot()
  71. # Check if file is a material library
  72. asset_typename = self.root[0].get('name')
  73. if not asset_typename == "MaterialLibraryAsset":
  74. if asset_typename:
  75. print("Given file is a {} file".format(self.root[0].get('name')))
  76. raise ValueError('File not valid')
  77. def save_changes(self):
  78. # type: (None) -> None
  79. # Over writes file with modified material library contents
  80. content = ElementTree.tostring(self.root)
  81. try:
  82. with open(self.document_filename, "wb") as document:
  83. document.write(content)
  84. except Exception as e:
  85. print(e)
  86. print("Failed to save changes to script")
  87. # Temporary fix, will need to use OnAssetReloaded callbacks
  88. general.idle_wait(0.5)
  89. def delete_material(self, material):
  90. # type: (str) -> bool
  91. # Deletes a material from the library
  92. index = self._find_material_index(material)
  93. if index != None:
  94. self.root[0][1].remove(self.root[0][1][index])
  95. return True
  96. else:
  97. print("{} not found in library. No deletion occurred.".format(material))
  98. return False
  99. def modify_material(self, material, attribute, value):
  100. # type: (str, str, float) -> bool
  101. # Modifies attributes of a given material in the library
  102. index = self._find_material_index(material)
  103. attribute_index = Physmaterial_Editor._get_attribute_index(attribute)
  104. formated_value = Physmaterial_Editor._value_formater(value, 'Restitution' == attribute, 'Combine' in attribute)
  105. if index != None:
  106. self.root[0][1][index][0][attribute_index].set('value', formated_value)
  107. return True
  108. else:
  109. print("{} not found in library. No modification of {} occurred.".format(material, attribute))
  110. return False
  111. @property
  112. def number_of_materials(self):
  113. # type: (str) -> int
  114. materials = self.root[0][1].findall(".//Class[@name='MaterialFromAssetConfiguration']")
  115. return len(materials)
  116. def _set_path(self):
  117. # type: (str) -> str
  118. if self.document_filename == None:
  119. self.document_filename = os.path.join(self.project_folder, "assets", "physics", "surfacetypemateriallibrary.physmaterial")
  120. else:
  121. for (root, directories, root_files) in os.walk(self.project_folder):
  122. for root_file in root_files:
  123. if root_file == self.document_filename:
  124. self.document_filename = os.path.join(root, root_file)
  125. break
  126. def _find_material_index(self, material):
  127. # type: (str) -> int
  128. found = False
  129. material_index = None
  130. for index, child in enumerate(self.root[0][1]):
  131. if child.findall(".//Class[@value='{}']".format(material)):
  132. if not found:
  133. found = True
  134. material_index = index
  135. return material_index
  136. @staticmethod
  137. def _value_formater(value, is_restitution, is_combine):
  138. # type: (float/int/str, bool, bool) -> str
  139. # Constants
  140. MIN_RESTITUTION = 0.0000000
  141. MAX_RESTITUTION = 1.0000000
  142. if is_combine:
  143. value = Physmaterial_Editor._get_combine_id(value)
  144. else:
  145. if isinstance(value, int) or isinstance(value, float):
  146. if is_restitution:
  147. value = max(min(value, MAX_RESTITUTION), MIN_RESTITUTION)
  148. value = "{:.7f}".format(value)
  149. else:
  150. raise ValueError("Must enter int or float. Entered value was of type {}.".format(type(value)))
  151. return value
  152. @staticmethod
  153. def _get_combine_id(combine_name):
  154. # type: (str) -> int
  155. # Maps the Combine mode to its enumerated value used by the Open 3D Engine Editor
  156. combine_dictionary = {"Average": "0", "Minimum": "1", "Maximum": "2", "Multiply": "3"}
  157. if combine_name not in combine_dictionary:
  158. raise ValueError("Invalid Combine Value given. {} is not in combine map".format(combine_name))
  159. return combine_dictionary[combine_name]
  160. @staticmethod
  161. def _get_attribute_index(attribute):
  162. # type: (str) -> int
  163. # Maps the attribute names to their corresponding index relative to the line defining the material name.
  164. attribute_dictionary = {
  165. "DynamicFriction": 1,
  166. "StaticFriction": 2,
  167. "Restitution": 3,
  168. "FrictionCombine": 4,
  169. "RestitutionCombine": 5,
  170. }
  171. if attribute not in attribute_dictionary:
  172. raise ValueError("Invalid Material Attribute given. {} is not in attribute map".format(attribute))
  173. return attribute_dictionary[attribute]