1 | #!@PERL@ |
1 | #!@PERL@ |
2 | |
2 | |
3 | # |
3 | # |
4 | # This file is part of Deliantra, the Roguelike Realtime MMORPG. |
4 | # This file is part of Deliantra, the Roguelike Realtime MMORPG. |
5 | # |
5 | # |
6 | # Copyright (©) 2007,2008,2009,2010 Marc Alexander Lehmann / Robin Redeker / the Deliantra team |
6 | # Copyright (©) 2007,2008,2009,2010,2011 Marc Alexander Lehmann / Robin Redeker / the Deliantra team |
7 | # |
7 | # |
8 | # Deliantra is free software: you can redistribute it and/or modify it under |
8 | # Deliantra is free software: you can redistribute it and/or modify it under |
9 | # the terms of the Affero GNU General Public License as published by the |
9 | # the terms of the Affero GNU General Public License as published by the |
10 | # Free Software Foundation, either version 3 of the License, or (at your |
10 | # Free Software Foundation, either version 3 of the License, or (at your |
11 | # option) any later version. |
11 | # option) any later version. |
… | |
… | |
28 | my $exec_prefix = "@exec_prefix@"; |
28 | my $exec_prefix = "@exec_prefix@"; |
29 | my $datarootdir = "@datarootdir@"; |
29 | my $datarootdir = "@datarootdir@"; |
30 | my $DATADIR = "@datadir@/@PACKAGE@"; |
30 | my $DATADIR = "@datadir@/@PACKAGE@"; |
31 | |
31 | |
32 | my $CONVERT = "@CONVERT@"; |
32 | my $CONVERT = "@CONVERT@"; |
33 | #my $IDENTIFY = "@IDENTIFY@"; |
33 | my $IDENTIFY = "@IDENTIFY@"; |
34 | my $OPTIPNG = "@OPTIPNG@"; |
34 | my $OPTIPNG = "@OPTIPNG@"; |
35 | my $RSYNC = "@RSYNC@"; |
35 | my $RSYNC = "@RSYNC@"; |
36 | my $PNGNQ = "@PNGNQ@"; |
36 | my $PNGNQ = "@PNGNQ@"; |
37 | |
37 | |
38 | use Getopt::Long; |
38 | use Getopt::Long; |
39 | use File::Temp; |
39 | use File::Temp; |
40 | use POSIX (); |
40 | use POSIX (); |
41 | use Carp; |
41 | use Carp; |
42 | |
42 | |
43 | use Coro::EV; |
|
|
44 | use AnyEvent; |
|
|
45 | use YAML::XS (); |
43 | use YAML::XS (); |
|
|
44 | use Digest::MD5 (); |
|
|
45 | use Storable (); |
46 | use JSON::XS (); |
46 | use JSON::XS (); |
47 | use IO::AIO (); |
47 | use IO::AIO (); |
48 | use Digest::MD5 (); |
48 | use Compress::LZF (); |
49 | |
49 | |
|
|
50 | use AnyEvent; |
50 | use Coro 5.12; |
51 | use Coro 5.12; |
|
|
52 | use Coro::EV; |
51 | use Coro::AIO; |
53 | use Coro::AIO; |
52 | use Coro::Util; |
54 | use Coro::Util; |
53 | use Coro::Channel; |
55 | use Coro::Channel; |
|
|
56 | use Coro::AnyEvent; |
54 | use Coro::Storable; $Storable::canonical = 1; |
57 | use Coro::Storable; $Storable::canonical = 1; |
55 | |
58 | |
56 | use Deliantra; |
59 | use Deliantra; |
57 | |
60 | |
58 | $SIG{QUIT} = sub { Carp::cluck "QUIT" }; |
61 | $SIG{QUIT} = sub { Carp::cluck "QUIT" }; |
… | |
… | |
113 | |
116 | |
114 | # here we could try to avoid collisions and reduce chksum size further |
117 | # here we could try to avoid collisions and reduce chksum size further |
115 | sub make_hash($\$\$;$) { |
118 | sub make_hash($\$\$;$) { |
116 | my ($id, $dataref, $hashref, $clen) = @_; |
119 | my ($id, $dataref, $hashref, $clen) = @_; |
117 | |
120 | |
118 | my $hash = substr +(Digest::MD5::md5 $$dataref), 0, $clen || 4; |
121 | my $hash = substr +(Digest::MD5::md5 $$dataref), 0, $clen || 5; |
119 | |
122 | |
120 | if (exists $hash{$hash}) { |
123 | if (exists $hash{$hash}) { |
121 | # hash collision, but some files are simply identical |
124 | # hash collision, but some files are simply identical |
122 | if (${$hash{$hash}[1]} ne $$dataref) { |
125 | if (${$hash{$hash}[1]} ne $$dataref) { |
123 | warn "hash collision $hash{$hash}[0] vs. $id\n"; |
126 | warn "hash collision $hash{$hash}[0] vs. $id\n"; |
… | |
… | |
129 | $hash{$hash} = [$id, $dataref, $hashref]; |
132 | $hash{$hash} = [$id, $dataref, $hashref]; |
130 | |
133 | |
131 | $$hashref = $hash; |
134 | $$hashref = $hash; |
132 | } |
135 | } |
133 | |
136 | |
|
|
137 | sub optipng($) { |
|
|
138 | system $OPTIPNG, "-i0", "-q", $_[0]; |
|
|
139 | die "$_[0] has zero size, aborting." unless -s $_[0]; |
|
|
140 | } |
|
|
141 | |
134 | mkdir $TMPDIR, 0700 |
142 | mkdir $TMPDIR, 0700 |
135 | or die "$TMPDIR: $!"; |
143 | or die "$TMPDIR: $!"; |
136 | |
144 | |
137 | sub fork_sub(&) { |
145 | sub fork_exec(&) { |
138 | my ($cb) = @_; |
146 | my ($cb) = @_; |
139 | |
147 | |
140 | if (my $pid = fork) { |
148 | if (my $pid = fork) { |
141 | my $current = $Coro::current; |
149 | my $current = $Coro::current; |
142 | my $w = AnyEvent->child (pid => $pid, cb => sub { $current->ready }); |
150 | my $w = AnyEvent->child (pid => $pid, cb => sub { $current->ready }); |
… | |
… | |
147 | warn $@; |
155 | warn $@; |
148 | POSIX::_exit 1; |
156 | POSIX::_exit 1; |
149 | } |
157 | } |
150 | } |
158 | } |
151 | |
159 | |
|
|
160 | sub imgsize($) { |
|
|
161 | open my $fh, "-|", $IDENTIFY, qw(-ping -format %w,%h --), $_[0] |
|
|
162 | or die "$IDENTIFY: $!"; |
|
|
163 | |
|
|
164 | Coro::AnyEvent::readable $fh; |
|
|
165 | |
|
|
166 | my ($w, $h) = split /,/, <$fh>; |
|
|
167 | |
|
|
168 | ($w+0, $h+0) |
|
|
169 | } |
|
|
170 | |
|
|
171 | # make $dst from $src, if not uptodate, by calling $how |
|
|
172 | sub make_file($$$) { |
|
|
173 | my ($src, $dst, $how) = @_; |
|
|
174 | |
|
|
175 | my $t = (aio_stat $dst) ? -1 : (stat _)[9]; |
|
|
176 | |
|
|
177 | for (ref $src ? @$src : $src) { |
|
|
178 | aio_stat $_ |
|
|
179 | and die "$_: $!"; |
|
|
180 | |
|
|
181 | if ((stat _)[9] > $t) { |
|
|
182 | # outdated, redo |
|
|
183 | $how->(); |
|
|
184 | last; |
|
|
185 | } |
|
|
186 | } |
|
|
187 | } |
|
|
188 | |
152 | sub inst_maps($) { |
189 | sub inst_maps($) { |
153 | my (undef, $path) = @_; |
190 | my (undef, $path) = @_; |
154 | |
191 | |
155 | print "\nInstalling '$path' to '$DATADIR/maps'\n\n"; |
192 | print "\nInstalling '$path' to '$DATADIR/maps'\n\n"; |
156 | |
193 | |
… | |
… | |
248 | unless ($path =~ /~$/) { |
285 | unless ($path =~ /~$/) { |
249 | # possibly enlarge |
286 | # possibly enlarge |
250 | if (0 > aio_stat "$stem.64x64.png") { |
287 | if (0 > aio_stat "$stem.64x64.png") { |
251 | my $other = "$stem.64x64.png~"; |
288 | my $other = "$stem.64x64.png~"; |
252 | |
289 | |
253 | if (0 > aio_lstat $other or (-M _) > (-M $path)) { |
290 | make_file $path, $other, sub { |
254 | fork_sub { |
291 | fork_exec { |
255 | my $CROP; |
292 | my $CROP; |
256 | my $SRC = "png:\Q$path\E"; |
293 | my $SRC = "png:\Q$path\E"; |
257 | |
294 | |
258 | my $is_floor = $arc->{is_floor}; |
295 | my $is_floor = $arc->{is_floor}; |
259 | my $is_wall = 0; |
296 | my $is_wall = 0; |
… | |
… | |
313 | |
350 | |
314 | system "convert -depth 8 $SRC rgba:-" |
351 | system "convert -depth 8 $SRC rgba:-" |
315 | . "| $exec_prefix/bin/cfhq2xa $w $h 0" |
352 | . "| $exec_prefix/bin/cfhq2xa $w $h 0" |
316 | . "| convert -depth 8 -size ".($w * 2)."x".($h * 2)." rgba:- $CROP $QUANTIZE -quality 00 png32:\Q$other\E~" |
353 | . "| convert -depth 8 -size ".($w * 2)."x".($h * 2)." rgba:- $CROP $QUANTIZE -quality 00 png32:\Q$other\E~" |
317 | and die "convert/cfhq2xa pipeline error: status $? ($!)"; |
354 | and die "convert/cfhq2xa pipeline error: status $? ($!)"; |
318 | system $OPTIPNG, "-i0", "-q", "$other~"; |
355 | optipng "$other~"; |
319 | die "$other~ has zero size, aborting." unless -s "$other~"; |
|
|
320 | rename "$other~", $other; |
356 | rename "$other~", $other; |
321 | }; |
357 | }; |
322 | } |
358 | }; |
323 | |
359 | |
324 | push @c_png, [$other, !$CACHE]; |
360 | push @c_png, [$other, !$CACHE]; |
325 | } |
361 | } |
326 | |
362 | |
327 | # possibly scale down |
363 | # possibly scale down |
328 | if (0 > aio_stat "$stem.32x32.png") { |
364 | if (0 > aio_stat "$stem.32x32.png") { |
329 | my $other = "$stem.32x32.png~"; |
365 | my $other = "$stem.32x32.png~"; |
330 | |
366 | |
331 | if (0 > aio_lstat $other or (-M _) > (-M $path)) { |
367 | make_file $path, $other, sub { |
332 | fork_sub { |
368 | fork_exec { |
333 | system "convert png:\Q$path\E -geometry 50% -filter lanczos $QUANTIZE -quality 00 png32:\Q$other\E~"; |
369 | system "convert png:\Q$path\E -geometry 50% -filter lanczos $QUANTIZE -quality 00 png32:\Q$other\E~"; |
334 | system $OPTIPNG, "-i0", "-q", "$other~"; |
370 | system $OPTIPNG, "-i0", "-q", "$other~"; |
335 | |
371 | |
336 | # reduce smoothfaces >10000 bytes |
372 | # reduce smoothfaces >10000 bytes |
337 | # obsolete, no longer required |
373 | # obsolete, no longer required |
… | |
… | |
352 | } |
388 | } |
353 | |
389 | |
354 | die "$other~ has zero size, aborting." unless -s "$other~"; |
390 | die "$other~ has zero size, aborting." unless -s "$other~"; |
355 | rename "$other~", $other; |
391 | rename "$other~", $other; |
356 | }; |
392 | }; |
357 | } |
393 | }; |
358 | |
394 | |
359 | #warn "scaled down $path to $other\n";#d# |
395 | #warn "scaled down $path to $other\n";#d# |
360 | push @c_png, [$other, !$CACHE]; |
396 | push @c_png, [$other, !$CACHE]; |
361 | } |
397 | } |
362 | } |
398 | } |
… | |
… | |
376 | } |
412 | } |
377 | |
413 | |
378 | my $mtime = (lstat $path)[9]; |
414 | my $mtime = (lstat $path)[9]; |
379 | my @todo = grep { $_->[3] <= $mtime } @tile; |
415 | my @todo = grep { $_->[3] <= $mtime } @tile; |
380 | if (@todo) { |
416 | if (@todo) { |
381 | fork_sub { |
417 | fork_exec { |
382 | open my $convert, "|-", $CONVERT, |
418 | open my $convert, "|-", $CONVERT, |
383 | "png:-", |
419 | "png:-", |
384 | (map { |
420 | (map { |
385 | ( |
421 | ( |
386 | "(", |
422 | "(", |
… | |
… | |
442 | my ($face, $visibility, $fg, $bg, $glyph) = split /\s+/, $_, 5; |
478 | my ($face, $visibility, $fg, $bg, $glyph) = split /\s+/, $_, 5; |
443 | # bg not used except for text clients |
479 | # bg not used except for text clients |
444 | |
480 | |
445 | utf8::decode $glyph; |
481 | utf8::decode $glyph; |
446 | $glyph =~ s/^\?(?=.)//; # remove "autoglyph" flag |
482 | $glyph =~ s/^\?(?=.)//; # remove "autoglyph" flag |
|
|
483 | $glyph =~ s/^"(.+)"$/$1/; # allow for ""-style quoting |
447 | |
484 | |
448 | $fg = "white" if $fg eq "none"; # lots of faces have no fg colour yet |
485 | $fg = "white" if $fg eq "none"; # lots of faces have no fg colour yet |
449 | |
486 | |
450 | (my $fgi = $COLOR{$fg}) |
487 | (my $fgi = $COLOR{$fg}) |
451 | // warn "WARNING: $path: $face specifies unknown foreground colour '$fg'.\n"; |
488 | // warn "WARNING: $path: $face specifies unknown foreground colour '$fg'.\n"; |
… | |
… | |
579 | } |
616 | } |
580 | |
617 | |
581 | $FILECACHE{$_[0]} |
618 | $FILECACHE{$_[0]} |
582 | } |
619 | } |
583 | |
620 | |
|
|
621 | # convert an image and a palette to some indexed 2d-matrix structure |
|
|
622 | sub process_plt { |
|
|
623 | my ($base, $plt) = @_; |
|
|
624 | |
|
|
625 | my ($w, $h) = imgsize "$base.png"; |
|
|
626 | |
|
|
627 | $w * $h |
|
|
628 | or die "$base.png: unable to identify correct size\n"; |
|
|
629 | |
|
|
630 | my @plt; |
|
|
631 | my %map; |
|
|
632 | |
|
|
633 | for (split /\n/, $plt) { |
|
|
634 | next unless /\S/; |
|
|
635 | next if /^\s*#/; |
|
|
636 | |
|
|
637 | /^([0-9a-fA-F]{3,6})\s*(.*?)\s*$/ |
|
|
638 | or die "unparseable palette entry for $base.plt: $_"; |
|
|
639 | |
|
|
640 | my ($rgb, $name) = ($1, $2); |
|
|
641 | |
|
|
642 | $rgb =~ s/^(.)(.)(.)$/$1$1$2$2$3$3/; |
|
|
643 | |
|
|
644 | $map{pack "H*", $rgb} = chr @plt; |
|
|
645 | push @plt, $name; |
|
|
646 | } |
|
|
647 | |
|
|
648 | make_file ["$base.plt", "$base.png"], "$base.tbl~", sub { |
|
|
649 | warn "building $base\n" if $VERBOSE >= 3; |
|
|
650 | |
|
|
651 | fork_exec { |
|
|
652 | open my $png, "-|", $CONVERT, qw(-depth 8 --), "$base.png", "rgb:" |
|
|
653 | or die "$base.png: $!"; |
|
|
654 | |
|
|
655 | local $/; |
|
|
656 | $png = <$png>; |
|
|
657 | |
|
|
658 | $w * $h * 3 == length $png |
|
|
659 | or die "$base.png: failed to read enough data from file\n"; |
|
|
660 | |
|
|
661 | $png =~ s/(...)/$map{$1}/ge; |
|
|
662 | |
|
|
663 | $w * $h == length $png |
|
|
664 | or die "$base.png: failed to map all data - wrong palette?\n"; |
|
|
665 | |
|
|
666 | { |
|
|
667 | open my $fh, ">:raw", "$base.tbl~~" |
|
|
668 | or die "$base.tbl~~: $!"; |
|
|
669 | syswrite $fh, $png; |
|
|
670 | } |
|
|
671 | |
|
|
672 | rename "$base.tbl~~", "$base.tbl~"; |
|
|
673 | }; |
|
|
674 | }; |
|
|
675 | |
|
|
676 | 0 <= aio_load "$base.tbl~", my $tbl |
|
|
677 | or die "$base.tbl~: $!"; |
|
|
678 | |
|
|
679 | IO::AIO::aio_unlink "$base.tbl~" unless $CACHE; |
|
|
680 | |
|
|
681 | Compress::LZF::compress nfreeze { |
|
|
682 | w => $w, |
|
|
683 | h => $h, |
|
|
684 | plt => \@plt, |
|
|
685 | tbl => $tbl, |
|
|
686 | } |
|
|
687 | } |
|
|
688 | |
584 | sub process_res { |
689 | sub process_res { |
585 | my ($dir, $file, $type) = @_; |
690 | my ($dir, $file, $type) = @_; |
586 | |
691 | |
587 | my $data; |
|
|
588 | aio_load "$dir/$file", $data; |
692 | 0 <= aio_load "$dir/$file", my $data |
|
|
693 | or die "$dir/$file: $!"; |
589 | |
694 | |
590 | my $meta = load_cached "$dir/meta", sub { JSON::XS->new->utf8->relaxed->decode (shift) }; |
695 | my $meta = load_cached "$dir/meta", sub { JSON::XS->new->utf8->relaxed->decode (shift) }; |
591 | |
696 | |
592 | utf8::decode $dir; |
697 | utf8::decode $dir; |
593 | utf8::decode $file; |
698 | utf8::decode $file; |
… | |
… | |
620 | } |
725 | } |
621 | |
726 | |
622 | $file =~ s/\.res$//; |
727 | $file =~ s/\.res$//; |
623 | $file =~ s/\.(ogg|wav|jpg|png)$//; |
728 | $file =~ s/\.(ogg|wav|jpg|png)$//; |
624 | |
729 | |
625 | substr $dir, 0, 1 + length $PATH, ""; |
730 | if ($file =~ s/\.plt$//) { |
626 | |
731 | $data = process_plt "$dir/$file", $data; |
627 | if (my $filter = $meta->{cfutil_filter}) { |
732 | } elsif (my $filter = $meta->{cfutil_filter}) { |
628 | if ($filter eq "yaml2json") { |
733 | if ($filter eq "yaml2json") { |
629 | $data = JSON::XS::encode_json YAML::XS::Load $data; |
734 | $data = JSON::XS::encode_json YAML::XS::Load $data; |
630 | } elsif ($filter eq "json2json") { |
735 | } elsif ($filter eq "json2json") { |
631 | $data = JSON::XS::encode_json JSON::XS->relaxed->utf8->decode ($data); |
736 | $data = JSON::XS::encode_json JSON::XS->relaxed->utf8->decode ($data); |
632 | } elsif ($filter eq "perl2json") { |
737 | } elsif ($filter eq "perl2json") { |
… | |
… | |
634 | $data = JSON::XS::encode_json $data; |
739 | $data = JSON::XS::encode_json $data; |
635 | } else { |
740 | } else { |
636 | warn "$dir/$file: unknown filter $filter, skipping\n"; |
741 | warn "$dir/$file: unknown filter $filter, skipping\n"; |
637 | } |
742 | } |
638 | } |
743 | } |
|
|
744 | |
|
|
745 | substr $dir, 0, 1 + length $PATH, ""; |
639 | |
746 | |
640 | $RESOURCE{"$dir/$file"} = { |
747 | $RESOURCE{"$dir/$file"} = { |
641 | type => (exists $meta->{type} ? delete $meta->{type} : $type), |
748 | type => (exists $meta->{type} ? delete $meta->{type} : $type), |
642 | data => $data, |
749 | data => $data, |
643 | %$meta ? (meta => $meta) : (), |
750 | %$meta ? (meta => $meta) : (), |
… | |
… | |
753 | if (!-d "$path/treasures") { |
860 | if (!-d "$path/treasures") { |
754 | warn "'$path' does not look like an arch directory ('treasures' directory is missing).\n"; |
861 | warn "'$path' does not look like an arch directory ('treasures' directory is missing).\n"; |
755 | exit 1 unless $FORCE; |
862 | exit 1 unless $FORCE; |
756 | } |
863 | } |
757 | |
864 | |
758 | print "start file scan, arc, any processing...\n" if $VERBOSE; |
865 | print "starting file scan, arc, any processing...\n" if $VERBOSE; |
759 | |
866 | |
760 | my @a_arc = map +(async \&process_arc), 1..2; |
867 | my @a_arc = map +(async \&process_arc), 1..2; |
761 | my @a_any = map +(async \&process_any), 1..4; |
868 | my @a_any = map +(async \&process_any), 1..4; |
762 | |
869 | |
763 | find_files $path; |
870 | find_files $path; |
764 | |
871 | |
765 | $c_arc->shutdown; |
872 | $c_arc->shutdown; |
766 | $c_any->shutdown; |
873 | $c_any->shutdown; |
767 | |
874 | |
768 | $_->join for @a_arc; # need to parse all archetypes before png processing |
875 | $_->join for @a_arc; # need to parse all archetypes before png processing |
|
|
876 | print "ended arc processing...\n" if $VERBOSE; |
769 | |
877 | |
770 | print "end arc, start png processing...\n" if $VERBOSE; |
878 | print "starting png processing...\n" if $VERBOSE; |
771 | |
879 | |
772 | # eight png crunchers work fine for my 4x smp machine |
880 | # eight png crunchers work fine for my 4x smp machine |
773 | my @a_png = map +(async \&process_png), 1..8; |
881 | my @a_png = map +(async \&process_png), 1..8; |
774 | |
882 | |
775 | print "end any processing...\n" if $VERBOSE; |
|
|
776 | $_->join for @a_any; |
883 | $_->join for @a_any; |
777 | |
|
|
778 | print "end png processing...\n" if $VERBOSE; |
884 | print "ended any processing...\n" if $VERBOSE; |
|
|
885 | |
779 | $_->join for @a_png; |
886 | $_->join for @a_png; |
|
|
887 | print "ended png processing...\n" if $VERBOSE; |
780 | |
888 | |
781 | print "scanning done, processing results...\n" if $VERBOSE; |
889 | print "scanning done, processing results...\n" if $VERBOSE; |
782 | { |
890 | { |
783 | # remove path prefix from editor_folder |
891 | # remove path prefix from editor_folder |
784 | $_->{editor_folder} =~ /^\x00/ |
892 | $_->{editor_folder} =~ /^\x00/ |
… | |
… | |
911 | version => 2, |
1019 | version => 2, |
912 | faceinfo => \%FACEINFO, |
1020 | faceinfo => \%FACEINFO, |
913 | animinfo => \%ANIMINFO, |
1021 | animinfo => \%ANIMINFO, |
914 | resource => \%RESOURCE, |
1022 | resource => \%RESOURCE, |
915 | }; |
1023 | }; |
916 | |
|
|
917 | } |
1024 | } |
918 | |
1025 | |
919 | print "committing files...\n" if $VERBOSE; |
1026 | print "committing files...\n" if $VERBOSE; |
920 | |
1027 | |
921 | for (qw(archetypes facedata treasures), @COMMIT) { |
1028 | for (qw(archetypes facedata treasures), @COMMIT) { |