c80e0a92640c08b72682a66ef76638ef1e546ba5
[xonotic/xonotic.git] / misc / tools / xonotic-map-compiler
1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5 use POSIX;
6 use File::Temp;
7
8 # change these to match your system, or define them in ~/.xonotic-map-compiler
9 # (just copy paste this part to the file ~/.xonotic-map-compiler)
10
11         # Path to Xonotic (where the data directory is in)
12         our $XONOTICDIR   = '/home/rpolzer/Games/Xonotic';
13
14         # Path to your q3map2 program. You find it in your GtkRadiant/install
15         # directory.
16         our $Q3MAP2      = '/home/rpolzer/Games/Xonotic/netradiant/install/q3map2.x86';
17
18         # General flags for q3map2 (for example -threads 4)
19         our $Q3MAP2FLAGS = '-fs_forbiddenpath xonotic-data.pk3 -fs_forbiddenpath xonotic-data.pk3dir -fs_forbiddenpath xonotic-nexcompat.pk3 -fs_forbiddenpath xonotic-nexcompat.pk3dir';
20
21         # Default flags for the -bsp stage
22         our $BSPFLAGS    = '-meta -maxarea -samplesize 8 -mv 1000000 -mi 6000000';
23
24         # Default flags for the -vis stage
25         our $VISFLAGS    = '';
26
27         # Default flags for the -light stage
28         our $LIGHTFLAGS  = '-lightmapsearchpower 3 -deluxe -patchshadows -randomsamples -samples 4 -lightmapsize 512 -fast -fastbounce -dirty -bouncegrid -fill';
29
30         # Default flags for the -minimap stage
31         our $MINIMAPFLAGS = '';
32
33         # Default order of commands
34         our $ORDER = 'vis,light';
35
36 # end of user changable part
37
38 do "$ENV{HOME}/.xonotic-map-compiler";
39
40 sub Usage()
41 {
42         print <<EOF;
43 Usage:
44 $0 mapname [-bsp bspflags...] [-vis visflags...] [-light lightflags...] [-minimap minimapflags]
45 EOF
46         exit 1;
47 }
48
49 my $options =
50 {
51         bsp => [split /\s+/, $BSPFLAGS],
52         vis => [split /\s+/, $VISFLAGS],
53         light => [split /\s+/, $LIGHTFLAGS],
54         minimap => [split /\s+/, $MINIMAPFLAGS],
55         scale => [], # can't have defaults atm
56         order => [split /\s*,\s*/, $ORDER],
57         maps => [],
58         scale => 1,
59         bsp_timeout => 0,
60         vis_timeout => 0,
61         light_timeout => 0,
62         minimap_timeout => 0,
63         scale_timeout => 0
64 };
65
66 my $curmode = 'maps';
67
68 while(@ARGV)
69 {
70         $_ = shift @ARGV;
71         my $enterflags = undef;
72         if($_ eq '-bsp')
73         {
74                 $enterflags = 'bsp';
75         }
76         elsif($_ eq '-vis')
77         {
78                 $enterflags = 'vis';
79         }
80         elsif($_ eq '-light')
81         {
82                 $enterflags = 'light';
83         }
84         elsif($_ eq '-minimap')
85         {
86                 $enterflags = 'minimap';
87         }
88         elsif($_ eq '-map')
89         {
90                 $curmode = 'maps';
91         }
92         elsif($_ eq '-scale')
93         {
94                 $options->{scale} = (shift @ARGV) || 1;
95                 $enterflags = 'scale';
96         }
97         elsif($_ eq '-novis')
98         {
99                 $options->{vis} = undef;
100         }
101         elsif($_ eq '-nolight')
102         {
103                 $options->{light} = undef;
104         }
105         elsif($_ eq '-nominimap')
106         {
107                 $options->{minimap} = undef;
108         }
109         elsif($_ eq '-noshaderlist')
110         {
111                 $options->{noshaderlist} = 1;
112         }
113         elsif($_ eq '-bsp_timeout')
114         {
115                 $options->{bsp_timeout} = shift @ARGV;
116         }
117         elsif($_ eq '-vis_timeout')
118         {
119                 $options->{vis_timeout} = shift @ARGV;
120         }
121         elsif($_ eq '-light_timeout')
122         {
123                 $options->{light_timeout} = shift @ARGV;
124         }
125         elsif($_ eq '-minimap_timeout')
126         {
127                 $options->{minimap_timeout} = shift @ARGV;
128         }
129         elsif($_ eq '-scale_timeout')
130         {
131                 $options->{minimap_timeout} = shift @ARGV;
132         }
133         elsif($_ eq '-order')
134         {
135                 $options->{order} = [split /\s*,\s*/, shift @ARGV];
136         }
137         elsif($_ =~ /^--no(-.*)/)
138         {
139                 if($curmode eq 'maps')
140                 {
141                         $curmode = 'bsp';
142                 }
143                 my $flag = $1;
144                 @{$options->{$curmode}} = grep { (($_ eq $flag) ... /^-/) !~ /^[0-9]+$/ } @{$options->{$curmode}};
145                         # so, e.g. --no-samplesize removes "-samplesize" and a following "3"
146         }
147         elsif($_ =~ /^-(-.*)/)
148         {
149                 if($curmode eq 'maps')
150                 {
151                         $curmode = 'bsp';
152                 }
153                 push @{$options->{$curmode}}, $1;
154         }
155         elsif($_ =~ /^-/ and $curmode eq 'maps')
156         {
157                 $curmode = 'bsp';
158                 push @{$options->{$curmode}}, $_;
159         }
160         else
161         {
162                 push @{$options->{$curmode}}, $_;
163         }
164         if(defined $enterflags)
165         {
166                 $curmode = $enterflags;
167                 if($ARGV[0] eq '+')
168                 {
169                         shift @ARGV;
170                 }
171                 else
172                 {
173                         $options->{$curmode} = [];
174                 }
175         }
176 }
177
178 my $linkdir = File::Temp::tempdir("xonotic-map-compiler.XXXXXX", TMPDIR => 1, CLEANUP => 1);
179
180 sub q3map2(@)
181 {
182         my $mode = $_[0];
183         my $timeout = undef;
184         $timeout = $options->{bsp_timeout} if $mode eq '-bsp';
185         $timeout = $options->{vis_timeout} if $mode eq '-vis';
186         $timeout = $options->{light_timeout} if $mode eq '-light';
187         $timeout = $options->{minimap_timeout} if $mode eq '-minimap';
188         $timeout = $options->{scale_timeout} if $mode eq '-scale';
189         die "Invalid call: not a standard q3map2 stage" if not defined $timeout;
190         my @args = ($Q3MAP2, split(/\s+/, $Q3MAP2FLAGS), '-game', 'xonotic', '-fs_basepath', $XONOTICDIR, '-fs_basepath', $linkdir, '-v', @_);
191         print "\$ @args\n";
192         defined(my $pid = fork())
193                 or die "fork: $!";
194         if($pid) # parent
195         {
196                 local $SIG{ALRM} = sub { warn "SIGALRM caught\n"; kill TERM => $pid; };
197                 alarm $timeout
198                         if $timeout;
199                 if(waitpid($pid, 0) != $pid)
200                 {
201                         die "waitpid: did not return our child process $pid: $!";
202                 }
203                 alarm 0;
204                 return ($? == 0);
205         }
206         else # child
207         {
208                 exec @args
209                         or die "exec: $!";
210         }
211 }
212
213 (my $mapdir = getcwd()) =~ s!/[^/]*(?:$)!!;
214 $mapdir = "/" if $mapdir eq "";
215 symlink "$mapdir", "$linkdir/data";
216
217 my ($prescale, $postscale) = ($options->{scale} =~ /^([0-9.]+)(?::([0-9.]+))?$/);
218 $postscale = 1 if not defined $postscale;
219
220 for my $m(@{$options->{maps}})
221 {
222         $m =~ s/\.(?:map|bsp)$//;
223         if($prescale != 1)
224         {
225                 open my $checkfh, "<", "$m.map"
226                         or die "open $m.map: $!";
227                 my $keeplights = 0;
228                 while(<$checkfh>)
229                 {
230                         /^\s*"_keeplights"\s+"1"\s*$/
231                                 or next;
232                         $keeplights = 1;
233                 }
234                 close $checkfh;
235                 die "$m does not define _keeplights to 1"
236                         unless $keeplights;
237         }
238
239         my %shaders = map { m!/([^/.]*)\.shader(?:$)! ? ($1 => 1) : () } glob "../scripts/*.shader";
240
241         my $restore_shaderlist = sub { };
242         if(!$options->{noshaderlist})
243         {
244                 my $previous_shaderlist = undef;
245                 my $shaderlist = "";
246                 if(open my $fh, "<", "$XONOTICDIR/data/scripts/shaderlist.txt")
247                 {
248                         while(<$fh>)
249                         {
250                                 $shaderlist .= $_;
251                         }
252
253                         # we may have to restore the file on exit
254                         $previous_shaderlist = $shaderlist
255                                 if "$XONOTICDIR/data" eq $mapdir;
256                 }
257                 else
258                 {
259                         # possibly extract the shader list from a pk3?
260                         local $ENV{N} = $XONOTICDIR;
261                         $shaderlist = `cd "\$N" && for X in "\$N"/data/data*.pk3; do Y=\$X; done; unzip -p "\$Y" scripts/shaderlist.txt`;
262                 }
263
264                 my $shaderlist_new = "";
265                 for(split /\r?\n|\r/, $shaderlist)
266                 {
267                         delete $shaders{$_};
268                         $shaderlist_new .= "$_\n";
269                 }
270                 if(%shaders)
271                 {
272                         for(sort keys %shaders)
273                         {
274                                 $shaderlist_new .= "$_\n";
275                         }
276                 }
277                 else
278                 {
279                         $shaderlist_new = undef;
280                 }
281
282                 $restore_shaderlist = sub
283                 {
284                         if(defined $shaderlist_new)
285                         {
286                                 if(defined $previous_shaderlist)
287                                 {
288                                         open my $fh, ">", "$mapdir/scripts/shaderlist.txt";
289                                         print $fh $previous_shaderlist;
290                                         close $fh;
291                                 }
292                                 else
293                                 {
294                                         unlink "$mapdir/scripts/shaderlist.txt";
295                                 }
296                         }
297                 };
298
299                 if(defined $shaderlist_new)
300                 {
301                         mkdir "$mapdir/scripts";
302                         open my $fh, ">", "$mapdir/scripts/shaderlist.txt";
303                         print $fh $shaderlist_new;
304                         close $fh;
305                 }
306         }
307
308         local $SIG{INT} = sub
309         {
310                 print "SIGINT caught, cleaning up...\n";
311                 $restore_shaderlist->();
312                 exit 0;
313         };
314
315         eval
316         {
317                 unlink <$m/lm_*>; # delete old external lightmaps
318                 q3map2 '-bsp', @{$options->{bsp}},   "$m.map"
319                         or die "-bsp: $?";
320                 if($prescale != 1)
321                 {
322                         q3map2 '-scale', @{$options->{scale}}, $prescale, "$m.bsp"
323                                 or die "-scale: $?";
324                         rename "${m}_s.bsp", "$m.bsp"
325                                 or die "rename ${m}_s.bsp $m.bsp: $!";
326                 }
327                 my @o = @{$options->{order}};
328                 push @o, qw/light vis/;
329                 my %o = ();
330
331                 for(@o)
332                 {
333                         next if $o{$_}++;
334                         if($_ eq 'light')
335                         {
336                                 if(defined $options->{light})
337                                 {
338                                         q3map2 '-light',        @{$options->{light}}, "$m.map"
339                                                 or die "-light: $?";
340                                 }
341                         }
342                         if($_ eq 'vis')
343                         {
344                                 if(defined $options->{vis})
345                                 {
346                                         q3map2 '-vis',          @{$options->{vis}},   "$m.map"
347                                                 or die "-vis: $?";
348                                 }
349                         }
350                 }
351
352                 if($postscale != 1)
353                 {
354                         q3map2 '-scale', @{$options->{scale}}, $postscale, "$m.bsp"
355                                 or die "-scale: $?";
356                         rename "${m}_s.bsp", "$m.bsp"
357                                 or die "rename ${m}_s.bsp $m.bsp: $!";
358                 }
359
360                 if(defined $options->{minimap})
361                 {
362                         q3map2 '-minimap',      @{$options->{minimap}}, "$m.map"
363                                 or die "-minimap: $?";
364                 }
365
366                 unlink "$m.srf";
367                 unlink "$m.prt";
368
369                 $restore_shaderlist->();
370                 1;
371         }
372         or do
373         {
374                 $restore_shaderlist->();
375                 die $@;
376         };
377 }