diff --git a/source/tools/entity/validate_dae.py b/source/tools/entity/validate_dae.py index 110e8c1f42..223280bceb 100755 --- a/source/tools/entity/validate_dae.py +++ b/source/tools/entity/validate_dae.py @@ -1,6 +1,8 @@ #!/usr/bin/env python3 import sys +from argparse import ArgumentParser +from concurrent.futures import ProcessPoolExecutor, as_completed from logging import INFO, Formatter, StreamHandler, getLogger from pathlib import Path @@ -8,36 +10,32 @@ from collada import Collada, DaeBrokenRefError, DaeError, common, controller from scriptlib import find_files -def validate_vertex(path_dae, log=print): - has_weightless = False - had_exception = False - dae = None - try: - dae = Collada(path_dae, ignore=[common.DaeUnsupportedError, DaeBrokenRefError]) - except DaeError as inst: - log("Failed to load %s", path_dae) - log(type(inst), inst) - had_exception = True +class InvalidControllerError(Exception): + """Mesh contains controllers of other types than Skin.""" + + +class VertexWithoutWeightError(Exception): + """Vertex has no weight.""" + + def __init__(self, num_vertices: int, num_vertices_no_weight: int) -> None: + self.num_vertices = num_vertices + self.num_vertices_no_weight = num_vertices_no_weight + + +def validate_mesh(path_dae: Path) -> None: + """Validate a mesh.""" + dae = Collada(path_dae.as_posix(), ignore=[common.DaeUnsupportedError, DaeBrokenRefError]) for ctr in dae.controllers: if type(ctr) is not controller.Skin: - had_exception = True - break + raise InvalidControllerError totalv = len(ctr.vcounts) totalv_0 = len(ctr.vcounts[ctr.vcounts == 0]) if totalv_0 > 0: - log( - "Mesh %s has %i (out of %i) vertices with no weight" - " and no bone assigned. Use P294 to find them in Blender.", - path_dae, - totalv_0, - totalv, - ) - has_weightless = True - return (int(has_weightless) << 1) | int(had_exception) + raise VertexWithoutWeightError(totalv, totalv_0) class DaeValidator: - def __init__(self, vfs_root, mods): + def __init__(self, vfs_root: Path, mods: list[str]) -> None: self.has_weightless_vtx = [] self.vfs_root = vfs_root self.mods = mods @@ -49,17 +47,43 @@ class DaeValidator: sh.setFormatter(Formatter("%(levelname)s - %(message)s")) self.log.addHandler(sh) - def run(self): + def run(self) -> bool: + """Run validation for a bunch of meshes.""" is_ok = True - files = find_files(self.vfs_root, self.mods, Path("art/meshes"), ["dae"]) i = 0 - for _, dae in files: - i += 1 - status = validate_vertex(dae.as_posix(), self.log.warning) - if status >= 1: - is_ok = False - if status >= 2: - self.has_weightless_vtx.append(dae) + with ProcessPoolExecutor() as executor: + futures = {} + for _, dae_path in find_files(self.vfs_root, self.mods, Path("art/meshes"), ["dae"]): + future = executor.submit(validate_mesh, dae_path) + futures[future] = dae_path + + for future in as_completed(futures): + i += 1 + + dae_path = futures[future] + + try: + future.result() + except InvalidControllerError: + self.log.warning("Mesh %s contains an invalid controller.", dae_path) + is_ok = False + continue + except DaeError as exc: + self.log.warning("Failed to load mesh %s: %s", dae_path, exc) + is_ok = False + continue + except VertexWithoutWeightError as exc: + self.log.warning( + "Mesh %s has %i (out of %i) vertices with no weight" + " and no bone assigned. Use P294 to find them in Blender.", + dae_path, + exc.num_vertices_no_weight, + exc.num_vertices, + ) + self.has_weightless_vtx.append(dae_path) + is_ok = False + continue + self.log.info( "%i out of %i files have vertices with no weight or bones.", len(self.has_weightless_vtx), @@ -68,9 +92,30 @@ class DaeValidator: return is_ok +def main() -> None: + parser = ArgumentParser(description="Validate COLLADA meshes.") + parser.add_argument( + "-m", + "--mod-name", + dest="mod_names", + nargs="*", + default=["public"], + help="The name of the mod to validate.", + ) + parser.add_argument( + "-r", + "--root", + dest="vfs_root", + default=Path(__file__).resolve().parents[3] / "binaries" / "data" / "mods", + type=Path, + help="The path to mod's root location.", + ) + args = parser.parse_args() + + dv = DaeValidator(args.vfs_root, args.mod_names) + if dv.run() > 0: + sys.exit(1) + + if __name__ == "__main__": - vfsr = Path(__file__).resolve().parents[3] / "binaries" / "data" / "mods" - mods = ["public"] - dv = DaeValidator(vfsr, mods) - print(f"DaeValidator returns {dv.run()}") - print(dv.has_weightless_vtx) + main()