Index: ps/trunk/source/tools/entity/checkrefs.py =================================================================== --- ps/trunk/source/tools/entity/checkrefs.py (revision 26990) +++ ps/trunk/source/tools/entity/checkrefs.py (revision 26991) @@ -1,610 +1,646 @@ #!/usr/bin/env python3 from argparse import ArgumentParser from io import BytesIO from json import load, loads from pathlib import Path from re import split, match from struct import unpack, calcsize from os.path import sep, exists, basename from xml.etree import ElementTree import sys from scriptlib import SimulTemplateEntity, find_files from logging import WARNING, getLogger, StreamHandler, INFO, 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 CheckRefs: def __init__(self): # list of relative root file:str self.files = [] # list of relative file:str self.roots = [] # list of tuple (parent_file:str, dep_file:str) self.deps = [] self.vfs_root = Path(__file__).resolve().parents[3] / 'binaries' / 'data' / 'mods' self.supportedTextureFormats = ('dds', 'png') self.supportedMeshesFormats = ('pmd', 'dae') self.supportedAnimationFormats = ('psa', 'dae') self.supportedAudioFormats = ('ogg') self.mods = [] 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 def main(self): ap = ArgumentParser(description="Checks the game files for missing dependencies, unused files," " and for file integrity.") ap.add_argument('-u', '--check-unused', action='store_true', help="check for all the unused files in the given mods and their dependencies." " Implies --check-map-xml. Currently yields a lot of false positives.") ap.add_argument('-x', '--check-map-xml', action='store_true', help="check maps for missing actor and templates.") ap.add_argument('-a', '--validate-actors', action='store_true', help="run the validator.py script to check if the actors files have extra or missing textures." " This currently only works for the public mod.") ap.add_argument('-t', '--validate-templates', action='store_true', help="run the validator.py script to check if the xml files match their (.rng) grammar file.") ap.add_argument('-m', '--mods', metavar="MOD", dest='mods', nargs='+', default=['public'], help="specify which mods to check. Default to public.") args = ap.parse_args() # force check_map_xml if check_unused is used to avoid false positives. args.check_map_xml |= args.check_unused # ordered uniq mods (dict maintains ordered keys from python 3.6) self.mods = list(dict.fromkeys([*args.mods, *self.get_mod_dependencies(*args.mods), 'mod']).keys()) self.logger.info(f"Checking {'|'.join(args.mods)}'s integrity.") self.logger.info(f"The following mods will be loaded: {'|'.join(self.mods)}.") if args.check_map_xml: self.add_maps_xml() self.add_maps_pmp() self.add_entities() self.add_actors() self.add_variants() self.add_art() self.add_materials() self.add_particles() self.add_soundgroups() self.add_audio() self.add_gui_xml() self.add_gui_data() self.add_civs() self.add_rms() self.add_techs() self.add_terrains() self.add_auras() self.add_tips() self.check_deps() if args.check_unused: self.check_unused() if args.validate_templates: sys.path.append("../xmlvalidator/") from validate_grammar import RelaxNGValidator validate = RelaxNGValidator(self.vfs_root, self.mods) validate.run() if args.validate_actors: sys.path.append("../xmlvalidator/") from validator import Validator validator = Validator(self.vfs_root, self.mods) validator.run() def get_mod_dependencies(self, *mods): modjsondeps = [] for mod in mods: mod_json_path = self.vfs_root / mod / 'mod.json' if not exists(mod_json_path): continue with open(mod_json_path, encoding='utf-8') as f: modjson = load(f) # 0ad's folder isn't named like the mod. modjsondeps.extend(['public' if '0ad' in dep else dep for dep in modjson.get('dependencies', [])]) return modjsondeps def vfs_to_relative_to_mods(self, vfs_path): for dep in self.mods: fn = Path(dep) / vfs_path if (self.vfs_root / fn).exists(): return fn return None def vfs_to_physical(self, vfs_path): fn = self.vfs_to_relative_to_mods(vfs_path) return self.vfs_root / fn def find_files(self, vfs_path, *ext_list): return find_files(self.vfs_root, self.mods, vfs_path, *ext_list) def add_maps_xml(self): self.logger.info("Loading maps XML...") mapfiles = self.find_files('maps/scenarios', 'xml') mapfiles.extend(self.find_files('maps/skirmishes', 'xml')) mapfiles.extend(self.find_files('maps/tutorials', 'xml')) actor_prefix = 'actor|' resource_prefix = 'resource|' for (fp, ffp) in sorted(mapfiles): self.files.append(str(fp)) self.roots.append(str(fp)) et_map = ElementTree.parse(ffp).getroot() entities = et_map.find('Entities') used = {entity.find('Template').text.strip() for entity in entities.findall('Entity')} if entities is not None else {} for template in used: if template.startswith(actor_prefix): self.deps.append((str(fp), f'art/actors/{template[len(actor_prefix):]}')) elif template.startswith(resource_prefix): self.deps.append((str(fp), f'simulation/templates/{template[len(resource_prefix):]}.xml')) else: self.deps.append((str(fp), f'simulation/templates/{template}.xml')) # Map previews settings = loads(et_map.find('ScriptSettings').text) if settings.get('Preview', None): self.deps.append((str(fp), f'art/textures/ui/session/icons/mappreview/{settings["Preview"]}')) def add_maps_pmp(self): self.logger.info("Loading maps PMP...") # Need to generate terrain texture filename=>relative path lookup first terrains = dict() for (fp, ffp) in self.find_files('art/terrains', 'xml'): name = fp.stem # ignore terrains.xml if name != 'terrains': if name in terrains: self.logger.warning(f"Duplicate terrain name '{name}' (from '{terrains[name]}' and '{ffp}')") terrains[name] = str(fp) mapfiles = self.find_files('maps/scenarios', 'pmp') mapfiles.extend(self.find_files('maps/skirmishes', 'pmp')) for (fp, ffp) in sorted(mapfiles): self.files.append(str(fp)) self.roots.append(str(fp)) with open(ffp, 'rb') as f: expected_header = b'PSMP' header = f.read(len(expected_header)) if header != expected_header: raise ValueError(f"Invalid PMP header {header} in '{ffp}'") int_fmt = '