1
0
forked from 0ad/0ad

Replace checkrefs.pl by a python script. This makes it easier to run on Windows for non technical persons.

- Add support for tips
- Fix other scripts not writing to the correct output (they were writing
info messages to stderr)

Based on a patch by: @mammadori	and @cyrille
Differential Revision: https://code.wildfiregames.com/D3213
This was SVN commit r26350.
This commit is contained in:
Stan 2022-02-12 15:43:42 +00:00
parent f17077272f
commit 936fb5a172
9 changed files with 891 additions and 937 deletions

View File

@ -1,191 +0,0 @@
package Entity;
use strict;
use warnings;
use XML::Parser;
use Data::Dumper;
use File::Find;
my $vfsroot = '../../../binaries/data/mods';
sub get_filename
{
my ($vfspath, $mod) = @_;
my $fn = "$vfsroot/$mod/simulation/templates/special/filter/$vfspath.xml";
if (not -e $fn) {
$fn = "$vfsroot/$mod/simulation/templates/mixins/$vfspath.xml";
}
if (not -e $fn) {
$fn = "$vfsroot/$mod/simulation/templates/$vfspath.xml";
}
return $fn;
}
sub get_file
{
my ($vfspath, $mod) = @_;
my $fn = get_filename($vfspath, $mod);
open my $f, $fn or die "Error loading $fn: $!";
local $/;
return <$f>;
}
sub trim
{
my ($t) = @_;
return '' if not defined $t;
$t =~ /^\s*(.*?)\s*$/s;
return $1;
}
sub load_xml
{
my ($vfspath, $file) = @_;
my $root = {};
my @stack = ($root);
my $p = new XML::Parser(Handlers => {
Start => sub {
my ($e, $n, %a) = @_;
my $t = {};
die "Duplicate child node '$n'" if exists $stack[-1]{$n};
$stack[-1]{$n} = $t;
for (keys %a) {
$t->{'@'.$_}{' content'} = trim($a{$_});
}
push @stack, $t;
},
End => sub {
my ($e, $n) = @_;
$stack[-1]{' content'} = trim($stack[-1]{' content'});
pop @stack;
},
Char => sub {
my ($e, $str) = @_;
$stack[-1]{' content'} .= $str;
},
});
eval {
$p->parse($file);
};
if ($@) {
die "Error parsing $vfspath: $@";
}
return $root;
}
sub apply_layer
{
my ($base, $new) = @_;
if ($new->{'@datatype'} and $new->{'@datatype'}{' content'} eq 'tokens') {
my @old = split /\s+/, ($base->{' content'} || '');
my @new = split /\s+/, ($new->{' content'} || '');
my @t = @old;
for my $n (@new) {
if ($n =~ /^-(.*)/) {
@t = grep $_ ne $1, @t;
} else {
push @t, $n if not grep $_ eq $n, @t;
}
}
$base->{' content'} = join ' ', @t;
} elsif ($new->{'@op'}) {
my $op = $new->{'@op'}{' content'};
my $op1 = $base->{' content'};
my $op2 = $new->{' content'};
if ($op eq 'add') {
$base->{' content'} = $op1 + $op2;
}
elsif ($op eq 'mul') {
$base->{' content'} = $op1 * $op2;
}
elsif ($op eq 'mul_round') {
# This is incorrect (floors instead of rounding)
# but for schema purposes it ought be fine.
$base->{' content'} = int($op1 * $op2);
}
else {
die "Invalid operator '$op'";
}
} else {
$base->{' content'} = $new->{' content'};
}
for my $k (grep $_ ne ' content', keys %$new) {
if ($new->{$k}{'@disable'}) {
delete $base->{$k};
} else {
if ($new->{$k}{'@replace'}) {
delete $base->{$k};
}
$base->{$k} ||= {};
apply_layer($base->{$k}, $new->{$k});
delete $base->{$k}{'@replace'};
}
}
}
sub get_main_mod
{
my ($vfspath, $mods) = @_;
my @mods_list = split(/\|/, $mods);
my $main_mod = $mods_list[0];
my $fn = "$vfsroot/$main_mod/simulation/templates/$vfspath.xml";
if (not -e $fn)
{
for my $dep (@mods_list)
{
$fn = "$vfsroot/$dep/simulation/templates/$vfspath.xml";
if (-e $fn)
{
$main_mod = $dep;
last;
}
}
}
return $main_mod;
}
sub load_inherited
{
my ($vfspath, $mods, $base) = @_;
if ($vfspath =~ /\|/) {
my @paths = split(/\|/, $vfspath, 2);
$base = load_inherited($paths[1], $mods, $base);
$base = load_inherited($paths[0], $mods, $base);
return $base
}
my $main_mod = get_main_mod($vfspath, $mods);
my $layer = load_xml($vfspath, get_file($vfspath, $main_mod));
if ($layer->{Entity}{'@parent'}) {
my $parent = load_inherited($layer->{Entity}{'@parent'}{' content'}, $mods, $base);
apply_layer($parent->{Entity}, $layer->{Entity});
return $parent;
} else {
if (not $base) {
return $layer;
}
else {
apply_layer($base->{Entity}, $layer->{Entity});
return $base
}
}
}
sub find_entities
{
my ($modName) = @_;
my @files;
my $find_process = sub {
return $File::Find::prune = 1 if $_ eq '.svn';
my $n = $File::Find::name;
return if /~$/;
return unless -f $_;
$n =~ s~\Q$vfsroot\E/$modName/simulation/templates/~~;
$n =~ s/\.xml$//;
push @files, $n;
};
find({ wanted => $find_process }, "$vfsroot/$modName/simulation/templates");
return @files;
}

View File

