Index: source/tools/entity/entvalidate.py =================================================================== --- source/tools/entity/entvalidate.py +++ source/tools/entity/entvalidate.py @@ -1,34 +1,51 @@ #!/usr/bin/env python3 -from os import chdir +import argparse +import logging from pathlib import Path +import shutil from subprocess import run, CalledProcessError -from sys import exit + from xml.etree import ElementTree -from scriptlib import warn, SimulTemplateEntity, find_files + +from scriptlib import SimulTemplateEntity, find_files + +logger = logging.getLogger(__name__) + +root = Path() +relaxng_schema = root / "scripts" / "entity.rng" +vfs_root = root +mod = "community-mod" -def main(): - root = Path(__file__).resolve().parents[3] - relaxng_schema = root / 'binaries' / 'system' / 'entity.rng' +def main() -> int: if not relaxng_schema.exists(): - warn(f"""Relax NG schema non existant. + logging.error(f"""Relax NG schema non existant. Please create the file {relaxng_schema.relative_to(root)} You can do that by running 'pyrogenesis -dumpSchema' in the 'system' directory""") - exit(1) - if run(['xmllint', '--version'], capture_output=True).returncode != 0: - warn("xmllint not found in your PATH, please install it (usually in libxml2 package)") - exit(2) - vfs_root = root / 'binaries' / 'data' / 'mods' + return 1 + if not shutil.which("xmllint"): + logging.error("xmllint not found in your PATH, please install it (usually in libxml2 package)") + return 2 + + parser = argparse.ArgumentParser(description='Validate templates') + parser.add_argument('-p', '--path', nargs='*', + help='Optionally, a list of templates to validate.') + args = parser.parse_args() + simul_templates_path = Path('simulation/templates') - simul_template_entity = SimulTemplateEntity(vfs_root) + simul_template_entity = SimulTemplateEntity(vfs_root, logger) count = 0 failed = 0 - for fp, _ in sorted(find_files(vfs_root, ['public'], 'simulation/templates', 'xml')): + templates = sorted([(Path(p), None) for p in args.path]) if args.path else sorted(find_files(vfs_root, [mod], 'simulation/templates', 'xml')) + for fp, _ in templates: if fp.stem.startswith('template_'): continue + path = fp.as_posix() + if path.startswith('simulation/templates/mixins/') or path.startswith("simulation/templates/special/"): + continue print(f"# {fp}...") count += 1 - entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), ['public']) + entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), [mod]) xmlcontent = ElementTree.tostring(entity, encoding='unicode') try: run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True) @@ -38,7 +55,8 @@ print(e.stdout) print(f"\nTotal: {count}; failed: {failed}") + return 0 + if __name__ == '__main__': - chdir(Path(__file__).resolve().parent) - main() + raise SystemExit(main()) Index: source/tools/entity/scriptlib/__init__.py =================================================================== --- source/tools/entity/scriptlib/__init__.py +++ source/tools/entity/scriptlib/__init__.py @@ -1,10 +1,10 @@ from collections import Counter from decimal import Decimal from re import split -from sys import stderr from xml.etree import ElementTree from os.path import exists + class SimulTemplateEntity: def __init__(self, vfs_root, logger): self.vfs_root = vfs_root @@ -12,7 +12,7 @@ def get_file(self, base_path, vfs_path, mod): default_path = self.vfs_root / mod / base_path - file = (default_path/ "special" / "filter" / vfs_path).with_suffix('.xml') + file = (default_path / "special" / "filter" / vfs_path).with_suffix('.xml') if not exists(file): file = (default_path / "mixins" / vfs_path).with_suffix('.xml') if not exists(file): @@ -48,28 +48,34 @@ elif token not in final_tokens: final_tokens.append(token) base_tag.text = ' '.join(final_tokens) + base_tag.set("datatype", "tokens") elif tag.get('op'): op = tag.get('op') op1 = Decimal(base_tag.text or '0') op2 = Decimal(tag.text or '0') + # Try converting to integers if possible, to pass validation. if op == 'add': - base_tag.text = str(op1 + op2) + base_tag.text = str(int(op1 + op2) if int(op1 + op2) == op1 + op2 else op1 + op2) elif op == 'mul': - base_tag.text = str(op1 * op2) + base_tag.text = str(int(op1 * op2) if int(op1 * op2) == op1 * op2 else op1 * op2) elif op == 'mul_round': base_tag.text = str(round(op1 * op2)) else: raise ValueError(f"Invalid operator '{op}'") else: base_tag.text = tag.text + for prop in tag.attrib: + if prop not in ('disable', 'replace', 'parent', 'merge'): + base_tag.set(prop, tag.get(prop)) for child in tag: base_child = base_tag.find(child.tag) if 'disable' in child.attrib: if base_child is not None: base_tag.remove(base_child) - else: + elif ('merge' not in child.attrib) or (base_child is not None): if 'replace' in child.attrib and base_child is not None: base_tag.remove(base_child) + base_child = None if base_child is None: base_child = ElementTree.Element(child.tag) base_tag.append(base_child) @@ -77,14 +83,19 @@ if 'replace' in base_child.attrib: del base_child.attrib['replace'] - def load_inherited(self, base_path, vfs_path, mods, base = None): + def load_inherited(self, base_path, vfs_path, mods): + entity = self._load_inherited(base_path, vfs_path, mods) + entity[:] = sorted(entity[:], key=lambda x: x.tag) + return entity + + def _load_inherited(self, base_path, vfs_path, mods, base=None): """ vfs_path should be relative to base_path in a mod """ if '|' in vfs_path: - paths = vfs_path.split("|", 2) - base = self.load_inherited(base_path, paths[1], mods, base); - base = self.load_inherited(base_path, paths[0], mods, base); + paths = vfs_path.split("|", 1) + base = self._load_inherited(base_path, paths[1], mods, base) + base = self._load_inherited(base_path, paths[0], mods, base) return base main_mod = self.get_main_mod(base_path, vfs_path, mods) @@ -97,7 +108,7 @@ for dup in duplicates: self.logger.warning(f"Duplicate child node '{dup}' in tag {el.tag} of {fp}") if layer.get('parent'): - parent = self.load_inherited(base_path, layer.get('parent'), mods) + parent = self._load_inherited(base_path, layer.get('parent'), mods, base) self.apply_layer(parent, layer) return parent else: