284 lines
16 KiB
Python
284 lines
16 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# -*- mode: python-mode; python-indent-offset: 4; -*-
|
|
# SPDX-License-Identifier: MIT
|
|
# SPDX-FileCopyrightText: © 2023 Wildfire Games
|
|
# SPDX-FileCopyrightText: © 2023 Vladislav Belov
|
|
|
|
import os
|
|
import sys
|
|
|
|
import xml.etree.ElementTree as ET
|
|
|
|
|
|
def output_xml_tree(tree, path):
|
|
with open(path, 'wt') as handle:
|
|
handle.write('<?xml version="1.0" encoding="utf-8"?>\n')
|
|
def output_xml_node(node, handle, depth):
|
|
indent = ' ' * depth
|
|
attributes = ''
|
|
for attribute_name in node.attrib.keys():
|
|
attributes += ' {}="{}"'.format(attribute_name, node.attrib[attribute_name])
|
|
if len(node) != 0:
|
|
assert (node.text is None) or (not node.text.strip())
|
|
handle.write('{}<{}{}>\n'.format(indent, node.tag, attributes))
|
|
for child in node:
|
|
output_xml_node(child, handle, depth + 1)
|
|
handle.write('{}</{}>\n'.format(indent, node.tag))
|
|
else:
|
|
if node.text is None:
|
|
handle.write('{}<{}{}/>\n'.format(indent, node.tag, attributes))
|
|
else:
|
|
handle.write('{}<{}{}>{}</{}>\n'.format(indent, node.tag, attributes, node.text, node.tag))
|
|
output_xml_node(tree.getroot(), handle, 0)
|
|
|
|
def remove_terrain_materials(path):
|
|
with open(path, 'rt') as handle:
|
|
xml = handle.read()
|
|
|
|
terrain_base = 'terrain_base.xml' in xml
|
|
terrain_norm = 'terrain_norm.xml' in xml
|
|
terrain_spec = 'terrain_spec.xml' in xml
|
|
terrain_triplanar = 'terrain_triplanar.xml' in xml
|
|
|
|
should_save = terrain_base or terrain_norm or terrain_spec or terrain_triplanar
|
|
|
|
assert xml.count('<material>') == 1
|
|
|
|
tree = ET.parse(path)
|
|
root = tree.getroot()
|
|
for element in root:
|
|
if len(element) != 0:
|
|
assert element.tag == 'textures'
|
|
for child in element:
|
|
assert child.tag == 'texture'
|
|
if terrain_base or terrain_spec or terrain_triplanar:
|
|
texture_element = ET.SubElement(element, 'texture')
|
|
texture_element.set('name', 'normTex')
|
|
texture_element.set('file', 'types/default_norm.png')
|
|
if terrain_base or terrain_norm or terrain_triplanar:
|
|
texture_element = ET.SubElement(element, 'texture')
|
|
texture_element.set('name', 'specTex')
|
|
texture_element.set('file', 'types/blackness.dds')
|
|
elif element.tag == 'material':
|
|
if terrain_base or terrain_norm or terrain_spec:
|
|
element.text = 'terrain_norm_spec.xml'
|
|
elif terrain_triplanar:
|
|
element.text = 'terrain_triplanar_norm_spec.xml'
|
|
|
|
if should_save:
|
|
output_xml_tree(tree, path)
|
|
|
|
def get_materials():
|
|
return {
|
|
'aura.xml': {'norm': False, 'spec': True, 'replacement': 'aura_norm_spec.xml'},
|
|
'aura_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'aura_norm_spec.xml'},
|
|
'basic_glow.xml': {'norm': False, 'spec': True, 'replacement': 'basic_glow_norm_spec.xml'},
|
|
'basic_glow_norm.xml': {'norm': True, 'spec': True, 'replacement': 'basic_glow_norm_spec.xml'},
|
|
'basic_glow_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_glow_norm_spec.xml'},
|
|
'basic_glow_wind.xml': {'norm': False, 'spec': True, 'replacement': 'basic_glow_wind_norm_spec.xml'},
|
|
'basic_glow_wind_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_glow_wind_norm_spec.xml'},
|
|
'basic_specmap.xml': {'norm': False, 'spec': True, 'replacement': 'no_trans_norm_spec.xml'},
|
|
'basic_trans.xml': {'norm': False, 'spec': False, 'replacement': 'basic_trans_norm_spec.xml'},
|
|
'basic_trans_ao.xml': {'norm': False, 'spec': False, 'replacement': 'basic_trans_ao_norm_spec.xml'},
|
|
'basic_trans_ao_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_trans_ao_norm_spec.xml'},
|
|
'basic_trans_ao_parallax_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_trans_ao_parallax_spec.xml'},
|
|
'basic_trans_ao_spec.xml': {'norm': False, 'spec': True, 'replacement': 'basic_trans_ao_norm_spec.xml'},
|
|
'basic_trans_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_trans_norm_spec.xml'},
|
|
'basic_trans_parallax_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_trans_parallax_spec.xml'},
|
|
'basic_trans_spec.xml': {'norm': False, 'spec': True, 'replacement': 'basic_trans_norm_spec.xml'},
|
|
'basic_trans_wind.xml': {'norm': False, 'spec': False, 'replacement': 'basic_trans_wind_norm_spec.xml'},
|
|
'basic_trans_wind_grain.xml': {'norm': False, 'spec': False, 'replacement': 'basic_trans_wind_grain_norm_spec.xml'},
|
|
'basic_trans_wind_grain_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_trans_wind_grain_norm_spec.xml'},
|
|
'basic_trans_wind_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'basic_trans_wind_norm_spec.xml'},
|
|
'default.xml': {'norm': False, 'spec': False, 'replacement': 'no_trans_norm_spec.xml'},
|
|
'no_trans_ao.xml': {'norm': False, 'spec': False, 'replacement': 'no_trans_ao_norm_spec.xml'},
|
|
'no_trans_ao_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'no_trans_ao_norm_spec.xml'},
|
|
'no_trans_ao_parallax_spec.xml': {'norm': True, 'spec': True, 'replacement': 'no_trans_ao_parallax_spec.xml'},
|
|
'no_trans_ao_spec.xml': {'norm': False, 'spec': True, 'replacement': 'no_trans_ao_norm_spec.xml'},
|
|
'no_trans_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'no_trans_norm_spec.xml'},
|
|
'no_trans_parallax_ao.xml': {'norm': True, 'spec': False, 'replacement': 'no_trans_ao_parallax_spec.xml'},
|
|
'no_trans_parallax_spec.xml': {'norm': True, 'spec': True, 'replacement': 'no_trans_parallax_spec.xml'},
|
|
'no_trans_spec.xml': {'norm': False, 'spec': True, 'replacement': 'no_trans_norm_spec.xml'},
|
|
'objectcolor.xml': {'norm': False, 'spec': False, 'replacement': 'objectcolor_norm_spec.xml'},
|
|
'objectcolor_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'objectcolor_norm_spec.xml'},
|
|
'objectcolor_specmap.xml': {'norm': False, 'spec': True, 'replacement': 'objectcolor_norm_spec.xml'},
|
|
'player_trans.xml': {'norm': False, 'spec': False, 'replacement': 'player_trans_norm_spec.xml'},
|
|
'player_trans_ao.xml': {'norm': False, 'spec': False, 'replacement': 'player_trans_ao_norm_spec.xml'},
|
|
'player_trans_ao_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_ao_norm_spec.xml'},
|
|
'player_trans_ao_parallax.xml': {'norm': True, 'spec': False, 'replacement': 'player_trans_ao_parallax_spec.xml'},
|
|
'player_trans_ao_parallax_spec.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_ao_parallax_spec.xml'},
|
|
'player_trans_ao_spec.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_ao_spec.xml'},
|
|
'player_trans_norm.xml': {'norm': True, 'spec': False, 'replacement': 'player_trans_norm_spec.xml'},
|
|
'player_trans_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_norm_spec.xml'},
|
|
'player_trans_norm_spec_helmet.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_norm_spec_helmet.xml'},
|
|
'player_trans_parallax.xml': {'norm': True, 'spec': False, 'replacement': 'player_trans_parallax_spec.xml'},
|
|
'player_trans_parallax_spec.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_parallax_spec.xml'},
|
|
'player_trans_parallax_spec_helmet.xml': {'norm': True, 'spec': True, 'replacement': 'player_trans_parallax_spec_helmet.xml'},
|
|
'player_trans_spec.xml': {'norm': False, 'spec': True, 'replacement': 'player_trans_norm_spec.xml'},
|
|
'player_trans_spec_helmet.xml': {'norm': False, 'spec': True, 'replacement': 'player_trans_norm_spec_helmet.xml'},
|
|
'player_water.xml': {'norm': False, 'spec': False, 'replacement': 'player_water.xml'},
|
|
'rock_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'rock_norm_spec.xml'},
|
|
'rock_norm_spec_ao.xml': {'norm': True, 'spec': True, 'replacement': 'rock_norm_spec_ao.xml'},
|
|
'rock_normstrong_spec.xml': {'norm': True, 'spec': True, 'replacement': 'rock_normstrong_spec.xml'},
|
|
'rock_normstrong_spec_ao.xml': {'norm': True, 'spec': True, 'replacement': 'rock_normstrong_spec_ao.xml'},
|
|
'terrain_base.xml': {'norm': False, 'spec': False, 'replacement': 'terrain_norm_spec.xml'},
|
|
'terrain_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'terrain_norm_spec.xml'},
|
|
'terrain_normstrong_spec.xml': {'norm': True, 'spec': True, 'replacement': 'terrain_normstrong_spec.xml'},
|
|
'terrain_normweak_spec.xml': {'norm': True, 'spec': True, 'replacement': 'terrain_normweak_spec.xml'},
|
|
'terrain_triplanar.xml': {'norm': False, 'spec': False, 'replacement': 'terrain_triplanar_norm_spec.xml'},
|
|
'terrain_triplanar_norm_spec.xml': {'norm': True, 'spec': True, 'replacement': 'terrain_triplanar_norm_spec.xml'},
|
|
'trans_wind.xml': {'norm': False, 'spec': False, 'replacement': 'basic_trans_wind_norm_spec.xml'},
|
|
'waterfall.xml': {'norm': False, 'spec': False, 'replacement': 'waterfall.xml'},
|
|
}
|
|
|
|
def get_material(material):
|
|
return get_materials()[material]
|
|
|
|
def remove_actor_materials(path):
|
|
with open(path, 'rt') as handle:
|
|
xml = handle.read()
|
|
|
|
should_save = False
|
|
|
|
tree = ET.parse(path)
|
|
root = tree.getroot()
|
|
|
|
material_name = None
|
|
|
|
if '<material>' in xml:
|
|
assert xml.count('<material>') == 1
|
|
|
|
if root.tag == 'actor':
|
|
for child in root:
|
|
if child.tag == 'material':
|
|
material_name = child.text.strip()
|
|
child.text = get_material(material_name)['replacement']
|
|
else:
|
|
assert root.tag == 'qualitylevels'
|
|
for root_child in root:
|
|
assert root_child.tag == 'inline' or root_child.tag == 'actor'
|
|
for child in root_child:
|
|
if child.tag == 'material':
|
|
material_name = child.text.strip()
|
|
child.text = get_material(material_name)['replacement']
|
|
else:
|
|
assert root.tag == 'actor'
|
|
material_name = 'default.xml'
|
|
material_element = ET.SubElement(root, 'material')
|
|
material_element.text = get_material(material_name)['replacement']
|
|
|
|
assert material_name is not None
|
|
|
|
material = get_material(material_name)
|
|
if material_name == material['replacement']:
|
|
return
|
|
replacement_material = get_material(material['replacement'])
|
|
assert (not material['norm']) or (not material['spec'])
|
|
assert replacement_material['norm'] and replacement_material['spec']
|
|
|
|
if material['norm'] != ('normTex' in xml) or material['spec'] != ('specTex' in xml) or '<!--' in xml:
|
|
sys.stdout.write(
|
|
'Actor "{}" should be handled manually. It should use "{}" instead of "{}".\n'.format(
|
|
path, material['replacement'], material_name))
|
|
return
|
|
should_save = True
|
|
|
|
# If there are no textures it means the material isn't really useful. We'll with them later.
|
|
if xml.count('<textures>') > 0:
|
|
def add_default_norm_element(textures_element, index=None):
|
|
if not index:
|
|
norm_element = ET.SubElement(textures_element, 'texture')
|
|
else:
|
|
norm_element = ET.Element('texture')
|
|
norm_element.set('file', 'default_norm.png')
|
|
norm_element.set('name', 'normTex')
|
|
if index:
|
|
textures_element.insert(index, norm_element)
|
|
def add_default_spec_element(textures_element, index=None):
|
|
if not index:
|
|
spec_element = ET.SubElement(textures_element, 'texture')
|
|
else:
|
|
spec_element = ET.Element('texture')
|
|
spec_element.set('file', 'null_black.dds')
|
|
spec_element.set('name', 'specTex')
|
|
if index:
|
|
textures_element.insert(index, spec_element)
|
|
def add_missing_textures(element, needs_norm, needs_spec):
|
|
for child in element:
|
|
if child.tag == 'textures':
|
|
expected_len = 1
|
|
if not needs_norm:
|
|
expected_len += 1
|
|
if not needs_spec:
|
|
expected_len += 1
|
|
if expected_len > 1:
|
|
only_base = True
|
|
for texture_child in child:
|
|
if texture_child.get('name') == 'normTex' or texture_child.get('name') == 'specTex':
|
|
only_base = False
|
|
if only_base:
|
|
continue
|
|
if needs_norm and needs_spec:
|
|
add_default_norm_element(child)
|
|
add_default_spec_element(child)
|
|
elif needs_norm:
|
|
add_default_norm_element(child)
|
|
elif needs_spec:
|
|
add_default_spec_element(child)
|
|
else:
|
|
add_missing_textures(child, needs_norm, needs_spec)
|
|
if not material['norm'] and not material['spec']:
|
|
if xml.count('<textures>') > 1:
|
|
group_element = ET.Element('group')
|
|
variant_element = ET.SubElement(group_element, 'variant')
|
|
textures_element = ET.SubElement(variant_element, 'textures')
|
|
add_default_norm_element(textures_element)
|
|
add_default_spec_element(textures_element)
|
|
if root.tag == 'actor':
|
|
last_index = len(root) - 1
|
|
assert root[last_index].tag == 'material'
|
|
root.insert(last_index, group_element)
|
|
else:
|
|
assert root.tag == 'qualitylevels'
|
|
for root_child in root:
|
|
assert root_child.tag == 'inline' or root_child.tag == 'actor'
|
|
last_index = len(root_child) - 1
|
|
assert root_child[last_index].tag == 'material'
|
|
root_child.insert(last_index, group_element)
|
|
else:
|
|
add_missing_textures(root, True, True)
|
|
else:
|
|
add_missing_textures(root, not material['norm'], not material['spec'])
|
|
|
|
if should_save:
|
|
sys.stdout.write('Actor "{}" is fixed.\n'.format(path))
|
|
output_xml_tree(tree, path)
|
|
|
|
def remove_terrain_materials_recursively(path):
|
|
for file_name in os.listdir(path):
|
|
file_path = os.path.join(path, file_name)
|
|
if os.path.isfile(file_path):
|
|
name, ext = os.path.splitext(file_name)
|
|
if ext.lower() == '.xml' and name.lower() != 'terrains':
|
|
remove_terrain_materials(file_path)
|
|
elif os.path.isdir(file_path):
|
|
remove_terrain_materials_recursively(file_path)
|
|
|
|
def remove_actor_materials_recursively(path):
|
|
for file_name in os.listdir(path):
|
|
file_path = os.path.join(path, file_name)
|
|
if os.path.isfile(file_path):
|
|
name, ext = os.path.splitext(file_name)
|
|
if ext.lower() == '.xml':
|
|
remove_actor_materials(file_path)
|
|
elif os.path.isdir(file_path):
|
|
remove_actor_materials_recursively(file_path)
|
|
|
|
if __name__ == '__main__':
|
|
# paths should be a list of paths to mods folders
|
|
paths = sys.argv[1:]
|
|
for path in paths:
|
|
remove_terrain_materials_recursively(os.path.join(path, 'art', 'terrains'))
|
|
remove_actor_materials_recursively(os.path.join(path, 'art', 'actors'))
|