#!/usr/bin/env python3 import argparse import os import re import sys from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger from xml.etree import ElementTree as ET 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 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 = ET.parse(physical_path) except ET.ParseError: self.logger.exception(physical_path) 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 = ET.parse(physical_path) except ET.ParseError: self.logger.exception(physical_path) return 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 = ET.parse(physical_path).getroot() except ET.ParseError: self.logger.exception(physical_path) 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() 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 self.inError = False 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 in (".git", ".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, ) self.inError = True if actor.material not in self.materials: continue material = self.materials[actor.material] missing_textures = ", ".join( { 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, ) self.inError = True extra_textures = ", ".join( { 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, ) self.inError = True return not self.inError 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(",")) if not validator.run(): sys.exit(1)