0ad/source/tools/entity/validator.py
Dunedan 03cb24d400
Move xmlvalidator code to entity directory
The xmlvalidator logic is only used by the checkrefs.py script, so this
moves it to the same directory to have it co-located and avoid having to
modify sys.path to import it.
2025-03-03 17:45:08 +01:00

234 lines
8.5 KiB
Python
Executable file

#!/usr/bin/env python3
import argparse
import os
import re
import sys
from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger
from xml.etree import ElementTree as ET
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
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 = ET.parse(physical_path)
except ET.ParseError:
self.logger.exception(physical_path)
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 = ET.parse(physical_path)
except ET.ParseError:
self.logger.exception(physical_path)
return
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 = ET.parse(physical_path).getroot()
except ET.ParseError:
self.logger.exception(physical_path)
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()
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
self.inError = False
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 in (".git", ".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,
)
self.inError = True
if actor.material not in self.materials:
continue
material = self.materials[actor.material]
missing_textures = ", ".join(
{
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,
)
self.inError = True
extra_textures = ", ".join(
{
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,
)
self.inError = True
return not self.inError
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(","))
if not validator.run():
sys.exit(1)