| [24892] | 1 | #!/usr/bin/env python3
|
|---|
| [20615] | 2 | import argparse
|
|---|
| 3 | import os
|
|---|
| [26350] | 4 | import sys
|
|---|
| [20615] | 5 | import re
|
|---|
| 6 | import xml.etree.ElementTree
|
|---|
| [26350] | 7 | from logging import getLogger, StreamHandler, INFO, WARNING, Formatter, Filter
|
|---|
| [20615] | 8 |
|
|---|
| [26350] | 9 | class SingleLevelFilter(Filter):
|
|---|
| 10 | def __init__(self, passlevel, reject):
|
|---|
| 11 | self.passlevel = passlevel
|
|---|
| 12 | self.reject = reject
|
|---|
| 13 |
|
|---|
| 14 | def filter(self, record):
|
|---|
| 15 | if self.reject:
|
|---|
| 16 | return (record.levelno != self.passlevel)
|
|---|
| 17 | else:
|
|---|
| 18 | return (record.levelno == self.passlevel)
|
|---|
| 19 |
|
|---|
| [20615] | 20 | class Actor:
|
|---|
| 21 | def __init__(self, mod_name, vfs_path):
|
|---|
| 22 | self.mod_name = mod_name
|
|---|
| 23 | self.vfs_path = vfs_path
|
|---|
| 24 | self.name = os.path.basename(vfs_path)
|
|---|
| 25 | self.textures = []
|
|---|
| 26 | self.material = ''
|
|---|
| [26333] | 27 | self.logger = getLogger(__name__)
|
|---|
| [20615] | 28 |
|
|---|
| 29 | def read(self, physical_path):
|
|---|
| [20648] | 30 | try:
|
|---|
| 31 | tree = xml.etree.ElementTree.parse(physical_path)
|
|---|
| 32 | except xml.etree.ElementTree.ParseError as err:
|
|---|
| [26350] | 33 | self.logger.error('"%s": %s' % (physical_path, err.msg))
|
|---|
| [20648] | 34 | return False
|
|---|
| [20615] | 35 | root = tree.getroot()
|
|---|
| [26333] | 36 | # Special case: particles don't need a diffuse texture.
|
|---|
| 37 | if len(root.findall('.//particles')) > 0:
|
|---|
| 38 | self.textures.append("baseTex")
|
|---|
| 39 |
|
|---|
| [20615] | 40 | for element in root.findall('.//material'):
|
|---|
| 41 | self.material = element.text
|
|---|
| 42 | for element in root.findall('.//texture'):
|
|---|
| 43 | self.textures.append(element.get('name'))
|
|---|
| [26333] | 44 | for element in root.findall('.//variant'):
|
|---|
| 45 | file = element.get('file')
|
|---|
| 46 | if file:
|
|---|
| 47 | self.read_variant(physical_path, os.path.join('art', 'variants', file))
|
|---|
| [20648] | 48 | return True
|
|---|
| [20615] | 49 |
|
|---|
| [26333] | 50 | def read_variant(self, actor_physical_path, relative_path):
|
|---|
| 51 | physical_path = actor_physical_path.replace(self.vfs_path, relative_path)
|
|---|
| 52 | try:
|
|---|
| 53 | tree = xml.etree.ElementTree.parse(physical_path)
|
|---|
| 54 | except xml.etree.ElementTree.ParseError as err:
|
|---|
| [26350] | 55 | self.logger.error('"%s": %s' % (physical_path, err.msg))
|
|---|
| [26333] | 56 | return False
|
|---|
| [20615] | 57 |
|
|---|
| [26333] | 58 | root = tree.getroot()
|
|---|
| 59 | file = root.get('file')
|
|---|
| 60 | if file:
|
|---|
| 61 | self.read_variant(actor_physical_path, os.path.join('art', 'variants', file))
|
|---|
| 62 |
|
|---|
| 63 | for element in root.findall('.//texture'):
|
|---|
| 64 | self.textures.append(element.get('name'))
|
|---|
| 65 |
|
|---|
| 66 |
|
|---|
| [20615] | 67 | class Material:
|
|---|
| 68 | def __init__(self, mod_name, vfs_path):
|
|---|
| 69 | self.mod_name = mod_name
|
|---|
| 70 | self.vfs_path = vfs_path
|
|---|
| 71 | self.name = os.path.basename(vfs_path)
|
|---|
| 72 | self.required_textures = []
|
|---|
| 73 |
|
|---|
| 74 | def read(self, physical_path):
|
|---|
| [20648] | 75 | try:
|
|---|
| 76 | root = xml.etree.ElementTree.parse(physical_path).getroot()
|
|---|
| 77 | except xml.etree.ElementTree.ParseError as err:
|
|---|
| [26350] | 78 | self.logger.error('"%s": %s' % (physical_path, err.msg))
|
|---|
| [20648] | 79 | return False
|
|---|
| [20615] | 80 | for element in root.findall('.//required_texture'):
|
|---|
| 81 | texture_name = element.get('name')
|
|---|
| 82 | self.required_textures.append(texture_name)
|
|---|
| [20648] | 83 | return True
|
|---|
| [20615] | 84 |
|
|---|
| 85 |
|
|---|
| 86 | class Validator:
|
|---|
| 87 | def __init__(self, vfs_root, mods=None):
|
|---|
| 88 | if mods is None:
|
|---|
| 89 | mods = ['mod', 'public']
|
|---|
| 90 |
|
|---|
| 91 | self.vfs_root = vfs_root
|
|---|
| 92 | self.mods = mods
|
|---|
| 93 | self.materials = {}
|
|---|
| [20648] | 94 | self.invalid_materials = {}
|
|---|
| [20615] | 95 | self.actors = []
|
|---|
| [26333] | 96 | self.__init_logger
|
|---|
| [20615] | 97 |
|
|---|
| [26333] | 98 | @property
|
|---|
| 99 | def __init_logger(self):
|
|---|
| 100 | logger = getLogger(__name__)
|
|---|
| 101 | logger.setLevel(INFO)
|
|---|
| [26350] | 102 | # create a console handler, seems nicer to Windows and for future uses
|
|---|
| 103 | ch = StreamHandler(sys.stdout)
|
|---|
| [26333] | 104 | ch.setLevel(INFO)
|
|---|
| 105 | ch.setFormatter(Formatter('%(levelname)s - %(message)s'))
|
|---|
| [26350] | 106 | f1 = SingleLevelFilter(INFO, False)
|
|---|
| 107 | ch.addFilter(f1)
|
|---|
| [26333] | 108 | logger.addHandler(ch)
|
|---|
| [26350] | 109 | errorch = StreamHandler(sys.stderr)
|
|---|
| 110 | errorch.setLevel(WARNING)
|
|---|
| 111 | errorch.setFormatter(Formatter('%(levelname)s - %(message)s'))
|
|---|
| 112 | logger.addHandler(errorch)
|
|---|
| [26333] | 113 | self.logger = logger
|
|---|
| 114 |
|
|---|
| [26334] | 115 | def get_mod_path(self, mod_name, vfs_path):
|
|---|
| 116 | return os.path.join(mod_name, vfs_path)
|
|---|
| 117 |
|
|---|
| [20615] | 118 | def get_physical_path(self, mod_name, vfs_path):
|
|---|
| [20648] | 119 | return os.path.realpath(os.path.join(self.vfs_root, mod_name, vfs_path))
|
|---|
| [20615] | 120 |
|
|---|
| 121 | def find_mod_files(self, mod_name, vfs_path, pattern):
|
|---|
| 122 | physical_path = self.get_physical_path(mod_name, vfs_path)
|
|---|
| 123 | result = []
|
|---|
| 124 | if not os.path.isdir(physical_path):
|
|---|
| 125 | return result
|
|---|
| 126 | for file_name in os.listdir(physical_path):
|
|---|
| 127 | if file_name == '.git' or file_name == '.svn':
|
|---|
| 128 | continue
|
|---|
| 129 | vfs_file_path = os.path.join(vfs_path, file_name)
|
|---|
| 130 | physical_file_path = os.path.join(physical_path, file_name)
|
|---|
| 131 | if os.path.isdir(physical_file_path):
|
|---|
| 132 | result += self.find_mod_files(mod_name, vfs_file_path, pattern)
|
|---|
| 133 | elif os.path.isfile(physical_file_path) and pattern.match(file_name):
|
|---|
| 134 | result.append({
|
|---|
| 135 | 'mod_name': mod_name,
|
|---|
| 136 | 'vfs_path': vfs_file_path
|
|---|
| 137 | })
|
|---|
| 138 | return result
|
|---|
| 139 |
|
|---|
| 140 | def find_all_mods_files(self, vfs_path, pattern):
|
|---|
| 141 | result = []
|
|---|
| 142 | for mod_name in reversed(self.mods):
|
|---|
| 143 | result += self.find_mod_files(mod_name, vfs_path, pattern)
|
|---|
| 144 | return result
|
|---|
| 145 |
|
|---|
| 146 | def find_materials(self, vfs_path):
|
|---|
| [26350] | 147 | self.logger.info('Collecting materials...')
|
|---|
| [20615] | 148 | material_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml'))
|
|---|
| 149 | for material_file in material_files:
|
|---|
| 150 | material_name = os.path.basename(material_file['vfs_path'])
|
|---|
| 151 | if material_name in self.materials:
|
|---|
| 152 | continue
|
|---|
| 153 | material = Material(material_file['mod_name'], material_file['vfs_path'])
|
|---|
| [20648] | 154 | if material.read(self.get_physical_path(material_file['mod_name'], material_file['vfs_path'])):
|
|---|
| 155 | self.materials[material_name] = material
|
|---|
| 156 | else:
|
|---|
| 157 | self.invalid_materials[material_name] = material
|
|---|
| [20615] | 158 |
|
|---|
| 159 | def find_actors(self, vfs_path):
|
|---|
| [26350] | 160 | self.logger.info('Collecting actors...')
|
|---|
| 161 |
|
|---|
| [20615] | 162 | actor_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml'))
|
|---|
| 163 | for actor_file in actor_files:
|
|---|
| 164 | actor = Actor(actor_file['mod_name'], actor_file['vfs_path'])
|
|---|
| [20648] | 165 | if actor.read(self.get_physical_path(actor_file['mod_name'], actor_file['vfs_path'])):
|
|---|
| 166 | self.actors.append(actor)
|
|---|
| [20615] | 167 |
|
|---|
| 168 | def run(self):
|
|---|
| 169 | self.find_materials(os.path.join('art', 'materials'))
|
|---|
| 170 | self.find_actors(os.path.join('art', 'actors'))
|
|---|
| [26350] | 171 | self.logger.info('Validating textures...')
|
|---|
| [20615] | 172 |
|
|---|
| 173 | for actor in self.actors:
|
|---|
| 174 | if not actor.material:
|
|---|
| 175 | continue
|
|---|
| [20648] | 176 | if actor.material not in self.materials and actor.material not in self.invalid_materials:
|
|---|
| [26333] | 177 | self.logger.error('"%s": unknown material "%s"' % (
|
|---|
| 178 | self.get_mod_path(actor.mod_name, actor.vfs_path),
|
|---|
| [20648] | 179 | actor.material
|
|---|
| 180 | ))
|
|---|
| 181 | if actor.material not in self.materials:
|
|---|
| 182 | continue
|
|---|
| [20615] | 183 | material = self.materials[actor.material]
|
|---|
| [26333] | 184 |
|
|---|
| 185 | missing_textures = ', '.join(set([required_texture for required_texture in material.required_textures if required_texture not in actor.textures]))
|
|---|
| 186 | if len(missing_textures) > 0:
|
|---|
| 187 | self.logger.error('"%s": actor does not contain required texture(s) "%s" from "%s"' % (
|
|---|
| 188 | self.get_mod_path(actor.mod_name, actor.vfs_path),
|
|---|
| 189 | missing_textures,
|
|---|
| [20615] | 190 | material.name
|
|---|
| 191 | ))
|
|---|
| [26333] | 192 |
|
|---|
| 193 | extra_textures = ', '.join(set([extra_texture for extra_texture in actor.textures if extra_texture not in material.required_textures]))
|
|---|
| 194 | if len(extra_textures) > 0:
|
|---|
| 195 | self.logger.warning('"%s": actor contains unnecessary texture(s) "%s" from "%s"' % (
|
|---|
| 196 | self.get_mod_path(actor.mod_name, actor.vfs_path),
|
|---|
| 197 | extra_textures,
|
|---|
| [20615] | 198 | material.name
|
|---|
| 199 | ))
|
|---|
| 200 |
|
|---|
| 201 | if __name__ == '__main__':
|
|---|
| 202 | script_dir = os.path.dirname(os.path.realpath(__file__))
|
|---|
| 203 | default_root = os.path.join(script_dir, '..', '..', '..', 'binaries', 'data', 'mods')
|
|---|
| 204 | parser = argparse.ArgumentParser(description='Actors/materials validator.')
|
|---|
| 205 | parser.add_argument('-r', '--root', action='store', dest='root', default=default_root)
|
|---|
| 206 | parser.add_argument('-m', '--mods', action='store', dest='mods', default='mod,public')
|
|---|
| 207 | args = parser.parse_args()
|
|---|
| 208 | validator = Validator(args.root, args.mods.split(','))
|
|---|
| 209 | validator.run()
|
|---|