#!/usr/bin/env python3 import argparse import os import sys import re import xml.etree.ElementTree from logging import getLogger, StreamHandler, INFO, WARNING, Formatter, Filter 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 Actor: def __init__(self, mod_name, vfs_path): self.mod_name = mod_name self.vfs_path = vfs_path self.name = os.path.basename(vfs_path) self.textures = [] self.material = '' self.logger = getLogger(__name__) def read(self, physical_path): try: tree = xml.etree.ElementTree.parse(physical_path) except xml.etree.ElementTree.ParseError as err: self.logger.error('"%s": %s' % (physical_path, err.msg)) return False root = tree.getroot() # Special case: particles don't need a diffuse texture. if len(root.findall('.//particles')) > 0: self.textures.append("baseTex") for element in root.findall('.//material'): self.material = element.text for element in root.findall('.//texture'): self.textures.append(element.get('name')) for element in root.findall('.//variant'): file = element.get('file') if file: self.read_variant(physical_path, os.path.join('art', 'variants', file)) return True def read_variant(self, actor_physical_path, relative_path): physical_path = actor_physical_path.replace(self.vfs_path, relative_path) try: tree = xml.etree.ElementTree.parse(physical_path) except xml.etree.ElementTree.ParseError as err: self.logger.error('"%s": %s' % (physical_path, err.msg)) return False root = tree.getroot() file = root.get('file') if file: self.read_variant(actor_physical_path, os.path.join('art', 'variants', file)) for element in root.findall('.//texture'): self.textures.append(element.get('name')) class Material: def __init__(self, mod_name, vfs_path): self.mod_name = mod_name self.vfs_path = vfs_path self.name = os.path.basename(vfs_path) self.required_textures = [] def read(self, physical_path): try: root = xml.etree.ElementTree.parse(physical_path).getroot() except xml.etree.ElementTree.ParseError as err: self.logger.error('"%s": %s' % (physical_path, err.msg)) return False for element in root.findall('.//required_texture'): texture_name = element.get('name') self.required_textures.append(texture_name) return True class Validator: def __init__(self, vfs_root, mods=None): if mods is None: mods = ['mod', 'public'] self.vfs_root = vfs_root self.mods = mods self.materials = {} self.invalid_materials = {} self.actors = [] self.__init_logger @property def __init_logger(self): logger = getLogger(__name__) logger.setLevel(INFO) # create a console handler, seems nicer to Windows and for future uses ch = StreamHandler(sys.stdout) ch.setLevel(INFO) ch.setFormatter(Formatter('%(levelname)s - %(message)s')) f1 = SingleLevelFilter(INFO, False) ch.addFilter(f1) logger.addHandler(ch) errorch = StreamHandler(sys.stderr) errorch.setLevel(WARNING) errorch.setFormatter(Formatter('%(levelname)s - %(message)s')) logger.addHandler(errorch) self.logger = logger def get_mod_path(self, mod_name, vfs_path): return os.path.join(mod_name, vfs_path) def get_physical_path(self, mod_name, vfs_path): return os.path.realpath(os.path.join(self.vfs_root, mod_name, vfs_path)) def find_mod_files(self, mod_name, vfs_path, pattern): physical_path = self.get_physical_path(mod_name, vfs_path) result = [] if not os.path.isdir(physical_path): return result for file_name in os.listdir(physical_path): if file_name == '.git' or file_name == '.svn': continue vfs_file_path = os.path.join(vfs_path, file_name) physical_file_path = os.path.join(physical_path, file_name) if os.path.isdir(physical_file_path): result += self.find_mod_files(mod_name, vfs_file_path, pattern) elif os.path.isfile(physical_file_path) and pattern.match(file_name): result.append({ 'mod_name': mod_name, 'vfs_path': vfs_file_path }) return result def find_all_mods_files(self, vfs_path, pattern): result = [] for mod_name in reversed(self.mods): result += self.find_mod_files(mod_name, vfs_path, pattern) return result def find_materials(self, vfs_path): self.logger.info('Collecting materials...') material_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml')) for material_file in material_files: material_name = os.path.basename(material_file['vfs_path']) if material_name in self.materials: continue material = Material(material_file['mod_name'], material_file['vfs_path']) if material.read(self.get_physical_path(material_file['mod_name'], material_file['vfs_path'])): self.materials[material_name] = material else: self.invalid_materials[material_name] = material def find_actors(self, vfs_path): self.logger.info('Collecting actors...') actor_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml')) for actor_file in actor_files: actor = Actor(actor_file['mod_name'], actor_file['vfs_path']) if actor.read(self.get_physical_path(actor_file['mod_name'], actor_file['vfs_path'])): self.actors.append(actor) def run(self): self.find_materials(os.path.join('art', 'materials')) self.find_actors(os.path.join('art', 'actors')) self.logger.info('Validating textures...') for actor in self.actors: if not actor.material: continue if actor.material not in self.materials and actor.material not in self.invalid_materials: self.logger.error('"%s": unknown material "%s"' % ( self.get_mod_path(actor.mod_name, actor.vfs_path), actor.material )) if actor.material not in self.materials: continue material = self.materials[actor.material] missing_textures = ', '.join(set([required_texture for required_texture in material.required_textures if required_texture not in actor.textures])) if len(missing_textures) > 0: self.logger.error('"%s": actor does not contain required texture(s) "%s" from "%s"' % ( self.get_mod_path(actor.mod_name, actor.vfs_path), missing_textures, material.name )) extra_textures = ', '.join(set([extra_texture for extra_texture in actor.textures if extra_texture not in material.required_textures])) if len(extra_textures) > 0: self.logger.warning('"%s": actor contains unnecessary texture(s) "%s" from "%s"' % ( self.get_mod_path(actor.mod_name, actor.vfs_path), extra_textures, material.name )) if __name__ == '__main__': script_dir = os.path.dirname(os.path.realpath(__file__)) default_root = os.path.join(script_dir, '..', '..', '..', 'binaries', 'data', 'mods') parser = argparse.ArgumentParser(description='Actors/materials validator.') parser.add_argument('-r', '--root', action='store', dest='root', default=default_root) parser.add_argument('-m', '--mods', action='store', dest='mods', default='mod,public') args = parser.parse_args() validator = Validator(args.root, args.mods.split(',')) validator.run()