mirror of
https://gitea.wildfiregames.com/0ad/0ad
synced 2026-06-16 05:13:58 -07:00
In the ruff config file added in #6954 explicitly selecting the ruff rules to check was missed, resulting in ruff only checking a very small subset of its available rules. That hasn't been desired, so this is the first of a series of commits enabling more rules. In this PR all rules whose violations can be either automatically fixed by ruff or are trivial to fix manually get enabled. For the follow up PRs it's intended to focus on one area of rules per PR to gradually improve the Python code quality.
235 lines
8.5 KiB
Python
Executable file
235 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
|
|
|
|
@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 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 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)
|