You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
196 lines
8.3 KiB
Python
196 lines
8.3 KiB
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 os
|
|
import azlmbr.legacy.general as general
|
|
|
|
from xml.etree import ElementTree
|
|
|
|
class Physmaterial_Editor:
|
|
"""
|
|
This class is used to adjust physmaterial files for use with Open 3D Engine.
|
|
|
|
NOTEWORTHY:
|
|
- Must use save_changes() for library modifications to take affect
|
|
- Once file is overwritten there is a small lag before the editor applies these changes. Tests
|
|
must be set up to allow time for this lag.
|
|
- You can use parse() to overwrite the Physmaterial_Editor object with a new file
|
|
|
|
Methods:
|
|
- __init__ (self, document_filename = None): Sets up Physmaterial Instance
|
|
- document_filename (type: string): the full path of your physmaterial file
|
|
- parse_file (self): Loads the material library into memory and creates and indexable root object.
|
|
- save_changes (self): Overwrites the contents of the input file with the modified library. Unless
|
|
this is called no changes will occur
|
|
- modify_material (self, material, attribute, value): Modifies a given material. Adjusts values
|
|
if possible, throws errors if not
|
|
- material (type: string): The name of the material, must be exact
|
|
- attribute (type: string): Name of the attribute, must be exact. Restrictions outlined below
|
|
- value (type: string, int, or float): New value for the given attribute. Restrictions
|
|
outlined below
|
|
- delete_material (self, material): Deletes given material from the library.
|
|
- material (type: string): The name of the material, must be exact
|
|
|
|
Properties:
|
|
- number_of_materials: Number of materials in the material library
|
|
|
|
Input Restrictions:
|
|
- Attribute: Can only be one of the five following values
|
|
- 'Dynamic Friction'
|
|
- 'Static Friction'
|
|
- 'Restitution'
|
|
- 'Friction Combine'
|
|
- 'Restitution Combine'
|
|
- Friction Values: Must be a number either int or float
|
|
- Restitution Values: Must be a number either int or float between 0 and 1
|
|
- Combine Values: Can only be one of the four following values
|
|
- 'Average'
|
|
- 'Minimum'
|
|
- 'Maximum'
|
|
- 'Multiply'
|
|
|
|
notes:
|
|
- Due to the setup of material libraries root has a lot of indices that must be used to get to the
|
|
actual library portion. There does not seem to be an easy way to remedy this issue as it makes
|
|
for a difficult rewrite process
|
|
- parse_file must only be called if the file path is not given during initialization.
|
|
"""
|
|
|
|
def __init__(self, document=None):
|
|
self.document_filename = document
|
|
self.project_folder = general.get_game_folder()
|
|
self._set_path()
|
|
self.parse_file()
|
|
|
|
def parse_file(self):
|
|
# type: (str) -> None
|
|
# See if a file exists at the given path
|
|
if not os.path.exists(self.document_filename):
|
|
raise ValueError("Given file, {} ,does not exist".format(self.document_filename))
|
|
# Brings Material Library contents into memory
|
|
try:
|
|
self.dom = ElementTree.parse(self.document_filename)
|
|
except Exception as e:
|
|
print(e)
|
|
raise ValueError('{} not valid'.format(self.document_filename))
|
|
# Turn parsed xml into usable form
|
|
self.root = self.dom.getroot()
|
|
# Check if file is a material library
|
|
asset_typename = self.root[0].get('name')
|
|
if not asset_typename == "MaterialLibraryAsset":
|
|
if asset_typename:
|
|
print("Given file is a {} file".format(self.root[0].get('name')))
|
|
raise ValueError('File not valid')
|
|
|
|
def save_changes(self):
|
|
# type: (None) -> None
|
|
# Over writes file with modified material library contents
|
|
content = ElementTree.tostring(self.root)
|
|
try:
|
|
with open(self.document_filename, "wb") as document:
|
|
document.write(content)
|
|
except Exception as e:
|
|
print(e)
|
|
print("Failed to save changes to script")
|
|
|
|
# Temporary fix, will need to use OnAssetReloaded callbacks
|
|
general.idle_wait(0.5)
|
|
|
|
def delete_material(self, material):
|
|
# type: (str) -> bool
|
|
# Deletes a material from the library
|
|
index = self._find_material_index(material)
|
|
if index != None:
|
|
self.root[0][1].remove(self.root[0][1][index])
|
|
return True
|
|
else:
|
|
print("{} not found in library. No deletion occurred.".format(material))
|
|
return False
|
|
|
|
def modify_material(self, material, attribute, value):
|
|
# type: (str, str, float) -> bool
|
|
# Modifies attributes of a given material in the library
|
|
index = self._find_material_index(material)
|
|
attribute_index = Physmaterial_Editor._get_attribute_index(attribute)
|
|
formated_value = Physmaterial_Editor._value_formater(value, 'Restitution' == attribute, 'Combine' in attribute)
|
|
if index != None:
|
|
self.root[0][1][index][0][attribute_index].set('value', formated_value)
|
|
return True
|
|
else:
|
|
print("{} not found in library. No modification of {} occurred.".format(material, attribute))
|
|
return False
|
|
|
|
@property
|
|
def number_of_materials(self):
|
|
# type: (str) -> int
|
|
materials = self.root[0][1].findall(".//Class[@name='MaterialFromAssetConfiguration']")
|
|
return len(materials)
|
|
|
|
def _set_path(self):
|
|
# type: (str) -> str
|
|
if self.document_filename == None:
|
|
self.document_filename = os.path.join(self.project_folder, "surfacetypemateriallibrary.physmaterial")
|
|
else:
|
|
for (root, directories, root_files) in os.walk(self.project_folder):
|
|
for root_file in root_files:
|
|
if root_file == self.document_filename:
|
|
self.document_filename = os.path.join(root, root_file)
|
|
break
|
|
|
|
def _find_material_index(self, material):
|
|
# type: (str) -> int
|
|
found = False
|
|
material_index = None
|
|
for index, child in enumerate(self.root[0][1]):
|
|
if child.findall(".//Class[@value='{}']".format(material)):
|
|
if not found:
|
|
found = True
|
|
material_index = index
|
|
return material_index
|
|
|
|
@staticmethod
|
|
def _value_formater(value, is_restitution, is_combine):
|
|
# type: (float/int/str, bool, bool) -> str
|
|
# Constants
|
|
MIN_RESTITUTION = 0.0000000
|
|
MAX_RESTITUTION = 1.0000000
|
|
|
|
if is_combine:
|
|
value = Physmaterial_Editor._get_combine_id(value)
|
|
else:
|
|
if isinstance(value, int) or isinstance(value, float):
|
|
if is_restitution:
|
|
value = max(min(value, MAX_RESTITUTION), MIN_RESTITUTION)
|
|
value = "{:.7f}".format(value)
|
|
else:
|
|
raise ValueError("Must enter int or float. Entered value was of type {}.".format(type(value)))
|
|
return value
|
|
|
|
@staticmethod
|
|
def _get_combine_id(combine_name):
|
|
# type: (str) -> int
|
|
# Maps the Combine mode to its enumerated value used by the Open 3D Engine Editor
|
|
combine_dictionary = {"Average": "0", "Minimum": "1", "Maximum": "2", "Multiply": "3"}
|
|
if combine_name not in combine_dictionary:
|
|
raise ValueError("Invalid Combine Value given. {} is not in combine map".format(combine_name))
|
|
return combine_dictionary[combine_name]
|
|
|
|
@staticmethod
|
|
def _get_attribute_index(attribute):
|
|
# type: (str) -> int
|
|
# Maps the attribute names to their corresponding index relative to the line defining the material name.
|
|
attribute_dictionary = {
|
|
"DynamicFriction": 1,
|
|
"StaticFriction": 2,
|
|
"Restitution": 3,
|
|
"FrictionCombine": 4,
|
|
"RestitutionCombine": 5,
|
|
}
|
|
if attribute not in attribute_dictionary:
|
|
raise ValueError("Invalid Material Attribute given. {} is not in attribute map".format(attribute))
|
|
return attribute_dictionary[attribute]
|