0ad/source/tools/xmlvalidator/validator.py
Stan 936fb5a172 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
This was SVN commit r26350.
2022-02-12 15:43:42 +00:00

209 lines
8.3 KiB
Python

#!/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()