@ -1,715 +0,0 @@
use strict;
use warnings;
use Data::Dumper;
use File::Find;
use XML::Simple;
use JSON;
use Getopt::Long qw(GetOptions);
use lib ".";
use Entity;
GetOptions (
'--check-unused' => \(my $checkUnused = 0),
'--check-map-xml' => \(my $checkMapXml = 0),
'--validate-templates' => \(my $validateTemplates = 0),
'--mod-to-check=s' => \(my $modToCheck = "public")
);
my @files;
my @roots;
my @deps;
# Force and checkMapXml if checkUnused is enabled to avoid false positives.
$checkMapXml |= $checkUnused;
my $vfsroot = '../../../binaries/data/mods';
my $supportedTextureFormats = 'dds|png';
my $mods = get_mod_dependencies_string($modToCheck);
my $mod_list_string = $modToCheck;
if ($mods ne "")
{
$mod_list_string = $mod_list_string."|$mods";
}
$mod_list_string = $mod_list_string."|mod";
print("Checking $modToCheck\'s integrity. \n");
print("The following mod(s) will be loaded: $mod_list_string. \n");
my @mods_list = split(/\|/, "$mod_list_string");
sub get_mod_dependencies
{
my ($mod) = @_;
my $modjson = parse_json_file_full_path("$vfsroot/$mod/mod.json");
my $modjsondeps = $modjson->{'dependencies'};
for my $dep (@{$modjsondeps})
{
# 0ad's folder isn't named like the mod.
if(index($dep, "0ad") != -1)
{
$dep = "public";
}
}
return $modjsondeps;
}
sub get_mod_dependencies_string
{
my ($mod) = @_;
return join( '|',@{get_mod_dependencies($mod)});
}
sub vfs_to_physical
{
my ($vfsPath) = @_;
my $fn = vfs_to_relative_to_mods($vfsPath);
return "$vfsroot/$fn";
}
sub vfs_to_relative_to_mods
{
my ($vfsPath) = @_;
for my $dep (@mods_list)
{
my $fn = "$dep/$vfsPath";
if (-e "$vfsroot/$fn")
{
return $fn;
}
}
}
sub find_files
{
my ($vfsPath, $extn) = @_;
my @files;
my $find_process = sub {
return $File::Find::prune = 1 if $_ eq '.svn';
my $n = $File::Find::name;
return if /~$/;
return unless -f $_;
return unless /\.($extn)$/;
$n =~ s~\Q$vfsroot\E/($mod_list_string)/~~;
push @files, $n;
};
for my $dep (@mods_list)
{
find({ wanted => $find_process },"$vfsroot/$dep/$vfsPath") if -d "$vfsroot/$dep/$vfsPath";
}
return @files;
}
sub parse_json_file_full_path
{
my ($vfspath) = @_;
open my $fh, $vfspath or die "Failed to open '$vfspath': $!";
# decode_json expects a UTF-8 string and doesn't handle BOMs, so we strip those
# (see http://trac.wildfiregames.com/ticket/1556)
return decode_json(do { local $/; my $file = <$fh>; $file =~ s/^\xEF\xBB\xBF//; $file });
}
sub parse_json_file
{
my ($vfspath) = @_;
return parse_json_file_full_path(vfs_to_physical($vfspath))
}
sub add_entities
{
print "Loading entities...\n";
my @entfiles = find_files('simulation/templates', 'xml');
s~^simulation/templates/(.*)\.xml$~$1~ for @entfiles;
for my $f (sort @entfiles)
{
my $path = "simulation/templates/$f.xml";
push @files, $path;
my $ent = Entity::load_inherited($f, "$mod_list_string");
if ($ent->{Entity}{'@parent'})
{
my @parents = split(/\|/, $ent->{Entity}{'@parent'}{' content'});
for my $parentPath (@parents)
{
push @deps, [ $path, "simulation/templates/" . $parentPath . ".xml" ];
}
}
if ($f !~ /^template_/)
{
push @roots, $path;
if ($ent->{Entity}{VisualActor} and $ent->{Entity}{VisualActor}{Actor})
{
my $phenotypes = $ent->{Entity}{Identity}{Phenotype}{' content'} || "default";
my @phenotypes = split /\s/,$phenotypes;
for my $phenotype (@phenotypes)
{
# See simulation2/components/CCmpVisualActor.cpp and Identity.js for explanation.
my $actorPath = $ent->{Entity}{VisualActor}{Actor}{' content'};
$actorPath =~ s/{phenotype}/$phenotype/g;
push @deps, [ $path, "art/actors/" . $actorPath ];
}
push @deps, [ $path, "art/actors/" . $ent->{Entity}{VisualActor}{FoundationActor}{' content'} ] if $ent->{Entity}{VisualActor}{FoundationActor};
}
if ($ent->{Entity}{Sound})
{
my $phenotypes = $ent->{Entity}{Identity}{Phenotype}{' content'} || "default";
my $lang = $ent->{Entity}{Identity}{Lang}{' content'} || "greek";
my @phenotypes = split /\s/,$phenotypes;
for my $phenotype (@phenotypes)
{
for (grep ref($_), values %{$ent->{Entity}{Sound}{SoundGroups}})
{
# see simulation/components/Sound.js and Identity.js for explanation
my $soundPath = $_->{' content'};
$soundPath =~ s/{phenotype}/$phenotype/g;
$soundPath =~ s/{lang}/$lang/g;
push @deps, [ $path, "audio/" . $soundPath ];
}
}
}
if ($ent->{Entity}{Identity})
{
push @deps, [ $path, "art/textures/ui/session/portraits/" . $ent->{Entity}{Identity}{Icon}{' content'} ] if $ent->{Entity}{Identity}{Icon} and $ent->{Entity}{Identity}{Icon}{' content'} ne '';
}
if ($ent->{Entity}{Heal} and $ent->{Entity}{Heal}{RangeOverlay})
{
push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Heal}{RangeOverlay}{LineTexture}{' content'} ] if $ent->{Entity}{Heal}{RangeOverlay}{LineTexture} and $ent->{Entity}{Heal}{RangeOverlay}{LineTexture}{' content'} ne '';
push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Heal}{RangeOverlay}{LineTextureMask}{' content'} ] if $ent->{Entity}{Heal}{RangeOverlay}{LineTextureMask} and $ent->{Entity}{Heal}{RangeOverlay}{LineTextureMask}{' content'} ne '';
}
if ($ent->{Entity}{Selectable} and $ent->{Entity}{Selectable}{Overlay} and $ent->{Entity}{Selectable}{Overlay}{Texture})
{
push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTexture}{' content'} ] if $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTexture} and $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTexture}{' content'} ne '';
push @deps, [ $path, "art/textures/selection/" . $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTextureMask}{' content'} ] if $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTextureMask} and $ent->{Entity}{Selectable}{Overlay}{Texture}{MainTextureMask}{' content'} ne '';
}
if ($ent->{Entity}{Formation})
{
push @deps, [ $path, "art/textures/ui/session/icons/" . $ent->{Entity}{Formation}{Icon}{' content'} ] if $ent->{Entity}{Formation}{Icon} and $ent->{Entity}{Formation}{Icon}{' content'} ne '';
}
}
}
}
sub push_variant_dependencies
{
my ($variant, $f) = @_;
push @deps, [ $f, "art/variants/$variant->{file}" ] if $variant->{file};
push @deps, [ $f, "art/meshes/$variant->{mesh}" ] if $variant->{mesh};
push @deps, [ $f, "art/particles/$variant->{particles}{file}" ] if $variant->{particles}{file};
for my $tex (@{$variant->{textures}{texture}})
{
push @deps, [ $f, "art/textures/skins/$tex->{file}" ] if $tex->{file};
}
for my $prop (@{$variant->{props}{prop}})
{
push @deps, [ $f, "art/actors/$prop->{actor}" ] if $prop->{actor};
}
for my $anim (@{$variant->{animations}{animation}})
{
push @deps, [ $f, "art/animation/$anim->{file}" ] if $anim->{file};
}
}
sub add_actors
{
print "Loading actors...\n";
my @actorfiles = find_files('art/actors', 'xml');
for my $f (sort @actorfiles)
{
push @files, $f;
push @roots, $f;
my $actor = XMLin(vfs_to_physical($f), ForceArray => [qw(group variant texture prop animation)], KeyAttr => []) or die "Failed to parse '$f': $!";
for my $group (@{$actor->{group}})
{
for my $variant (@{$group->{variant}})
{
push_variant_dependencies($variant, $f);
}
}
push @deps, [ $f, "art/materials/$actor->{material}" ] if $actor->{material};
}
}
sub add_variants
{
print "Loading variants...\n";
my @variantfiles = find_files('art/variants', 'xml');
for my $f (sort @variantfiles)
{
push @files, $f;
push @roots, $f;
my $variant = XMLin(vfs_to_physical($f), ForceArray => [qw(texture prop animation)], KeyAttr => []) or die "Failed to parse '$f': $!";
push_variant_dependencies($variant, $f);
}
}
sub add_art
{
print "Loading art files...\n";
push @files, find_files('art/textures/particles', $supportedTextureFormats);
push @files, find_files('art/textures/terrain', $supportedTextureFormats);
push @files, find_files('art/textures/skins', $supportedTextureFormats);
push @files, find_files('art/meshes', 'pmd|dae');
push @files, find_files('art/animation', 'psa|dae');
}
sub add_materials
{
print "Loading materials...\n";
my @materialfiles = find_files('art/materials', 'xml');
for my $f (sort @materialfiles)
{
push @files, $f;
my $material = XMLin(vfs_to_physical($f), ForceArray => [qw(alternative)], KeyAttr => []);
for my $alternative (@{$material->{alternative}})
{
push @deps, [ $f, "art/materials/$alternative->{material}" ] if $alternative->{material};
}
}
}
sub add_particles
{
print "Loading particles...\n";
my @particlefiles = find_files('art/particles', 'xml');
for my $f (sort @particlefiles)
{
push @files, $f;
my $particle = XMLin(vfs_to_physical($f));
push @deps, [ $f, "$particle->{texture}" ] if $particle->{texture};
}
}
sub add_maps_xml
{
print "Loading maps XML...\n";
my @mapfiles = find_files('maps/scenarios', 'xml');
push @mapfiles, find_files('maps/skirmishes', 'xml');
push @mapfiles, find_files('maps/tutorials', 'xml');
for my $f (sort @mapfiles)
{
push @files, $f;
push @roots, $f;
my $map = XMLin(vfs_to_physical($f), ForceArray => [qw(Entity)], KeyAttr => []) or die "Failed to parse '$f': $!";
my %used;
for my $entity (@{$map->{Entities}{Entity}})
{
$used{$entity->{Template}} = 1;
}
for my $template (keys %used)
{
if ($template =~ /^actor\|(.*)$/)
{
# Handle special 'actor|' case
push @deps, [ $f, "art/actors/$1" ];
}
else
{
if ($template =~ /^resource\|(.*)$/)
{
# Handle special 'resource|' case
$template = $1;
}
push @deps, [ $f, "simulation/templates/$template.xml" ];
}
}
# Map previews
my $settings = decode_json($map->{ScriptSettings});
push @deps, [ $f, "art/textures/ui/session/icons/mappreview/" . $settings->{Preview} ] if $settings->{Preview};
}
}
sub add_maps_pmp
{
print "Loading maps PMP...\n";
# Need to generate terrain texture filename=>path lookup first
my %terrains;
for my $f (find_files('art/terrains', 'xml'))
{
$f =~ /([^\/]+)\.xml/ or die;
# ignore terrains.xml
if ($f !~ /terrains.xml$/)
{
warn "Duplicate terrain name '$1' (from '$terrains{$1}' and '$f')\n" if $terrains{$1};
$terrains{$1} = $f;
}
}
my @mapfiles = find_files('maps/scenarios', 'pmp');
push @mapfiles, find_files('maps/skirmishes', 'pmp');
for my $f (sort @mapfiles)
{
push @files, $f;
push @roots, $f;
open my $fh, vfs_to_physical($f) or die "Failed to open '$f': $!";
binmode $fh;
my $buf;
read $fh, $buf, 4;
die "Invalid PMP header ($buf) in '$f'" unless $buf eq "PSMP";
read $fh, $buf, 4;
my $version = unpack 'V', $buf;
die "Invalid PMP version ($version) in '$f'" unless $version == 7;
read $fh, $buf, 4;
my $datasize = unpack 'V', $buf;
read $fh, $buf, 4;
my $mapsize = unpack 'V', $buf;
seek $fh, 2 * ($mapsize*16+1)*($mapsize*16+1), 1; # heightmap
read $fh, $buf, 4;
my $numtexs = unpack 'V', $buf;
for (0..$numtexs-1)
{
read $fh, $buf, 4;
my $len = unpack 'V', $buf;
my $str;
read $fh, $str, $len;
push @deps, [ $f, $terrains{$str} || "art/terrains/(unknown)/$str" ];
}
# ignore patches data
}
}
sub add_soundgroups
{
print "Loading sound groups...\n";
my @soundfiles = find_files('audio', 'xml');
for my $f (sort @soundfiles)
{
push @files, $f;
push @roots, $f;
my $sound = XMLin(vfs_to_physical($f), ForceArray => [qw(Sound)], KeyAttr => []) or die "Failed to parse '$f': $!";
my $path = $sound->{Path};
$path =~ s/\/$//; # strip optional trailing slash
for (@{$sound->{Sound}})
{
push @deps, [$f, "$path/$_" ];
}
}
}
sub add_audio
{
print "Loading audio files...\n";
push @files, find_files('audio', 'ogg');
}
sub add_gui_xml
{
print "Loading GUI XML...\n";
my @guifiles = find_files('gui', 'xml');
for my $f (sort @guifiles)
{
push @files, $f;
# GUI page definitions are assumed to be named page[_something].xml and alone in that.
if ($f =~ /\/page(_[^.\/]+)?\.xml$/)
{
push @roots, $f;
my $xml = XMLin(vfs_to_physical($f), ForceArray => [qw(include)], KeyAttr => []) or die "Failed to parse '$f': $!";
for my $include (@{$xml->{include}})
{
# If including an entire directory, find all the *.xml files
if ($include =~ /\/$/)
{
push @deps, [ $f, $_ ] for find_files("gui/$include", 'xml');
}
else
{
push @deps, [ $f, "gui/$include" ];
}
}
}
else
{
my $xml = XMLin(vfs_to_physical($f), ForceArray => [qw(object script action sprite image)], KeyAttr => [], KeepRoot => 1) or die "Failed to parse '$f': $!";
my $name = (keys %$xml)[0];
if ($name eq 'objects' or $name eq 'object')
{
for (grep ref $_ , @{$xml->{objects}{script}})
{
push @deps, [ $f, $_->{file} ] if $_->{file};
if ($_->{directory})
{
# If including an entire directory, find all the *.js files
push @deps, [ $f, $_ ] for find_files($_->{directory}, 'js')
}
}
my $add_objects;
$add_objects = sub
{
my ($parent) = @_;
for my $obj (@{$parent->{object}})
{
# TODO: look at sprites, styles, etc
$add_objects->($obj);
}
};
$add_objects->($xml->{objects});
}
elsif ($name eq 'setup')
{
# TODO: look at sprites, styles, etc
}
elsif ($name eq 'styles')
{
# TODO: look at sprites, styles, etc
}
elsif ($name eq 'sprites')
{
for my $sprite (@{$xml->{sprites}{sprite}})
{
for my $image (@{$sprite->{image}})
{
push @deps, [ $f, "art/textures/ui/$image->{texture}" ] if $image->{texture};
}
}
}
else
{
print "Unexpected GUI XML root element '$name':\n" . Dumper $xml;
exit;
}
}
}
}
sub add_gui_data
{
print "Loading GUI data...\n";
push @files, find_files('gui', 'js');
push @files, find_files('art/textures/ui', $supportedTextureFormats);
push @files, find_files('art/textures/selection', $supportedTextureFormats);
}
sub add_civs
{
print "Loading civs...\n";
my @civfiles = find_files('simulation/data/civs', 'json');
for my $f (sort @civfiles)
{
push @files, $f;
push @roots, $f;
my $civ = parse_json_file($f);
push @deps, [ $f, "audio/music/" . $_->{File} ] for @{$civ->{Music}};
}
}
sub add_rms
{
print "Loading random maps...\n";
push @files, find_files('maps/random', 'js');
my @rmsdefs = find_files('maps/random', 'json');
for my $f (sort @rmsdefs)
{
next if $f =~ /^maps\/random\/rmbiome/;
push @files, $f;
push @roots, $f;
my $rms = parse_json_file($f);
push @deps, [ $f, "maps/random/" . $rms->{settings}{Script} ] if $rms->{settings}{Script};
# Map previews
push @deps, [ $f, "art/textures/ui/session/icons/mappreview/" . $rms->{settings}{Preview} ] if $rms->{settings}{Preview};
}
}
sub add_techs
{
print "Loading techs...\n";
my @techfiles = find_files('simulation/data/technologies', 'json');
for my $f (sort @techfiles)
{
push @files, $f;
push @roots, $f;
my $tech = parse_json_file($f);
push @deps, [ $f, "art/textures/ui/session/portraits/technologies/" . $tech->{icon} ] if $tech->{icon};
push @deps, [ $f, "simulation/data/technologies/" . $tech->{supersedes} . ".json" ] if $tech->{supersedes};
}
}
sub add_auras
{
print "Loading auras...\n";
my @aurafiles = find_files('simulation/data/auras', 'json');
for my $f (sort @aurafiles)
{
push @files, $f;
push @roots, $f;
my $aura = parse_json_file($f);
push @deps, [ $f, $aura->{overlayIcon} ] if $aura->{overlayIcon};
if($aura->{rangeOverlay})
{
push @deps, [ $f, "art/textures/selection/" . $aura->{rangeOverlay}{lineTexture} ] if $aura->{rangeOverlay}{lineTexture};
push @deps, [ $f, "art/textures/selection/" . $aura->{rangeOverlay}{lineTextureMask} ] if $aura->{rangeOverlay}{lineTextureMask};
}
}
}
sub add_terrains
{
print "Loading terrains...\n";
my @terrains = find_files('art/terrains', 'xml');
for my $f (sort @terrains)
{
# ignore terrains.xml
if ($f !~ /terrains.xml$/)
{
push @files, $f;
push @roots, $f;
my $terrain = XMLin(vfs_to_physical($f), ForceArray => [qw(texture)], KeyAttr => []) or die "Failed to parse '$f': $!";
for my $texture (@{$terrain->{textures}{texture}})
{
push @deps, [ $f, "art/textures/terrain/$texture->{file}" ] if $texture->{file};
}
push @deps, [ $f, "art/materials/$terrain->{material}" ] if $terrain->{material};
}
}
}
sub check_deps
{
my %files;
@files{@files} = ();
my %lcfiles;
@lcfiles{map lc($_), @files} = @files;
my %revdeps;
for my $d (@deps)
{
push @{$revdeps{$d->[1]}}, $d->[0];
}
for my $f (sort keys %revdeps)
{
if ($f =~ /simulation\/templates\//)
{
next if exists $files{$f =~ s/templates\//templates\/special\/filter\//r};
next if exists $files{$f =~ s/templates\//templates\/mixins\//r};
}
next if exists $files{$f};
warn "Missing file '$f' referenced by: " . (join ', ', map "'$_'", map vfs_to_relative_to_mods($_), sort @{$revdeps{$f}}) . "\n";
if (exists $lcfiles{lc $f})
{
warn "### Case-insensitive match (found '$lcfiles{lc $f}')\n";
}
}
}
sub check_unused
{
my %reachable;
@reachable{@roots} = ();
my %deps;
for my $d (@deps)
{
push @{$deps{$d->[0]}}, $d->[1];
}
while (1)
{
my @newreachable;
for my $r (keys %reachable)
{
push @newreachable, grep { not exists $reachable{$_} } @{$deps{$r}};
}
last if @newreachable == 0;
@reachable{@newreachable} = ();
}
for my $f (sort @files)
{
next if exists $reachable{$f}
|| index($f, "art/terrains/") != -1
|| index($f, "maps/random/") != -1
|| index($f, "art/materials/") != -1;
warn "Unused file '" . vfs_to_relative_to_mods($f) . "'\n";
}
}
add_maps_xml() if $checkMapXml;
add_maps_pmp();
add_entities();
add_actors();
add_variants();
add_art();
add_materials();
add_particles();
add_soundgroups();
add_audio();
add_gui_xml();
add_gui_data();
add_civs();
add_rms();
add_techs();
add_terrains();
add_auras();
check_deps();
check_unused() if $checkUnused;
print "\n" if $checkUnused;
system("perl ../xmlvalidator/validate.pl") if $validateTemplates;

View File

@ -0,0 +1,605 @@
#!/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 = '<L' # little endian long int
int_len = calcsize(int_fmt)
version, = unpack(int_fmt, f.read(int_len))
if version != 7:
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):
self.logger.info("Loading entities...")
simul_templates_path = Path('simulation/templates')
# TODO: We might want to get computed templates through the RL interface instead of computing the values ourselves.
simul_template_entity = SimulTemplateEntity(self.vfs_root, self.logger)
for (fp, _) 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'):
for parent in entity.get('parent').split('|'):
self.deps.append((str(fp), str(simul_templates_path / (parent + '.xml'))))
if not str(fp).startswith('template_'):
self.roots.append(str(fp))
if entity and entity.find('VisualActor') is not None and entity.find('VisualActor').find('Actor') is not None:
if entity.find('Identity'):
phenotype_tag = entity.find('Identity').find('Phenotype')
phenotypes = split(r'\s', phenotype_tag.text if phenotype_tag is not None 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}'))
foundation_actor = entity.find('VisualActor').find('FoundationActor')
if foundation_actor:
self.deps.append((str(fp), f'art/actors/{foundation_actor.text}'))
if entity.find('Sound'):
phenotype_tag = entity.find('Identity').find('Phenotype')
phenotypes = split(r'\s', phenotype_tag.text if phenotype_tag is not None and phenotype_tag.text else 'default')
lang_tag = entity.find('Identity').find('Lang')
lang = lang_tag.text if lang_tag is not None 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') is not None:
icon = entity.find('Identity').find('Icon')
if icon is not None and icon.text:
self.deps.append((str(fp), f'art/textures/ui/session/portraits/{icon.text}'))
if entity.find('Heal') is not None and entity.find('Heal').find('RangeOverlay') is not None:
range_overlay = entity.find('Heal').find('RangeOverlay')
for tag in ('LineTexture', 'LineTextureMask'):
elem = range_overlay.find(tag)
if elem is not None and elem.text:
self.deps.append((str(fp), f'art/textures/selection/{elem.text}'))
if entity.find('Selectable') is not None and entity.find('Selectable').find('Overlay') is not None \
and entity.find('Selectable').find('Overlay').find('Texture') is not None:
texture = entity.find('Selectable').find('Overlay').find('Texture')
for tag in ('MainTexture', 'MainTextureMask'):
elem = texture.find(tag)
if elem is not None and elem.text:
self.deps.append((str(fp), f'art/textures/selection/{elem.text}'))
if entity.find('Formation') is not None:
icon = entity.find('Formation').find('Icon')
if icon is not None 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') is not None else []
prop_actors = [prop.get('actor') for prop in variant.find('props').findall('prop')] \
if variant.find('props') is not None else []
animation_files = [anim.get('file') for anim in variant.find('animations').findall('animation')] \
if variant.find('animations') is not None else []
if variant_file:
self.deps.append((str(fp), f'art/variants/{variant_file}'))
if mesh is not None and mesh.text:
self.deps.append((str(fp), f'art/meshes/{mesh.text}'))
if particles is not None 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 append_actor_dependencies(self, actor, fp):
for group in actor.findall('group'):
for variant in group.findall('variant'):
self.append_variant_dependencies(variant, fp)
material = actor.find('material')
if material is not None and material.text:
self.deps.append((str(fp), f'art/materials/{material.text}'))
def add_actors(self):
self.logger.info("Loading actors...")
for (fp, ffp) in sorted(self.find_files('art/actors', 'xml')):
self.files.append(str(fp))
self.roots.append(str(fp))
root = ElementTree.parse(ffp).getroot()
if root.tag == 'actor':
self.append_actor_dependencies(root, fp)
# model has lods
elif root.tag == 'qualitylevels':
qualitylevels = root
for actor in qualitylevels.findall('actor'):
self.append_actor_dependencies(actor, fp)
for actor in qualitylevels.findall('inline'):
self.append_actor_dependencies(actor, fp)
def add_variants(self):
self.logger.info("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):
self.logger.info("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):
self.logger.info("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):
self.logger.info("Loading particles...")
for (fp, ffp) in sorted(self.find_files('art/particles', 'xml')):
self.files.append(str(fp))
self.roots.append(str(fp))
particle = ElementTree.parse(ffp).getroot()
texture = particle.find('texture')
if texture:
self.deps.append((str(fp), texture.text))
def add_soundgroups(self):
self.logger.info("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):
self.logger.info("Loading audio files...")
self.files.extend([str(fp) for (fp, ffp) in self.find_files('audio/', self.supportedAudioFormats)])
def add_gui_object_repeat(self, obj, fp):
for repeat in obj.findall('repeat'):
for sub_obj in repeat.findall('object'):
# TODO: look at sprites, styles, etc
self.add_gui_object_include(sub_obj, fp)
for sub_obj in repeat.findall('objects'):
# TODO: look at sprites, styles, etc
self.add_gui_object_include(sub_obj, fp)
self.add_gui_object_include(repeat, fp)
def add_gui_object_include(self, obj, fp):
for include in obj.findall('include'):
included_file = include.get('file')
if included_file:
self.deps.append((str(fp), f'{included_file}'))
def add_gui_object(self, parent, fp):
if parent is None:
return
for obj in parent.findall('object'):
# TODO: look at sprites, styles, etc
self.add_gui_object_repeat(obj, fp)
self.add_gui_object_include(obj, fp)
self.add_gui_object(obj, fp)
for obj in parent.findall('objects'):
# TODO: look at sprites, styles, etc
self.add_gui_object_repeat(obj, fp)
self.add_gui_object_include(obj, fp)
self.add_gui_object(obj, fp)
def add_gui_xml(self):
self.logger.info("Loading GUI XML...")
for (fp, ffp) in sorted(self.find_files('gui', 'xml')):
self.files.append(str(fp))
# GUI page definitions are assumed to be named page_[something].xml and alone in that.
if match(r".*[\\\/]page(_[^.\/\\]+)?\.xml$", str(fp)):
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
self.roots.append(str(fp))
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')])
self.add_gui_object(root_xml, fp)
elif name == 'setup':
# TODO: look at sprites, styles, etc
pass
elif name == 'styles':
for style in root_xml.findall('style'):
if(style.get('sound_opened')):
self.deps.append((str(fp), f"{style.get('sound_opened')}"))
if(style.get('sound_closed')):
self.deps.append((str(fp), f"{style.get('sound_closed')}"))
if(style.get('sound_selected')):
self.deps.append((str(fp), f"{style.get('sound_selected')}"))
if(style.get('sound_disabled')):
self.deps.append((str(fp), f"{style.get('sound_disabled')}"))
# 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):
self.logger.info("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):
self.logger.info("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, encoding='utf-8') as f:
civ = load(f)
for music in civ.get('Music', []):
self.deps.append((str(fp), f"audio/music/{music['File']}"))
def add_tips(self):
self.logger.info("Loading tips...")
for (fp, ffp) in sorted(self.find_files('gui/text/tips', 'txt')):
relative_path = str(fp)
self.files.append(relative_path)
self.roots.append(relative_path)
self.deps.append((relative_path, f"art/textures/ui/loading/tips/{basename(relative_path).split('.')[0]}.png"))
def add_rms(self):
self.logger.info("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, encoding='utf-8') 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):
self.logger.info("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, encoding='utf-8') 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):
self.logger.info("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') is not None:
material = terrain.find('material').text
self.deps.append((str(fp), f"art/materials/{material}"))
def add_auras(self):
self.logger.info("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, encoding='utf-8') 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):
self.logger.info("Looking for missing files...")
uniq_files = set(self.files)
uniq_files = [r.replace(sep, '/') for r in uniq_files]
lower_case_files = {f.lower(): f for f in uniq_files}
reverse_deps = dict()
for parent, dep in self.deps:
if sep != '/':
parent = parent.replace(sep, '/')
dep = dep.replace(sep, '/')
if dep not in reverse_deps:
reverse_deps[dep] = {parent}
else:
reverse_deps[dep].add(parent)
for dep in sorted(reverse_deps.keys()):
if "simulation/templates" in dep and (
dep.replace("templates/", "template/special/filter/") in uniq_files or
dep.replace("templates/", "template/mixins/") in uniq_files
):
continue
if dep in uniq_files:
continue
callers = [str(self.vfs_to_relative_to_mods(ref)) for ref in reverse_deps[dep]]
self.logger.warning(f"Missing file '{dep}' referenced by: {', '.join(sorted(callers))}")
if dep.lower() in lower_case_files:
self.logger.warning(f"### Case-insensitive match (found '{lower_case_files[dep.lower()]}')")
def check_unused(self):
self.logger.info("Looking for unused files...")
deps = dict()
for parent, dep in self.deps:
if sep != '/':
parent = parent.replace(sep, '/')
dep = dep.replace(sep, '/')
if parent not in deps:
deps[parent] = {dep}
else:
deps[parent].add(dep)
uniq_files = set(self.files)
uniq_files = [r.replace(sep, '/') for r in uniq_files]
reachable = list(set(self.roots))
reachable = [r.replace(sep, '/') for r in reachable]
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(uniq_files):
if any((
f in reachable,
'art/terrains/' in f,
'maps/random/' in f,
)):
continue
self.logger.warning(f"Unused file '{str(self.vfs_to_relative_to_mods(f))}'")
if __name__ == '__main__':
check_ref = CheckRefs()
check_ref.main()

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python3
from os import chdir
from pathlib import Path
from re import split
from subprocess import run
from sys import exit
from scriptlib import warn, SimulTemplateEntity, find_files
def find_entities(vfs_root):
base = vfs_root / 'public' / 'simulation' / 'templates'
return [str(fp.relative_to(base).with_suffix('')) for (_, fp) in find_files(vfs_root, ['public'], 'simulation/templates', 'xml')]
def main():
vfs_root = Path(__file__).resolve().parents[3] / 'binaries' / 'data' / 'mods'
simul_templates_path = Path('simulation/templates')
simul_template_entity = SimulTemplateEntity(vfs_root)
with open('creation.dot', 'w') as dot_f:
dot_f.write('digraph G {\n')
files = sorted(find_entities(vfs_root))
for f in files:
if f.startswith('template_'):
continue
print(f"# {f}...")
entity = simul_template_entity.load_inherited(simul_templates_path, f, ['public'])
if entity.find('Builder') is not None and entity.find('Builder').find('Entities') is not None:
entities = entity.find('Builder').find('Entities').text.replace('{civ}', entity.find('Identity').find('Civ').text)
builders = split(r'\s+', entities.strip())
for builder in builders:
if Path(builder) in files:
warn(f"Invalid Builder reference: {f} -> {builder}")
dot_f.write(f'"{f}" -> "{builder}" [color=green];\n')
if entity.find('TrainingQueue') is not None and entity.find('TrainingQueue').find('Entities') is not None:
entities = entity.find('TrainingQueue').find('Entities').text.replace('{civ}', entity.find('Identity').find('Civ').text)
training_queues = split(r'\s+', entities.strip())
for training_queue in training_queues:
if Path(training_queue) in files:
warn(f"Invalid TrainingQueue reference: {f} -> {training_queue}")
dot_f.write(f'"{f}" -> "{training_queue}" [color=blue];\n')
dot_f.write('}\n')
if run(['dot', '-V'], capture_output=True).returncode == 0:
exit(run(['dot', '-Tpng', 'creation.dot', '-o', 'creation.png'], text=True).returncode)
if __name__ == '__main__':
chdir(Path(__file__).resolve().parent)
main()

View File

@ -0,0 +1,44 @@
#!/usr/bin/env python3
from os import chdir
from pathlib import Path
from subprocess import run, CalledProcessError
from sys import exit
from xml.etree import ElementTree
from scriptlib import warn, SimulTemplateEntity, find_files
def main():
root = Path(__file__).resolve().parents[3]
relaxng_schema = root / 'binaries' / 'system' / 'entity.rng'
if not relaxng_schema.exists():
warn(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'
simul_templates_path = Path('simulation/templates')
simul_template_entity = SimulTemplateEntity(vfs_root)
count = 0
failed = 0
for fp, _ in sorted(find_files(vfs_root, ['public'], 'simulation/templates', 'xml')):
if fp.stem.startswith('template_'):
continue
print(f"# {fp}...")
count += 1
entity = simul_template_entity.load_inherited(simul_templates_path, str(fp.relative_to(simul_templates_path)), ['public'])
xmlcontent = ElementTree.tostring(entity, encoding='unicode')
try:
run(['xmllint', '--relaxng', str(relaxng_schema.resolve()), '-'], input=xmlcontent, capture_output=True, text=True, check=True)
except CalledProcessError as e:
failed += 1
print(e.stderr)
print(e.stdout)
print(f"\nTotal: {count}; failed: {failed}")
if __name__ == '__main__':
chdir(Path(__file__).resolve().parent)
main()

View File

@ -1,4 +1,4 @@
# Checkrefs.pl
# Checkrefs.py
## Description
@ -6,24 +6,27 @@ This script checks the game files for missing dependencies, unused files, and fo
## Requirements
- Perl interpreter installed
- Dependencies:
- XML::Parser
- XML::Simple
- Getopt::Long
- File::Find
- Data::Dumper
- JSON
- Python 3.6+ interpreter installed
- lxml for the -a option.
## Usage
- cd in source/tools/entity and run the script.
- cd in `source/tools/entity` and run the script.
```
Usage: perl checkrefs.pl [OPTION]...
usage: checkrefs.py [-h] [-u] [-x] [-a] [-t] [-m MOD [MOD ...]]
Checks the game files for missing dependencies, unused files, and for file integrity.
--check-unused check for all the unused files in the given mods and their dependencies. Implies --check-map-xml. Currently yields a lot of false positives.
--check-map-xml check maps for missing actor and templates.
--validate-templates run the validate.pl script to check if the xml files match their (.rng) grammar file. This currently only works for the public mod.
--mod-to-check=mods specify which mods to check. 'mods' should be a list of mods separated by '|'. Default value: 'public|mod'.
options:
-h, --help show this help message and exit
-u, --check-unused check for all the unused files in the given mods and their dependencies. Implies --check-map-
xml. Currently yields a lot of false positives.
-x, --check-map-xml check maps for missing actor and templates.
-a, --validate-actors
run the validator.py script to check if the actors files have extra or missing textures.
-t, --validate-templates
run the validator.py script to check if the xml files match their (.rng) grammar file.
-m MOD [MOD ...], --mods MOD [MOD ...]
specify which mods to check. Default to public.
```

View File

@ -0,0 +1,128 @@
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
self.logger = logger
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')
if not exists(file):
file = (default_path / "mixins" / vfs_path).with_suffix('.xml')
if not exists(file):
file = (default_path / vfs_path).with_suffix('.xml')
return file
def get_main_mod(self, base_path, vfs_path, mods):
for mod in mods:
fp = self.get_file(base_path, 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):
"""
apply tag layer to base_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:
final_tokens.remove(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':
base_tag.text = str(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 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:
if 'replace' in child.attrib and base_child is not None:
base_tag.remove(base_child)
if base_child is None:
base_child = ElementTree.Element(child.tag)
base_tag.append(base_child)
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, 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);
return base
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:
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)
self.apply_layer(parent, layer)
return parent
else:
if not base:
return layer
else:
self.apply_layer(base, layer)
return base
def find_files(vfs_root, mods, 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 dp.name != '.git' 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 mods for (rp, fp) in find_recursive(vfs_root / mod / vfs_path, vfs_root / mod)]

View File

@ -4,8 +4,20 @@ from pathlib import Path
from os.path import sep, join, realpath, exists, basename, dirname
from json import load, loads
from re import split, match
from logging import getLogger, StreamHandler, INFO, Formatter
from logging import getLogger, StreamHandler, INFO, WARNING, Filter, Formatter
import lxml.etree
import sys
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 VFS_File:
def __init__(self, mod_name, vfs_path):
@ -24,11 +36,16 @@ class RelaxNGValidator:
logger = getLogger(__name__)
logger.setLevel(INFO)
# create a console handler, seems nicer to Windows and for future uses
ch = StreamHandler()
ch = StreamHandler(sys.stdout)
ch.setLevel(INFO)
ch.setFormatter(Formatter('%(levelname)s - %(message)s'))
# ch.setFormatter(Formatter('%(message)s')) # same output as perl
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 run (self):

View File

@ -1,10 +1,21 @@
#!/usr/bin/env python3
import argparse
import os
import sys
import re
import time
import xml.etree.ElementTree
from logging import getLogger, StreamHandler, INFO, Formatter
from logging import getLogger, StreamHandler, INFO, WARNING, 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 Actor:
def __init__(self, mod_name, vfs_path):
@ -19,7 +30,7 @@ class Actor:
try:
tree = xml.etree.ElementTree.parse(physical_path)
except xml.etree.ElementTree.ParseError as err:
self.logger.error('"%s": %s\n' % (physical_path, err.msg))
self.logger.error('"%s": %s' % (physical_path, err.msg))
return False
root = tree.getroot()
# Special case: particles don't need a diffuse texture.
@ -41,7 +52,7 @@ class Actor:
try:
tree = xml.etree.ElementTree.parse(physical_path)
except xml.etree.ElementTree.ParseError as err:
self.logger.error('"%s": %s\n' % (physical_path, err.msg))
self.logger.error('"%s": %s' % (physical_path, err.msg))
return False
root = tree.getroot()
@ -64,7 +75,7 @@ class Material:
try:
root = xml.etree.ElementTree.parse(physical_path).getroot()
except xml.etree.ElementTree.ParseError as err:
self.logger.error('"%s": %s\n' % (physical_path, err.msg))
self.logger.error('"%s": %s' % (physical_path, err.msg))
return False
for element in root.findall('.//required_texture'):
texture_name = element.get('name')
@ -88,10 +99,17 @@ class Validator:
def __init_logger(self):
logger = getLogger(__name__)
logger.setLevel(INFO)
ch = StreamHandler()
# 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 get_mod_path(self, mod_name, vfs_path):
@ -126,6 +144,7 @@ class Validator:
return result
def find_materials(self, vfs_path):
self.logger.info('Collecting materials...')
material_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml'))
for material_file in material_files:
material_name = os.path.basename(material_file['vfs_path'])
@ -138,6 +157,8 @@ class Validator:
self.invalid_materials[material_name] = material
def find_actors(self, vfs_path):
self.logger.info('Collecting actors...')
actor_files = self.find_all_mods_files(vfs_path, re.compile(r'.*\.xml'))
for actor_file in actor_files:
actor = Actor(actor_file['mod_name'], actor_file['vfs_path'])
@ -145,12 +166,9 @@ class Validator:
self.actors.append(actor)
def run(self):
start_time = time.time()
self.logger.info('Collecting list of files to check\n')
self.find_materials(os.path.join('art', 'materials'))
self.find_actors(os.path.join('art', 'actors'))
self.logger.info('Validating textures...')
for actor in self.actors:
if not actor.material:
@ -180,9 +198,6 @@ class Validator:
material.name
))
finish_time = time.time()
self.logger.info('Total execution time: %.3f seconds.\n' % (finish_time - start_time))
if __name__ == '__main__':
script_dir = os.path.dirname(os.path.realpath(__file__))
default_root = os.path.join(script_dir, '..', '..', '..', 'binaries', 'data', 'mods')