#!/usr/bin/env python3 # -*- mode: python-mode; python-indent-offset: 4; -*- # SPDX-License-Identifier: MIT # SPDX-FileCopyrightText: © 2023 Stanislas Daniel Claude Dolcini from logging import getLogger, StreamHandler, INFO, WARNING, Formatter, Filter from pathlib import Path from sys import stdout, stderr import xml.etree.ElementTree as ET import os import fileinput class SingleLevelFilter(Filter): def __init__(self, passlevel, reject): self.passlevel = passlevel self.reject = reject def filter(self, record): if self.reject: return (record.levelno != self.passlevel) else: return (record.levelno == self.passlevel) class BaseFixer(): def __init__(self, vfs_root, verbose=False): self.logger = self.__init_logger() self.vfs_root = Path(vfs_root) self.verbose = verbose self.files = [] def __init_logger(self): logger = getLogger(__name__) logger.setLevel(INFO) # create a console handler, seems nicer to Windows and for future uses ch = StreamHandler(stdout) ch.setLevel(INFO) ch.setFormatter(Formatter('%(levelname)s - %(message)s')) f1 = SingleLevelFilter(INFO, False) ch.addFilter(f1) logger.addHandler(ch) errorch = StreamHandler(stderr) errorch.setLevel(WARNING) errorch.setFormatter(Formatter('%(levelname)s - %(message)s')) logger.addHandler(errorch) return logger def fix_style(self, xml_path): changes = [ [' />', '/>'], ["version='1.0'", 'version="1.0"'], ["'utf-8'", '"utf-8"'] ] for line in fileinput.input(xml_path, inplace=True): for change in changes: line = line.replace(change[0], change[1]) print(line, end="") with open(xml_path, 'a', encoding='utf-8') as file: file.write('\n') def indent(self, elem, level=0, more_sibs=False): i = "\n" if level: i += (level-1) * ' ' num_kids = len(elem) if num_kids: if not elem.text or not elem.text.strip(): elem.text = i + " " if level: elem.text += ' ' count = 0 for kid in elem: self.indent(kid, level+1, count < num_kids - 1) count += 1 if not elem.tail or not elem.tail.strip(): elem.tail = i if more_sibs: elem.tail += ' ' else: if level and (not elem.tail or not elem.tail.strip()): elem.tail = i if more_sibs: elem.tail += ' ' def sort(self, root): # sort the first layer root[:] = sorted(root, key=lambda child: (child.tag,child.get('name'))) # sort the second layer for c in root: c[:] = sorted(c, key=lambda child: (child.tag,child.get('name'))) for cp in c: cp[:] = sorted(cp, key=lambda child: (child.tag,child.get('name'))) for scp in cp: scp[:] = sorted(scp, key=lambda child: (child.tag,child.get('name'))) def save_xml_file(self, tree, root, xml_file, sort=True): if sort: self.sort(root) self.indent(root) tree.write(xml_file, xml_declaration=True, encoding='utf-8') self.fix_style(xml_file) def add_files(self, path, extensions : tuple[str]): self.files = [] if os.path.isfile(str(self.vfs_root)): self.files.append(self.vfs_root) elif os.path.isdir(str(self.vfs_root)): for root, _, files in os.walk(str(self.vfs_root)): for name in files: file_path = os.path.join(root, name) if os.path.isfile(file_path) and path in file_path and name.endswith(extensions): self.files.append(file_path) if self.verbose: if len(self.files) > 0: self.logger.info("Found" {len(self.files)} file(s).") else: self.logger.info(f"No files were found.") class ActorToQualityLevel(BaseFixer): def __init__(self, vfs_root, verbose=False): BaseFixer.__init__(self, vfs_root, verbose) self.add_files('', tuple(".xml")) def process_actor(self, actorNode): actorNode.tag = "qualitylevels" tag1 = ET.Element("actor") tag1.attrib['quality'] = 'low' tag1.attrib['inline'] = '' tag2 = ET.Element("actor") tag2.attrib['quality'] = 'medium' tag2.attrib['inline'] = '' tag3 = ET.Element("actor") tag3.attrib['inline'] = '' tag = ET.Element("inline") tag.attrib['version'] = '1' groups = list() for group in reversed(actorNode): if group.tag == 'castshadow': group.attrib['minquality'] = 'medium' groups.append(group) actorNode.remove(group) actorNode.append(tag1) actorNode.append(tag2) actorNode.append(tag3) for group in reversed(groups): tag.append(group) actorNode.append(tag) meshGroup = ET.Element("group") meshes = [] for group in tag: if group.tag == 'group': for variant in group: cmpMesh = variant.find("mesh") if cmpMesh == None: continue meshes.append(cmpMesh.text) variant.remove(cmpMesh) for mesh in meshes: file_name = os.path.basename(mesh) file_path = mesh.replace(os.path.basename(mesh), "") # DEFAULT meshVariant = ET.SubElement(meshGroup, "variant") meshVariant.attrib['minquality'] = 'high' meshVariant.attrib['frequency'] = '1' meshVariant.attrib['name'] = 'mesh-variant' cmpMesh = ET.SubElement(meshVariant, "mesh") cmpMesh.text = mesh # MEDIUM meshVariant = ET.SubElement(meshGroup, "variant") meshVariant.attrib['maxquality'] = 'high' meshVariant.attrib['minquality'] = 'medium' meshVariant.attrib['frequency'] = '1' meshVariant.attrib['name'] = 'mesh-variant' cmpMesh = ET.SubElement(meshVariant, "mesh") cmpMesh.text = f"{file_path}medium/{file_name}" # LOW meshVariant = ET.SubElement(meshGroup, "variant") meshVariant.attrib['maxquality'] = 'medium' meshVariant.attrib['frequency'] = '1' meshVariant.attrib['name'] = 'mesh-variant' cmpMesh = ET.SubElement(meshVariant, "mesh") cmpMesh.text = f"{file_path}low/{file_name}" tag.insert(1 if tag.find('castshadow') is None else 0, meshGroup) return True def run(self): count = 0 for file in self.files: tree = ET.parse(file) root = tree.getroot() changed = False if root.tag == 'actor': changed = self.process_actor(root) elif root.tag == 'qualitylevels': continue if changed: self.save_xml_file(tree, root, file, False) count = count + 1 self.logger.info(f"Fixed {count} file(s).") if __name__ == '__main__': script_dir = "." mod_name = "public" print(f"Running in {Path(script_dir) / mod_name}") print("Converting to quality levels...") template_fixer = ActorToQualityLevel(Path(script_dir) / mod_name, True) template_fixer.run()