mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 13:23:56 -07:00
- 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.
209 lines
8.3 KiB
Python
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()
|