This Trac instance is not used for development anymore!

We migrated our development workflow to git and Gitea.
To test the future redirection, replace trac by ariadne in the page URL.

source: ps/trunk/source/tools/xmlvalidator/validator.py

Last change on this file was 26350, checked in by Stan, 3 years ago

Replace checkrefs.pl by a python script. This makes it easier to run on Windows for non technical persons.

  • Add support for tips
  • Fix other scripts not writing to the correct output (they were writing info messages to stderr)

Based on a patch by: @mammadori and @cyrille

Differential Revision: https://code.wildfiregames.com/D3213

  • Property svn:eol-style set to native
File size: 8.3 KB
RevLine 
[24892]1#!/usr/bin/env python3
[20615]2import argparse
3import os
[26350]4import sys
[20615]5import re
6import xml.etree.ElementTree
[26350]7from logging import getLogger, StreamHandler, INFO, WARNING, Formatter, Filter
[20615]8
[26350]9class 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]20class 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]67class 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
86class 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
201if __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()
Note: See TracBrowser for help on using the repository browser.