Changeset View
Changeset View
Standalone View
Standalone View
source/tools/entity/checkrefs.py
- This file was added.
Property | Old Value | New Value |
---|---|---|
File Mode | null | 100755 |
#!/usr/bin/env python3 | |||||
from argparse import ArgumentParser | |||||
from collections import Counter | |||||
from decimal import Decimal | |||||
from io import BytesIO | |||||
from json import load, loads | |||||
from pathlib import Path | |||||
from re import split | |||||
from struct import unpack, calcsize | |||||
from subprocess import run | |||||
from sys import stderr, exit | |||||
from xml.etree import ElementTree | |||||
def warn(s): | |||||
print(f"warn: {s}", file=stderr) | |||||
class SimulTemplateEntity: | |||||
def __init__(self, vfs_root): | |||||
self.vfs_root = vfs_root | |||||
def get_file(self, vfs_path, mod): | |||||
return (self.vfs_root / mod / vfs_path).with_suffix('.xml') | |||||
def get_main_mod(self, vfs_path, mods): | |||||
for mod in mods: | |||||
fp = self.get_file(vfs_path, mod) | |||||
if fp.exists(): | |||||
main_mod = mod | |||||
break | |||||
else: | |||||
# default to first mod | |||||
# it should then not exist | |||||
# it will raise an exception when trying to read it | |||||
main_mod = mods[0] | |||||
return main_mod | |||||
def apply_layer(self, base_tag, tag): | |||||
if tag.get('datatype') == 'tokens': | |||||
base_tokens = split(r'\s+', base_tag.text or '') | |||||
tokens = split(r'\s+', tag.text or '') | |||||
final_tokens = base_tokens.copy() | |||||
for token in tokens: | |||||
if token.startswith('-'): | |||||
token_to_remove = token[1:] | |||||
if token_to_remove in final_tokens: | |||||
del final_tokens[token_to_remove] | |||||
elif token not in final_tokens: | |||||
final_tokens.append(token) | |||||
base_tag.text = ' '.join(final_tokens) | |||||
elif tag.get('op'): | |||||
op = tag.get('op') | |||||
op1 = Decimal(base_tag.text or '0') | |||||
op2 = Decimal(tag.text or '0') | |||||
if op == 'add': | |||||
base_tag.text = str(op1 + op2) | |||||
elif op == 'mul': | |||||
Stan: Apparently we also support mul_round can you add it? (it's mul + rounding) | |||||
base_tag.text = str(op1 * op2) | |||||
else: | |||||
raise ValueError(f"Invalid operator '{op}'") | |||||
else: | |||||
base_tag.text = tag.text | |||||
for child in tag: | |||||
base_child = base_tag.find(child.tag) | |||||
if 'disable' in child.attrib: | |||||
if base_child: | |||||
base_tag.remove(base_child) | |||||
else: | |||||
if 'replace' in child.attrib and base_child: | |||||
base_tag.remove(base_child) | |||||
if not base_child: | |||||
base_child = ElementTree.Element(child.tag) | |||||
self.apply_layer(base_child, child) | |||||
if 'replace' in base_child.attrib: | |||||
del base_child.attrib['replace'] | |||||
def load_inherited(self, base_path, vfs_path, mods): | |||||
""" | |||||
vfs_path should be relative to base_path in a mod | |||||
""" | |||||
main_mod = self.get_main_mod(base_path / vfs_path, mods) | |||||
fp = self.get_file(base_path / vfs_path, main_mod) | |||||
layer = ElementTree.parse(fp).getroot() | |||||
for el in layer.iter(): | |||||
children = [x.tag for x in el] | |||||
duplicates = [x for x, c in Counter(children).items() if c > 1] | |||||
if duplicates: | |||||
for dup in duplicates: | |||||
warn(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) | |||||
self.apply_layer(parent, layer) | |||||
return parent | |||||
else: | |||||
return layer | |||||
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.mods = [] | |||||
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('-t', '--validate-templates', action='store_true', | |||||
help="run the validator.py script to check if the xml files match their (.rng) grammar file." | |||||
" This currently only works for the public mod.") | |||||
ap.add_argument('-m', '--mod-to-check', 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()) | |||||
print(f"Checking {'|'.join(args.mods)}'s integrity.") | |||||
print(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() | |||||
# Do some checks based on file, roots, deps | |||||
self.check_deps() | |||||
if args.check_unused: | |||||
self.check_unused() | |||||
print('') | |||||
if args.validate_templates: | |||||
proc = run([str((Path(__file__).parent / '../xmlvalidator/validator.py').resolve())]) | |||||
exit(proc.returncode) | |||||
def get_mod_dependencies(self, *mods): | |||||
modjsondeps = [] | |||||
for mod in mods: | |||||
with open(self.vfs_root / mod / 'mod.json') 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 | |||||
Not Done Inline ActionsMissing if entity.find('Identity') is not None and entity.find('Identity').find('Phenotype') is not None: Stan: Missing
```lang=python
if entity.find('Identity') is not None and entity.find('Identity'). | |||||
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): | |||||
""" | |||||
returns a list of 2-size tuple with: | |||||
- Path relative to the mod base | |||||
- full Path | |||||
""" | |||||
full_exts = ['.' + ext for ext in ext_list] | |||||
def find_recursive(dp, base): | |||||
"""(relative Path, full Path) generator""" | |||||
if dp.is_dir(): | |||||
if dp.name != '.svn' and not dp.name.endswith('~'): | |||||
for fp in dp.iterdir(): | |||||
yield from find_recursive(fp, base) | |||||
elif dp.suffix in full_exts: | |||||
relative_file_path = dp.relative_to(base) | |||||
yield (relative_file_path, dp.resolve()) | |||||
return [(rp, fp) for mod in self.mods for (rp, fp) in find_recursive(self.vfs_root / mod / vfs_path, self.vfs_root / mod)] | |||||
def add_maps_xml(self): | |||||
print("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 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): | |||||
print("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: | |||||
warn(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 = '<L' # little endian long int | |||||
int_len = calcsize(int_fmt) | |||||
version, = unpack(int_fmt, f.read(int_len)) | |||||
if version != 6: | |||||
raise ValueError(f"Invalid PMP version ({version}) in '{ffp}'") | |||||
datasize, = unpack(int_fmt, f.read(int_len)) | |||||
mapsize, = unpack(int_fmt, f.read(int_len)) | |||||
f.seek(2 * (mapsize * 16 + 1) * (mapsize * 16 + 1), 1) # skip heightmap | |||||
numtexs, = unpack(int_fmt, f.read(int_len)) | |||||
for i in range(numtexs): | |||||
length, = unpack(int_fmt, f.read(int_len)) | |||||
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 add_entities(self): | |||||
print("Loading entities...") | |||||
simul_templates_path = Path('simulation/templates') | |||||
simul_template_entity = SimulTemplateEntity(self.vfs_root) | |||||
for (fp, ffp) in sorted(self.find_files(simul_templates_path, 'xml')): | |||||
self.files.append(str(fp)) | |||||
entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), self.mods) | |||||
if entity.get('parent'): | |||||
self.deps.append((str(fp), str(simul_templates_path / (entity.get('parent') + '.xml')))) | |||||
if not str(fp).startswith('template_'): | |||||
self.roots.append(str(fp)) | |||||
if entity.find('VisualActor') and entity.find('VisualActor').find('Actor'): | |||||
phenotype_tag = entity.find('Identity').find('Phenotype') | |||||
phenotypes = split(r'\s', phenotype_tag.text if phenotype_tag and phenotype_tag.text else 'default') | |||||
actor = entity.find('VisualActor').find('Actor') | |||||
if '{phenotype}' in actor.text: | |||||
for phenotype in phenotypes: | |||||
# See simulation2/components/CCmpVisualActor.cpp and Identity.js for explanation. | |||||
actor_path = actor.text.replace('{phenotype}', phenotype) | |||||
self.deps.append((str(fp), f'art/actors/{actor_path}')) | |||||
else: | |||||
actor_path = actor.text | |||||
self.deps.append((str(fp), f'art/actors/{actor_path}')) | |||||
foundaction_actor = entity.find('VisualActor').find('FoundationActor') | |||||
if foundaction_actor: | |||||
self.deps.append((str(fp), f'art/actors/{foundaction_actor.text}')) | |||||
if entity.find('Sound'): | |||||
phenotype_tag = entity.find('Identity').find('Phenotype') | |||||
phenotypes = split(r'\s', phenotype_tag.text if phenotype_tag and phenotype_tag.text else 'default') | |||||
lang_tag = entity.find('Identity').find('Lang') | |||||
lang = lang_tag.text if lang_tag and lang_tag.text else 'greek' | |||||
sound_groups = entity.find('Sound').find('SoundGroups') | |||||
for sound_group in sound_groups: | |||||
if sound_group.text and sound_group.text.strip(): | |||||
if '{phenotype}' in sound_group.text: | |||||
for phenotype in phenotypes: | |||||
# see simulation/components/Sound.js and Identity.js for explanation | |||||
sound_path = sound_group.text.replace('{phenotype}', phenotype).replace('{lang}', lang) | |||||
self.deps.append((str(fp), f'audio/{sound_path}')) | |||||
else: | |||||
sound_path = sound_group.text.replace('{lang}', lang) | |||||
self.deps.append((str(fp), f'audio/{sound_path}')) | |||||
if entity.find('Identity'): | |||||
icon = entity.find('Identity').find('Icon') | |||||
if icon and icon.text: | |||||
self.deps.append((str(fp), f'art/textures/ui/session/portraits/{icon.text}')) | |||||
if entity.find('Heal') and entity.find('Heal').find('RangeOverlay'): | |||||
range_overlay = entity.find('Heal').find('RangeOverlay') | |||||
for tag in ('LineTexture', 'LineTextureMask'): | |||||
elem = range_overlay.find(tag) | |||||
if elem and elem.text: | |||||
self.deps.append((str(fp), f'art/textures/selection/{elem.text}')) | |||||
if entity.find('Selectable') and entity.find('Selectable').find('Overlay') \ | |||||
and entity.find('Selectable').find('Overlay').find('Texture'): | |||||
Not Done Inline ActionsRefs D3123 in case the check assumes the file is at the root. Stan: Refs D3123 in case the check assumes the file is at the root. | |||||
texture = entity.find('Selectable').find('Overlay').find('Texture') | |||||
for tag in ('MainTexture', 'MainTextureMask'): | |||||
elem = texture.find(tag) | |||||
if elem and elem.text: | |||||
self.deps.append((str(fp), f'art/textures/selection/{elem.text}')) | |||||
if entity.find('Formation'): | |||||
icon = entity.find('Formation').find('Icon') | |||||
if icon and icon.text: | |||||
self.deps.append((str(fp), f'art/textures/ui/session/icons/{icon.text}')) | |||||
def append_variant_dependencies(self, variant, fp): | |||||
variant_file = variant.get('file') | |||||
mesh = variant.find('mesh') | |||||
particles = variant.find('particles') | |||||
texture_files = [tex.get('file') for tex in variant.find('textures').findall('texture')] if variant.find('textures') else [] | |||||
prop_actors = [prop.get('actor') for prop in variant.find('props').findall('prop')] if variant.find('props') else [] | |||||
animation_files = [anim.get('file') for anim in variant.find('animations').findall('animation')] if variant.find('animations') else [] | |||||
if variant_file: | |||||
self.deps.append((str(fp), f'art/variants/{variant_file}')) | |||||
if mesh and mesh.text: | |||||
self.deps.append((str(fp), f'art/meshes/{mesh.text}')) | |||||
if particles and particles.get('file'): | |||||
self.deps.append((str(fp), f'art/particles/{particles.get("file")}')) | |||||
for texture_file in [x for x in texture_files if x]: | |||||
self.deps.append((str(fp), f'art/textures/skins/{texture_file}')) | |||||
for prop_actor in [x for x in prop_actors if x]: | |||||
self.deps.append((str(fp), f'art/actors/{prop_actor}')) | |||||
for animation_file in [x for x in animation_files if x]: | |||||
self.deps.append((str(fp), f'art/animation/{animation_file}')) | |||||
def add_actors(self): | |||||
print("Loading actors...") | |||||
for (fp, ffp) in sorted(self.find_files('art/actors', 'xml')): | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
actor = ElementTree.parse(ffp).getroot() | |||||
for group in actor.findall('group'): | |||||
for variant in group.findall('variant'): | |||||
self.append_variant_dependencies(variant, fp) | |||||
material = actor.find('material') | |||||
if material and material.text: | |||||
Not Done Inline ActionsStill get: Traceback (most recent call last): File "checkrefs.py", line 476, in <module> CheckRefs().main() File "checkrefs.py", line 59, in main self.add_gui_xml() File "checkrefs.py", line 353, in add_gui_xml raise ValueError(f"Unexpected GUI XML root element '{name}':\n{bio.read().decode('ascii')}") ValueError: Unexpected GUI XML root element 'page': <page> </page> It could also print the filename :) Stan: Still get:
```
Traceback (most recent call last):
File "checkrefs.py", line 476, in <module>… | |||||
self.deps.append((str(fp), f'art/materials/{material.text}')) | |||||
def add_variants(self): | |||||
print("Loading variants...") | |||||
for (fp, ffp) in sorted(self.find_files('art/variants', 'xml')): | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
variant = ElementTree.parse(ffp).getroot() | |||||
self.append_variant_dependencies(variant, fp) | |||||
def add_art(self): | |||||
print("Loading art files...") | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/textures/particles', *self.supportedTextureFormats)]) | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/textures/terrain', *self.supportedTextureFormats)]) | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/textures/skins', *self.supportedTextureFormats)]) | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/meshes', *self.supportedMeshesFormats)]) | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/animation', *self.supportedAnimationFormats)]) | |||||
def add_materials(self): | |||||
print("Loading materials...") | |||||
for (fp, ffp) in sorted(self.find_files('art/materials', 'xml')): | |||||
self.files.append(str(fp)) | |||||
material_elem = ElementTree.parse(ffp).getroot() | |||||
for alternative in material_elem.findall('alternative'): | |||||
material = alternative.get('material') | |||||
if material: | |||||
self.deps.append((str(fp), f'art/materials/{material}')) | |||||
def add_particles(self): | |||||
print("Loading particles...") | |||||
for (fp, ffp) in sorted(self.find_files('art/particles', 'xml')): | |||||
self.files.append(str(fp)) | |||||
particle = ElementTree.parse(ffp).getroot() | |||||
texture = particle.get('texture') | |||||
if texture: | |||||
self.deps.append((str(fp), texture)) | |||||
def add_soundgroups(self): | |||||
print("Loading sound groups...") | |||||
for (fp, ffp) in sorted(self.find_files('audio', 'xml')): | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
sound_group = ElementTree.parse(ffp).getroot() | |||||
path = sound_group.find('Path').text.rstrip('/') | |||||
for sound in sound_group.findall('sound'): | |||||
self.deps.append((str(fp), f'{path}/{sound.text}')) | |||||
def add_audio(self): | |||||
print("Loading audio files...") | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('audio', 'ogg')]) | |||||
def add_gui_xml(self): | |||||
print("Loading GUI XML...") | |||||
for (fp, ffp) in sorted(self.find_files('gui', 'xml')): | |||||
self.files.append(str(fp)) | |||||
if str(fp).startswith('gui/page_'): | |||||
self.roots.append(str(fp)) | |||||
root_xml = ElementTree.parse(ffp).getroot() | |||||
for include in root_xml.findall('include'): | |||||
# If including an entire directory, find all the *.xml files | |||||
if include.text.endswith('/'): | |||||
self.deps.extend([(str(fp), str(sub_fp)) for (sub_fp, sub_ffp) in self.find_files(f'gui/{include.text}', 'xml')]) | |||||
else: | |||||
self.deps.append((str(fp), f'gui/{include.text}')) | |||||
else: | |||||
xml = ElementTree.parse(ffp) | |||||
root_xml = xml.getroot() | |||||
name = root_xml.tag | |||||
if name in ('objects', 'object'): | |||||
for script in root_xml.findall('script'): | |||||
if script.get('file'): | |||||
self.deps.append((str(fp), script.get('file'))) | |||||
if script.get('directory'): | |||||
# If including an entire directory, find all the *.js files | |||||
self.deps.extend([(str(fp), str(sub_fp)) for (sub_fp, sub_ffp) in self.find_files(script.get('directory'), 'js')]) | |||||
if name == 'objects': | |||||
def add_objects(parent): | |||||
for obj in parent.findall('object'): | |||||
# TODO: look at sprites, styles, etc | |||||
add_objects(obj) | |||||
add_objects(root_xml) | |||||
elif name == 'setup': | |||||
# TODO: look at sprites, styles, etc | |||||
pass | |||||
elif name == 'styles': | |||||
# TODO: look at sprites, styles, etc | |||||
pass | |||||
elif name == 'sprites': | |||||
for sprite in root_xml.findall('sprite'): | |||||
for image in sprite.findall('image'): | |||||
if image.get('texture'): | |||||
self.deps.append((str(fp), f"art/textures/ui/{image.get('texture')}")) | |||||
else: | |||||
bio = BytesIO() | |||||
xml.write(bio) | |||||
bio.seek(0) | |||||
raise ValueError(f"Unexpected GUI XML root element '{name}':\n{bio.read().decode('ascii')}") | |||||
def add_gui_data(self): | |||||
print("Loading GUI data...") | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('gui', 'js')]) | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/textures/ui', *self.supportedTextureFormats)]) | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('art/textures/selection', *self.supportedTextureFormats)]) | |||||
def add_civs(self): | |||||
print("Loading civs...") | |||||
for (fp, ffp) in sorted(self.find_files('simulation/data/civs', 'json')): | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
with open(ffp) as f: | |||||
civ = load(f) | |||||
if civ.get('Emblem', None): | |||||
self.deps.append((str(fp), f"art/textures/ui/{civ['Emblem']}")) | |||||
for music in civ.get('Music', []): | |||||
self.deps.append((str(fp), f"audio/music/{music['File']}")) | |||||
def add_rms(self): | |||||
print("Loading random maps...") | |||||
self.files.extend([str(fp) for (fp, ffp) in self.find_files('maps/random', 'js')]) | |||||
for (fp, ffp) in sorted(self.find_files('maps/random', 'json')): | |||||
if str(fp).startswith('maps/random/rmbiome'): | |||||
continue | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
with open(ffp) as f: | |||||
randmap = load(f) | |||||
settings = randmap.get('settings', {}) | |||||
if settings.get('Script', None): | |||||
self.deps.append((str(fp), f"maps/random/{settings['Script']}")) | |||||
# Map previews | |||||
if settings.get('Preview', None): | |||||
self.deps.append((str(fp), f'art/textures/ui/session/icons/mappreview/{settings["Preview"]}')) | |||||
def add_techs(self): | |||||
print("Loading techs...") | |||||
for (fp, ffp) in sorted(self.find_files('simulation/data/technologies', 'json')): | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
with open(ffp) as f: | |||||
tech = load(f) | |||||
if tech.get('icon', None): | |||||
self.deps.append((str(fp), f"art/textures/ui/session/portraits/technologies/{tech['icon']}")) | |||||
if tech.get('supersedes', None): | |||||
self.deps.append((str(fp), f"simulation/data/technologies/{tech['supersedes']}.json")) | |||||
def add_terrains(self): | |||||
print("Loading terrains...") | |||||
for (fp, ffp) in sorted(self.find_files('art/terrains', 'xml')): | |||||
# ignore terrains.xml | |||||
if str(fp).endswith('terrains.xml'): | |||||
continue | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
terrain = ElementTree.parse(ffp).getroot() | |||||
for texture in terrain.find('textures').findall('texture'): | |||||
if texture.get('file'): | |||||
self.deps.append((str(fp), f"art/textures/terrain/{texture.get('file')}")) | |||||
if terrain.find('material'): | |||||
material = terrain.find('material').text | |||||
self.deps.append((str(fp), f"art/materials/{material}")) | |||||
def add_auras(self): | |||||
print("Loading auras...") | |||||
for (fp, ffp) in sorted(self.find_files('simulation/data/auras', 'json')): | |||||
self.files.append(str(fp)) | |||||
self.roots.append(str(fp)) | |||||
with open(ffp) as f: | |||||
aura = load(f) | |||||
if aura.get('overlayIcon', None): | |||||
self.deps.append((str(fp), aura['overlayIcon'])) | |||||
range_overlay = aura.get('rangeOverlay', {}) | |||||
for prop in ('lineTexture', 'lineTextureMask'): | |||||
if range_overlay.get(prop, None): | |||||
self.deps.append((str(fp), f"art/textures/selection/{range_overlay[prop]}")) | |||||
def check_deps(self): | |||||
uniq_files = set(self.files) | |||||
lower_case_files = {f.lower(): f for f in uniq_files} | |||||
reverse_deps = dict() | |||||
for parent, dep in self.deps: | |||||
if dep not in reverse_deps: | |||||
reverse_deps[dep] = {parent} | |||||
else: | |||||
reverse_deps[dep].add(parent) | |||||
for dep in sorted(reverse_deps.keys()): | |||||
if dep in uniq_files: | |||||
continue | |||||
warn(f"Missing file '{dep}' referenced by: {', '.join(sorted([str(self.vfs_to_relative_to_mods(ref)) for ref in reverse_deps[dep]]))}") | |||||
if dep.lower() in lower_case_files: | |||||
warn(f"### Case-insensitive match (found '{lower_case_files[dep.lower()]}')") | |||||
def check_unused(self): | |||||
reachable = list(set(self.roots)) | |||||
deps = dict() | |||||
for parent, dep in self.deps: | |||||
if parent not in deps: | |||||
deps[parent] = {dep} | |||||
else: | |||||
deps[parent].add(dep) | |||||
while True: | |||||
new_reachable = [] | |||||
for r in reachable: | |||||
new_reachable.extend([x for x in deps.get(r, {}) if x not in reachable]) | |||||
if new_reachable: | |||||
reachable.extend(new_reachable) | |||||
else: | |||||
break | |||||
for f in sorted(self.files): | |||||
if any(( | |||||
f in reachable, | |||||
'art/terrains' in f, | |||||
'maps/random' in f, | |||||
'art/materials' in f, | |||||
)): | |||||
continue | |||||
warn(f"Unused file '{str(self.vfs_to_relative_to_mods(f))}'") | |||||
if __name__ == '__main__': | |||||
CheckRefs().main() |
Wildfire Games · Phabricator
Apparently we also support mul_round can you add it? (it's mul + rounding)