Merge 'remotes/trunk'
[0ad.git] / source / tools / xmlvalidator / validator.py
blobef004445551342a2415496c2bcf6a61dce52340d
1 #!/usr/bin/env python3
2 import argparse
3 import os
4 import sys
5 import re
6 import xml.etree.ElementTree
7 from logging import getLogger, StreamHandler, INFO, WARNING, Formatter, Filter
9 class SingleLevelFilter(Filter):
10 def __init__(self, passlevel, reject):
11 self.passlevel = passlevel
12 self.reject = reject
14 def filter(self, record):
15 if self.reject:
16 return (record.levelno != self.passlevel)
17 else:
18 return (record.levelno == self.passlevel)
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 = ''
27 self.logger = getLogger(__name__)
29 def read(self, physical_path):
30 try:
31 tree = xml.etree.ElementTree.parse(physical_path)
32 except xml.etree.ElementTree.ParseError as err:
33 self.logger.error('"%s": %s' % (physical_path, err.msg))
34 return False
35 root = tree.getroot()
36 # Special case: particles don't need a diffuse texture.
37 if len(root.findall('.//particles')) > 0:
38 self.textures.append("baseTex")
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'))
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))
48 return True
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:
55 self.logger.error('"%s": %s' % (physical_path, err.msg))
56 return False
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))
63 for element in root.findall('.//texture'):
64 self.textures.append(element.get('name'))
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 = []
74 def read(self, physical_path):
75 try:
76 root = xml.etree.ElementTree.parse(physical_path).getroot()
77 except xml.etree.ElementTree.ParseError as err:
78 self.logger.error('"%s": %s' % (physical_path, err.msg))
79 return False
80 for element in root.findall('.//required_texture'):
81 texture_name = element.get('name')
82 self.required_textures.append(texture_name)
83 return True
86 class Validator:
87 def __init__(self, vfs_root, mods=None):
88 if mods is None:
89 mods = ['mod', 'public']
91 self.vfs_root = vfs_root
92 self.mods = mods
93 self.materials = {}
94 self.invalid_materials = {}
95 self.actors = []
96 self.__init_logger
98 @property
99 def __init_logger(self):
100 logger = getLogger(__name__)
101 logger.setLevel(INFO)
102 # create a console handler, seems nicer to Windows and for future uses
103 ch = StreamHandler(sys.stdout)
104 ch.setLevel(INFO)
105 ch.setFormatter(Formatter('%(levelname)s - %(message)s'))
106 f1 = SingleLevelFilter(INFO, False)
107 ch.addFilter(f1)
108 logger.addHandler(ch)
109 errorch = StreamHandler(sys.stderr)
110 errorch.setLevel(WARNING)
111 errorch.setFormatter(Formatter('%(levelname)s - %(message)s'))
112 logger.addHandler(errorch)
113 self.logger = logger
115 def get_mod_path(self, mod_name, vfs_path):
116 return os.path.join(mod_name, vfs_path)
118 def get_physical_path(self, mod_name, vfs_path):
119 return os.path.realpath(os.path.join(self.vfs_root, mod_name, vfs_path))
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
138 return result
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
146 def find_materials(self, vfs_path):
147 self.logger.info('Collecting materials...')
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'])
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
159 def find_actors(self, vfs_path):
160 self.logger.info('Collecting actors...')
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'])
165 if actor.read(self.get_physical_path(actor_file['mod_name'], actor_file['vfs_path'])):
166 self.actors.append(actor)
168 def run(self):
169 self.find_materials(os.path.join('art', 'materials'))
170 self.find_actors(os.path.join('art', 'actors'))
171 self.logger.info('Validating textures...')
173 for actor in self.actors:
174 if not actor.material:
175 continue
176 if actor.material not in self.materials and actor.material not in self.invalid_materials:
177 self.logger.error('"%s": unknown material "%s"' % (
178 self.get_mod_path(actor.mod_name, actor.vfs_path),
179 actor.material
181 if actor.material not in self.materials:
182 continue
183 material = self.materials[actor.material]
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,
190 material.name
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,
198 material.name
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()