Index: build/jenkins/pipelines/docker-differential.Jenkinsfile =================================================================== --- build/jenkins/pipelines/docker-differential.Jenkinsfile +++ build/jenkins/pipelines/docker-differential.Jenkinsfile @@ -1,4 +1,4 @@ -/* Copyright (C) 2022 Wildfire Games. +/* Copyright (C) 2024 Wildfire Games. * This file is part of 0 A.D. * * 0 A.D. is free software: you can redistribute it and/or modify @@ -173,8 +173,8 @@ } stage("Data checks") { steps { - warnError('CheckRefs.pl script failed!') { - sh "cd source/tools/entity/ && python3 checkrefs.py -tax 2> data-errors.txt" + warnError('CheckRefs.py script failed!') { + sh "cd source/tools/entity/ && python3 checkrefs.py -actx 2> data-errors.txt" } } } Index: source/tools/entity/checkrefs.py =================================================================== --- source/tools/entity/checkrefs.py +++ source/tools/entity/checkrefs.py @@ -11,6 +11,12 @@ from scriptlib import SimulTemplateEntity, find_files from logging import WARNING, getLogger, StreamHandler, INFO, Formatter, Filter +# The maximum number of players in a 0ad match. This is also the minimum +# number of AINames necessary in a civ to avoid duplication of names in a +# match. Note: ensure that this matches the value of MAX_NUM_PLAYERS in +# source/tools/atlas/AtlasUI/ScenarioEditor/Sections/Player/Player.cpp. +MAX_NUM_PLAYERS = 8 + class SingleLevelFilter(Filter): def __init__(self, passlevel, reject): self.passlevel = passlevel @@ -63,6 +69,9 @@ " 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('-c', '--check-civ-data', action='store_true', + help="check civilization json data files for sufficient AINames and other" + " minimum requirements.") 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.") @@ -97,6 +106,8 @@ self.add_auras() self.add_tips() self.check_deps() + if args.check_civ_data: + self.check_civ_data() if args.check_unused: self.check_unused() if args.validate_templates: @@ -197,25 +208,39 @@ terrain_name = f.read(length).decode('ascii') # suppose ascii encoding self.deps.append((str(fp), terrains.get(terrain_name, f'art/terrains/(unknown)/{terrain_name}'))) - def get_existing_civ_codes(self): - existing_civs = set() + def get_civ_data_from_tag(self, tag): + civ_data = [] for (_, ffp) in sorted(self.find_files('simulation/data/civs', 'json')): with open(ffp, encoding='utf-8') as f: civ = load(f) - code = civ.get('Code', None) - if code is not None: - existing_civs.add(code) - - return existing_civs + civ_code = civ.get('Code', None) + if civ_code is not None: + result = civ.get(tag, None) + if result is not None: + civ_data.append((civ_code, result)) + + return civ_data + + def get_civ_codes_as_set(self): + civ_data = self.get_civ_data_from_tag('Code') + civ_codes = set() + for civ_data in civ_data: + civ_codes.add(civ_data[0]) + + return civ_codes + + def get_civ_codes_and_ainames(self): + civ_codes_and_ainames = self.get_civ_data_from_tag('AINames') + return civ_codes_and_ainames def get_custom_phase_techs(self): - existing_civs = self.get_existing_civ_codes() + civ_codes = self.get_civ_codes_as_set() custom_phase_techs = [] for (fp, _) in self.find_files("simulation/data/technologies", 'json'): path_str = str(fp) if "phase" in path_str: # Get the last part of the phase tech name. - if basename(path_str).split("_")[-1].split(".")[0] in existing_civs: + if basename(path_str).split("_")[-1].split(".")[0] in civ_codes: custom_phase_techs.append(str(fp.relative_to("simulation/data/technologies")).replace(sep, '/')) return custom_phase_techs @@ -680,6 +705,21 @@ continue self.logger.warning(f"Unused file '{str(self.vfs_to_relative_to_mods(f))}'") + def check_civ_count(self): + civ_codes = self.get_civ_codes_as_set() + if len(civ_codes) == 0: + self.logger.warning(f"No valid civilizations defined in simulation/data/civs") + + def check_civ_ainame_counts(self): + civ_codes_and_ainames = self.get_civ_codes_and_ainames() + for civ_code_and_ainames in civ_codes_and_ainames: + if len(civ_code_and_ainames[1]) < MAX_NUM_PLAYERS: + self.logger.warning(f"civ '{str(civ_code_and_ainames[0])}' has less than the minimum 8 AINames. Add more AINames in order to prevent duplication of AI-controlled player names in a match.") + + def check_civ_data(self): + self.check_civ_count() + self.check_civ_ainame_counts() + if __name__ == '__main__': check_ref = CheckRefs()