ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/server/utils/cfutil.in
Revision: 1.14
Committed: Wed Mar 14 00:04:59 2007 UTC (17 years, 2 months ago) by root
Branch: MAIN
Changes since 1.13: +7 -9 lines
Log Message:
- rewrote smooth face handling, as a side-effect, smoothing seems to work
  again and smooth faces can be reloaded.
- the server now sends the full animation for an object the first time
  it is seen, this uses slightly more bandwidth initially, but avoids
  the flickering for objects change their face later.

File Contents

# Content
1 #!@PERL@
2
3 use strict;
4
5 my $prefix = "@prefix@";
6 my $exec_prefix = "@exec_prefix@";
7 my $datarootdir = "@datarootdir@";
8 my $DATADIR = "@datadir@/@PACKAGE@";
9
10 my $CONVERT = "@CONVERT@";
11 my $IDENTIFY = "@IDENTIFY@";
12 my $OPTIPNG = "@OPTIPNG@";
13 my $RSYNC = "@RSYNC@";
14
15 use Getopt::Long;
16 use Coro::Event;
17 use AnyEvent;
18 use IO::AIO ();
19 use File::Temp;
20 use Crossfire;
21 use Coro;
22 use Coro::AIO;
23 use POSIX ();
24 use Digest::MD5;
25
26 sub usage {
27 warn <<EOF;
28 Usage: cfutil [-v] [-q] [--force] [--cache]
29 [--install-arch path]
30 [--install-maps maps]
31 [--print-statedir]
32 [--print-confdir]
33 [--print-datadir]
34 [--print-libdir]
35 [--print-bindir]
36 EOF
37 exit 1;
38 }
39
40 my $VERBOSE = 1;
41 my $CACHE = 0;
42 my $FORCE;
43 my $TMPDIR = "/tmp/cfutil$$~";
44 my $TMPFILE = "aaaa0";
45
46 END { system "rm", "-rf", $TMPDIR }
47
48 Event->signal (signal => "INT", cb => sub { exit 1 });
49 Event->signal (signal => "TERM", cb => sub { exit 1 });
50
51 mkdir $TMPDIR, 0700
52 or die "$TMPDIR: $!";
53
54 sub fork_sub(&) {
55 my ($cb) = @_;
56
57 if (my $pid = fork) {
58 my $current = $Coro::current;
59 my $w = AnyEvent->child (pid => $pid, cb => sub { $current->ready });
60 Coro::schedule;
61 } else {
62 eval { $cb->() };
63 POSIX::_exit 0 unless $@;
64 warn $@;
65 POSIX::_exit 1;
66 }
67 }
68
69 sub inst_maps($) {
70 my (undef, $path) = @_;
71
72 print "installing '$path' to '$DATADIR/maps'\n";
73
74 if (!-f "$path/regions") {
75 warn "'$path' does not look like a maps directory ('regions' file is missing).\n";
76 exit 1 unless $FORCE;
77 }
78
79 system $RSYNC, "-av", "$path/.", "$DATADIR/maps/.", "--delete", "--exclude", "CVS", "--delete-excluded"
80 or die "map installation failed.\n";
81
82 print "maps installed successfully.\n";
83 }
84
85 {
86 our %PNG32;
87 our %FACEINFO;
88 our @ARC;
89 our $TRS;
90 our $NFILE;
91 our %ANIM;
92
93 our (@png, @trs, @arc); # files we are interested in
94
95 sub commit_png {
96 my ($name, $data) = @_;
97
98 $PNG32{$name} = $data;
99 $FACEINFO{$name} ||= {};
100 }
101
102 sub process_png {
103 while (@png) {
104 my $path = pop @png;
105
106 my $png;
107 aio_lstat $path;
108 my ($size, $mtime) = (stat _)[7,9];
109
110 if (0 > aio_load $path, $png) {
111 warn "$path: $!, skipping.\n";
112 next;
113 }
114
115 # quickly extract width and height of the (necessarily PNG) image
116 unless ($png =~ /^\x89PNG\x0d\x0a\x1a\x0a....IHDR(........)/s) {
117 warn "$path: not a recongized png file, skipping.\n";
118 next;
119 }
120
121 my ($w, $h) = unpack "NN", $1;
122
123 (my $face = $path) =~ s/^.*\///;
124 my $T = 32;
125
126 unless ($face =~ s/\.base\.(...)\.png$/.$1/) {
127 warn "$path: weird filename, skipping.\n";
128 next;
129 }
130
131 if ($w < $T || $h < $T) {
132 warn "$path: too small ($w $h), skipping.\n";
133 next;
134 }
135
136 if ($w % $T || $h % $T) {
137 warn "$path: weird png size ($w $h), skipping.\n";
138 next;
139 }
140
141 if (($w > $T || $h > $T) && $face !~ /_S\./) {
142 # split
143 my @tile;
144 for my $x (0 .. (int $w / $T) - 1) {
145 for my $y (0 .. (int $h / $T) - 1) {
146 my $file = "$path+$x+$y~";
147 aio_lstat $file;
148 push @tile, [$x, $y, $file, (stat _)[9]];
149 }
150 }
151
152 my $mtime = (lstat $path)[9];
153 my @todo = grep { $_->[3] <= $mtime } @tile;
154 if (@todo) {
155 fork_sub {
156 open my $convert, "|-", $CONVERT,
157 "png:-",
158 (map {
159 (
160 "(",
161 "+clone",
162 -crop => (sprintf "%dx%d+%d+%d", $T, $T, $_->[0] * $T, $_->[1] * $T),
163 "+repage",
164 -write => "png:$_->[2]~",
165 "+delete",
166 ")",
167 )
168 } @todo),
169 "null:";
170
171 binmode $convert;
172 print $convert $png;
173 close $convert;
174
175 # pass 2, optimise, and rename
176 for (@todo) {
177 system $OPTIPNG, "-o5", "-i0", "-q", "$_->[2]~";
178 rename "$_->[2]~", $_->[2];
179 }
180 };
181 }
182
183 for (@tile) {
184 my ($x, $y, $file) = @$_;
185 my $tile;
186
187 if (0 > aio_load $file, $tile) {
188 die "$path: unable to read tile +$x+$y, aborting.\n";
189 }
190 IO::AIO::aio_unlink $file unless $CACHE;
191 commit_png $x|$y ? "$face+$x+$y" : $face, $tile;
192 }
193 } else {
194 # use as-is (either small, use smooth)
195 commit_png $face, $png;
196 }
197 }
198 }
199
200 sub process_arc {
201 while (@arc) {
202 my ($dir, $file) = @{pop @arc};
203
204 my $arc;
205 aio_load "$dir/$file", $arc; # simply pre-cache, as read_arch wants a file :/
206
207 my $arc = read_arch "$dir/$file";
208 for my $o (values %$arc) {
209 push @ARC, $o;
210
211 $o->{editor_folder} = $dir;
212
213 my $visibility = delete $o->{visibility};
214 my $magicmap = delete $o->{magicmap};
215
216 # find upper left corner :/
217 # omg, this is sooo broken
218 my ($dx, $dy);
219 for (my $o = $o; $o; $o = $o->{more}) {
220 $dx = $o->{x} if $o->{x} < $dx;
221 $dy = $o->{y} if $o->{y} < $dy;
222 }
223
224 for (my $o = $o; $o; $o = $o->{more}) {
225 my $x = $o->{x} - $dx;
226 my $y = $o->{y} - $dy;
227
228 my $ext = $x|$y ? "+$x+$y" : "";
229
230 $o->{face} .= $ext unless /^blank.x11$|^empty.x11$/;
231
232 my $visibility = delete $o->{visibility} if exists $o->{visibility};
233 my $magicmap = delete $o->{magicmap} if exists $o->{magicmap};
234
235 my $anim = delete $o->{anim};
236
237 if ($anim) {
238 $o->{animation} = "$o->{_name}";
239
240 for (@$anim) {
241 $_ .= $ext unless /^facings\s|^blank.x11$|^empty.x11$/;
242 }
243
244 $ANIM{"$o->{_name}$ext"} =
245 join "", map "$_\n",
246 "anim $o->{_name}",
247 @$anim,
248 "mina";
249 }
250
251 for my $face ($o->{face} || (), @{$anim || []}) {
252 next if $face =~ /^facings\s|^blank.x11$|^empty.x11$/;
253
254 my $info = $FACEINFO{$face} ||= {};
255
256 $info->{visibility} = $visibility if defined $visibility;
257 $info->{magicmap} = $magicmap if defined $magicmap;
258 }
259
260 if (my $smooth = delete $o->{smoothface}) {
261 my ($face, $smooth) = split /\s+/, $smooth;
262 # skip empty_S.x11, it seems to server no purpose whatsoever
263 # but increases bandwidth demands and worse.
264 unless ($smooth eq "empty_S.x11") {
265 $FACEINFO{$face}{smooth} = $smooth;
266 }
267 }
268 }
269 }
270 }
271 }
272
273 sub process_trs {
274 while (@trs) {
275 my ($dir, $file) = @{pop @trs};
276 my $path = "$dir/$file";
277
278 my $trs;
279 if (0 > aio_load $path, $trs) {
280 warn "$path: $!, skipping.\n";
281 next;
282 }
283
284 $TRS .= $trs;
285 }
286 }
287
288 sub find_files;
289 sub find_files {
290 my ($path) = @_;
291
292 IO::AIO::aioreq_pri 4;
293 IO::AIO::aio_scandir $path, 4, sub {
294 my ($dirs, $nondirs) = @_;
295
296 find_files "$path/$_"
297 for grep $_ !~ /^(?:CVS|dev)$/, @$dirs;
298
299 for my $file (@$nondirs) {
300 if ($file =~ /\.png$/) {
301 push @png, "$path/$file";
302 } elsif ($file =~ /\.trs$/) {
303 push @trs, [$path, $file];
304 } elsif ($file =~ /\.arc$/) {
305 push @arc, [$path, $file];
306 } else {
307 warn "ignoring $path/$file\n" if $VERBOSE >= 2;
308 }
309 }
310 };
311 }
312
313 sub inst_arch($) {
314 my (undef, $path) = @_;
315
316 print "installing '$path' to '$DATADIR'\n";
317
318 if (!-d "$path/treasures") {
319 warn "'$path' does not look like an arch directory ('treasures' directory is missing).\n";
320 exit 1 unless $FORCE;
321 }
322
323 find_files $path;
324 IO::AIO::flush;
325
326 $_->join for (
327 (async \&process_png), (async \&process_png),
328 (async \&process_trs), (async \&process_trs),
329 (async \&process_arc), (async \&process_arc),
330 );
331
332 {
333 open my $fh, ">:utf8", "$DATADIR/animations~"
334 or die "$DATADIR/animations~: $!";
335 print $fh join "", map $ANIM{$_}, sort keys %ANIM
336 }
337
338 {
339 open my $fh, ">:utf8", "$DATADIR/archetypes~"
340 or die "$DATADIR/archetypes~: $!";
341 substr $_->{editor_folder}, 0, 1 + length $path, "" for @ARC;
342 print $fh Crossfire::archlist_to_string \@ARC;
343 }
344
345 {
346 open my $fh, ">:utf8", "$DATADIR/treasures~"
347 or die "$DATADIR/treasures~: $!";
348 print $fh $TRS;
349 }
350
351 {
352 while (my ($k, $v) = each %FACEINFO) {
353 $v->{data32} ||= delete $PNG32{$k};
354 }
355
356 while (my ($k, $v) = each %FACEINFO) {
357 length $v->{data32} or warn "$k: face has no png32. this will not work (shoddy gcfclient will crash of course).\n";
358
359 $v->{chksum32} = Digest::MD5::md5 $v->{data32};
360 }
361
362 open my $fh, ">:perlio", "$DATADIR/faces~"
363 or die "$DATADIR/faces~: $!";
364
365 print $fh Storable::nfreeze \%FACEINFO;
366 }
367
368 for (qw(archetypes faces animations treasures)) {
369 chmod 0644, "$DATADIR/$_~";
370 rename "$DATADIR/$_~", "$DATADIR/$_"
371 or die "$DATADIR/$_: $!";
372 }
373
374 print "archetype data installed successfully.\n";
375 }
376 }
377
378 Getopt::Long::Configure ("bundling", "no_ignore_case");
379 GetOptions (
380 "verbose|v:+" => \$VERBOSE,
381 "cache" => \$CACHE,
382 "quiet|q" => sub { $VERBOSE = 0 },
383 "force" => sub { $FORCE = 1 },
384 "install-arch=s" => \&inst_arch,
385 "install-maps=s" => \&inst_maps,
386 "print-statedir" => sub { print "@pkgstatedir@\n" },
387 "print-datadir" => sub { print "$DATADIR\n" },
388 "print-confdir" => sub { print "@pkgconfdir@\n" },
389 "print-libdir" => sub { print "@libdir@/@PACKAGE@\n" },
390 "print-bindir" => sub { print "@bindir@/@PACKAGE@\n" },
391 ) or usage;
392