diff --git a/.gitea/workflows/checkrefs.yml b/.gitea/workflows/checkrefs.yml index f5ed1b557b..34445cda2c 100644 --- a/.gitea/workflows/checkrefs.yml +++ b/.gitea/workflows/checkrefs.yml @@ -11,6 +11,8 @@ jobs: steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 + with: + python-version: "3.11" - name: Add remote fork origin for LFS run: | PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}" @@ -31,9 +33,10 @@ jobs: - name: Download necessary LFS assets shell: sh {0} run: | - git lfs pull -I binaries/data/mods/public/maps + git lfs pull -I binaries/data/mods/public/art/meshes,binaries/data/mods/public/maps ORIGIN_LFS_PULL_RESULT=$? - git lfs pull ${{ gitea.actor }} -I binaries/data/mods/public/maps + git lfs pull ${{ gitea.actor }} \ + -I binaries/data/mods/public/art/meshes,binaries/data/mods/public/maps FORK_LFS_PULL_RESULT=$? PR_REPO="${{ gitea.event.pull_request.head.repo.full_name || gitea.repository }}" @@ -47,9 +50,12 @@ jobs: fi - name: Install lxml run: pip3 install lxml + - name: Install collada + run: pip3 install "pycollada>=0.9" - name: Check for missing references run: | ./source/tools/entity/checkrefs.py \ --check-map-xml \ --validate-templates \ - --validate-actors + --validate-actors \ + --validate-meshes diff --git a/source/tools/entity/checkrefs.py b/source/tools/entity/checkrefs.py index 52f60a7a35..d16f995e55 100755 --- a/source/tools/entity/checkrefs.py +++ b/source/tools/entity/checkrefs.py @@ -119,6 +119,12 @@ class CheckRefs: default=["public"], help="specify which mods to check. Default to public.", ) + ap.add_argument( + "-d", + "--validate-meshes", + action="store_true", + help="check if meshes have vertices with no weight.", + ) args = ap.parse_args() # force check_map_xml if check_unused is used to avoid false positives. args.check_map_xml |= args.check_unused @@ -161,6 +167,12 @@ class CheckRefs: if not validator.run(): self.inError = True + if args.validate_meshes: + from validate_dae import DaeValidator + + dv = DaeValidator(self.vfs_root, self.mods) + self.inError = not dv.run() + return not self.inError def get_mod_dependencies(self, *mods): diff --git a/source/tools/entity/validate_dae.py b/source/tools/entity/validate_dae.py new file mode 100755 index 0000000000..12a845a3d8 --- /dev/null +++ b/source/tools/entity/validate_dae.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 + +import sys +from logging import INFO, Formatter, StreamHandler, getLogger +from pathlib import Path + +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 + for ctr in dae.controllers: + if type(ctr) is not controller.Skin: + had_exception = True + break + 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) + + +class DaeValidator: + def __init__(self, vfs_root, mods): + self.has_weightless_vtx = [] + self.vfs_root = vfs_root + self.mods = mods + + self.log = getLogger() + self.log.setLevel(INFO) + sh = StreamHandler(sys.stdout) + sh.setLevel(INFO) + sh.setFormatter(Formatter("%(levelname)s - %(message)s")) + self.log.addHandler(sh) + + def run(self): + is_ok = True + files = find_files(self.vfs_root, self.mods, "art/meshes", "dae") + self.log.info("Checking %i meshes for invalid weights.", len(files)) + for _, dae in files: + status = validate_vertex(dae.as_posix(), self.log.warning) + if status >= 1: + is_ok = False + if status >= 2: + self.has_weightless_vtx.append(dae) + self.log.info( + "%i out of %i files have vertices with no weight or bones.", + len(self.has_weightless_vtx), + len(files), + ) + return is_ok + + +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)