2021-02-12 13:25:33 -08:00
|
|
|
#!/usr/bin/env python3
|
2017-12-08 13:40:29 -08:00
|
|
|
import argparse
|
|
|
|
|
import os
|
|
|
|
|
import re
|
2024-08-24 21:29:39 -07:00
|
|
|
import sys
|
|
|
|
|
from logging import INFO, WARNING, Filter, Formatter, StreamHandler, getLogger
|
|
|
|
|
from xml.etree import ElementTree as ET
|
2022-02-12 07:43:42 -08:00
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
|
2022-02-12 07:43:42 -08:00
|
|
|
class SingleLevelFilter(Filter):
|
|
|
|
|
def __init__(self, passlevel, reject):
|
|
|
|
|
self.passlevel = passlevel
|
|
|
|
|
self.reject = reject
|
|
|
|
|
|
|
|
|
|
def filter(self, record):
|
|
|
|
|
if self.reject:
|
2024-08-22 00:18:20 -07:00
|
|
|
return record.levelno != self.passlevel
|
2024-08-24 21:29:39 -07:00
|
|
|
return record.levelno == self.passlevel
|
2024-08-22 00:18:20 -07:00
|
|
|
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
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 = []
|
2024-08-22 00:18:20 -07:00
|
|
|
self.material = ""
|
2022-02-09 14:07:36 -08:00
|
|
|
self.logger = getLogger(__name__)
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
def read(self, physical_path):
|
2017-12-12 12:23:26 -08:00
|
|
|
try:
|
2024-08-24 21:29:39 -07:00
|
|
|
tree = ET.parse(physical_path)
|
|
|
|
|
except ET.ParseError:
|
|
|
|
|
self.logger.exception(physical_path)
|
2017-12-12 12:23:26 -08:00
|
|
|
return False
|
2017-12-08 13:40:29 -08:00
|
|
|
root = tree.getroot()
|
2022-02-09 14:07:36 -08:00
|
|
|
# Special case: particles don't need a diffuse texture.
|
2024-08-22 00:18:20 -07:00
|
|
|
if len(root.findall(".//particles")) > 0:
|
2022-02-09 14:07:36 -08:00
|
|
|
self.textures.append("baseTex")
|
|
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
for element in root.findall(".//material"):
|
2017-12-08 13:40:29 -08:00
|
|
|
self.material = element.text
|
2024-08-22 00:18:20 -07:00
|
|
|
for element in root.findall(".//texture"):
|
|
|
|
|
self.textures.append(element.get("name"))
|
|
|
|
|
for element in root.findall(".//variant"):
|
|
|
|
|
file = element.get("file")
|
2022-02-09 14:07:36 -08:00
|
|
|
if file:
|
2024-08-22 00:18:20 -07:00
|
|
|
self.read_variant(physical_path, os.path.join("art", "variants", file))
|
2017-12-12 12:23:26 -08:00
|
|
|
return True
|
2017-12-08 13:40:29 -08:00
|
|
|
|
2022-02-09 14:07:36 -08:00
|
|
|
def read_variant(self, actor_physical_path, relative_path):
|
|
|
|
|
physical_path = actor_physical_path.replace(self.vfs_path, relative_path)
|
|
|
|
|
try:
|
2024-08-24 21:29:39 -07:00
|
|
|
tree = ET.parse(physical_path)
|
|
|
|
|
except ET.ParseError:
|
|
|
|
|
self.logger.exception(physical_path)
|
|
|
|
|
return
|
2022-02-09 14:07:36 -08:00
|
|
|
|
|
|
|
|
root = tree.getroot()
|
2024-08-22 00:18:20 -07:00
|
|
|
file = root.get("file")
|
2022-02-09 14:07:36 -08:00
|
|
|
if file:
|
2024-08-22 00:18:20 -07:00
|
|
|
self.read_variant(actor_physical_path, os.path.join("art", "variants", file))
|
2022-02-09 14:07:36 -08:00
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
for element in root.findall(".//texture"):
|
|
|
|
|
self.textures.append(element.get("name"))
|
2022-02-09 14:07:36 -08:00
|
|
|
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
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):
|
2017-12-12 12:23:26 -08:00
|
|
|
try:
|
2024-08-24 21:29:39 -07:00
|
|
|
root = ET.parse(physical_path).getroot()
|
|
|
|
|
except ET.ParseError:
|
|
|
|
|
self.logger.exception(physical_path)
|
2017-12-12 12:23:26 -08:00
|
|
|
return False
|
2024-08-22 00:18:20 -07:00
|
|
|
for element in root.findall(".//required_texture"):
|
|
|
|
|
texture_name = element.get("name")
|
2017-12-08 13:40:29 -08:00
|
|
|
self.required_textures.append(texture_name)
|
2017-12-12 12:23:26 -08:00
|
|
|
return True
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class Validator:
|
|
|
|
|
def __init__(self, vfs_root, mods=None):
|
|
|
|
|
if mods is None:
|
2024-08-22 00:18:20 -07:00
|
|
|
mods = ["mod", "public"]
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
self.vfs_root = vfs_root
|
|
|
|
|
self.mods = mods
|
|
|
|
|
self.materials = {}
|
2017-12-12 12:23:26 -08:00
|
|
|
self.invalid_materials = {}
|
2017-12-08 13:40:29 -08:00
|
|
|
self.actors = []
|
2024-08-27 10:28:11 -07:00
|
|
|
self.__init_logger()
|
2022-02-09 14:07:36 -08:00
|
|
|
|
|
|
|
|
def __init_logger(self):
|
|
|
|
|
logger = getLogger(__name__)
|
|
|
|
|
logger.setLevel(INFO)
|
2022-02-12 07:43:42 -08:00
|
|
|
# create a console handler, seems nicer to Windows and for future uses
|
|
|
|
|
ch = StreamHandler(sys.stdout)
|
2022-02-09 14:07:36 -08:00
|
|
|
ch.setLevel(INFO)
|
2024-08-22 00:18:20 -07:00
|
|
|
ch.setFormatter(Formatter("%(levelname)s - %(message)s"))
|
2022-02-12 07:43:42 -08:00
|
|
|
f1 = SingleLevelFilter(INFO, False)
|
|
|
|
|
ch.addFilter(f1)
|
2022-02-09 14:07:36 -08:00
|
|
|
logger.addHandler(ch)
|
2022-02-12 07:43:42 -08:00
|
|
|
errorch = StreamHandler(sys.stderr)
|
|
|
|
|
errorch.setLevel(WARNING)
|
2024-08-22 00:18:20 -07:00
|
|
|
errorch.setFormatter(Formatter("%(levelname)s - %(message)s"))
|
2022-02-12 07:43:42 -08:00
|
|
|
logger.addHandler(errorch)
|
2022-02-09 14:07:36 -08:00
|
|
|
self.logger = logger
|
2024-08-23 06:37:51 -07:00
|
|
|
self.inError = False
|
2017-12-08 13:40:29 -08:00
|
|
|
|
2022-02-09 15:06:40 -08:00
|
|
|
def get_mod_path(self, mod_name, vfs_path):
|
|
|
|
|
return os.path.join(mod_name, vfs_path)
|
|
|
|
|
|
2017-12-08 13:40:29 -08:00
|
|
|
def get_physical_path(self, mod_name, vfs_path):
|
2017-12-12 12:23:26 -08:00
|
|
|
return os.path.realpath(os.path.join(self.vfs_root, mod_name, vfs_path))
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
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):
|
2024-08-24 21:29:39 -07:00
|
|
|
if file_name in (".git", ".svn"):
|
2017-12-08 13:40:29 -08:00
|
|
|
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):
|
2024-08-22 00:18:20 -07:00
|
|
|
result.append({"mod_name": mod_name, "vfs_path": vfs_file_path})
|
2017-12-08 13:40:29 -08:00
|
|
|
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):
|
2024-08-22 00:18:20 -07:00
|
|
|
self.logger.info("Collecting materials...")
|
|
|
|
|
material_files = self.find_all_mods_files(vfs_path, re.compile(r".*\.xml"))
|
2017-12-08 13:40:29 -08:00
|
|
|
for material_file in material_files:
|
2024-08-22 00:18:20 -07:00
|
|
|
material_name = os.path.basename(material_file["vfs_path"])
|
2017-12-08 13:40:29 -08:00
|
|
|
if material_name in self.materials:
|
|
|
|
|
continue
|
2024-08-22 00:18:20 -07:00
|
|
|
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"])
|
|
|
|
|
):
|
2017-12-12 12:23:26 -08:00
|
|
|
self.materials[material_name] = material
|
|
|
|
|
else:
|
|
|
|
|
self.invalid_materials[material_name] = material
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
def find_actors(self, vfs_path):
|
2024-08-22 00:18:20 -07:00
|
|
|
self.logger.info("Collecting actors...")
|
2022-02-12 07:43:42 -08:00
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
actor_files = self.find_all_mods_files(vfs_path, re.compile(r".*\.xml"))
|
2017-12-08 13:40:29 -08:00
|
|
|
for actor_file in actor_files:
|
2024-08-22 00:18:20 -07:00
|
|
|
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"])):
|
2017-12-12 12:23:26 -08:00
|
|
|
self.actors.append(actor)
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
def run(self):
|
2024-08-22 00:18:20 -07:00
|
|
|
self.find_materials(os.path.join("art", "materials"))
|
|
|
|
|
self.find_actors(os.path.join("art", "actors"))
|
|
|
|
|
self.logger.info("Validating textures...")
|
2017-12-08 13:40:29 -08:00
|
|
|
|
|
|
|
|
for actor in self.actors:
|
|
|
|
|
if not actor.material:
|
|
|
|
|
continue
|
2024-08-22 00:18:20 -07:00
|
|
|
if (
|
|
|
|
|
actor.material not in self.materials
|
|
|
|
|
and actor.material not in self.invalid_materials
|
|
|
|
|
):
|
|
|
|
|
self.logger.error(
|
2024-08-24 21:29:39 -07:00
|
|
|
'"%s": unknown material "%s"',
|
|
|
|
|
self.get_mod_path(actor.mod_name, actor.vfs_path),
|
|
|
|
|
actor.material,
|
2024-08-22 00:18:20 -07:00
|
|
|
)
|
2024-08-23 06:37:51 -07:00
|
|
|
self.inError = True
|
2017-12-12 12:23:26 -08:00
|
|
|
if actor.material not in self.materials:
|
|
|
|
|
continue
|
2017-12-08 13:40:29 -08:00
|
|
|
material = self.materials[actor.material]
|
2022-02-09 14:07:36 -08:00
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
missing_textures = ", ".join(
|
2024-08-24 21:29:39 -07:00
|
|
|
{
|
|
|
|
|
required_texture
|
|
|
|
|
for required_texture in material.required_textures
|
|
|
|
|
if required_texture not in actor.textures
|
|
|
|
|
}
|
2024-08-22 00:18:20 -07:00
|
|
|
)
|
2022-02-09 14:07:36 -08:00
|
|
|
if len(missing_textures) > 0:
|
2024-08-22 00:18:20 -07:00
|
|
|
self.logger.error(
|
2024-08-24 21:29:39 -07:00
|
|
|
'"%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,
|
2024-08-22 00:18:20 -07:00
|
|
|
)
|
2024-08-23 06:37:51 -07:00
|
|
|
self.inError = True
|
2022-02-09 14:07:36 -08:00
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
extra_textures = ", ".join(
|
2024-08-24 21:29:39 -07:00
|
|
|
{
|
|
|
|
|
extra_texture
|
|
|
|
|
for extra_texture in actor.textures
|
|
|
|
|
if extra_texture not in material.required_textures
|
|
|
|
|
}
|
2024-08-22 00:18:20 -07:00
|
|
|
)
|
2022-02-09 14:07:36 -08:00
|
|
|
if len(extra_textures) > 0:
|
2024-08-22 00:18:20 -07:00
|
|
|
self.logger.warning(
|
2024-08-24 21:29:39 -07:00
|
|
|
'"%s": actor contains unnecessary texture(s) "%s" from "%s"',
|
|
|
|
|
self.get_mod_path(actor.mod_name, actor.vfs_path),
|
|
|
|
|
extra_textures,
|
|
|
|
|
material.name,
|
2024-08-22 00:18:20 -07:00
|
|
|
)
|
2024-08-23 06:37:51 -07:00
|
|
|
self.inError = True
|
|
|
|
|
|
2024-08-25 08:08:00 -07:00
|
|
|
return not self.inError
|
2017-12-08 13:40:29 -08:00
|
|
|
|
2024-08-22 00:18:20 -07:00
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
2017-12-08 13:40:29 -08:00
|
|
|
script_dir = os.path.dirname(os.path.realpath(__file__))
|
2024-08-22 00:18:20 -07:00
|
|
|
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")
|
2017-12-08 13:40:29 -08:00
|
|
|
args = parser.parse_args()
|
2024-08-22 00:18:20 -07:00
|
|
|
validator = Validator(args.root, args.mods.split(","))
|
2024-08-23 06:37:51 -07:00
|
|
|
if not validator.run():
|
|
|
|
|
sys.exit(1)
|