mirror of
https://gitea.wildfiregames.com/0ad/0ad.git
synced 2026-06-23 22:27:24 +00:00
c49d4eedd0
To improve quality und uniformity of the included Python code this lints and formats the included Python files with ruff.
244 lines
9.0 KiB
Python
244 lines
9.0 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
|
|
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 == ".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)
|
|
)
|
|
self.inError = True
|
|
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,
|
|
)
|
|
)
|
|
self.inError = True
|
|
|
|
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,
|
|
)
|
|
)
|
|
self.inError = True
|
|
|
|
return 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)
|