7 use Getopt::Long 2.33 qw(HelpMessage :config bundling);
12 'raw|r', # full output
13 'version=i', # force version
14 ) or HelpMessage(-exitval => 2);
17 package Shiar_Parse::WormEdit;
23 "WormEdit053\000LVL" => 53,
24 "WormEdit\34195\000LVL" => 95,
25 "WormEdit\34194\000LVL" => 94,
26 "WormEdit\34193\000LVL" => 93,
33 description => 'Ca64x256',
42 map { (start => $_, end => $_) } [1,
59 enddata => 'Ca255x256',
61 levels => ['*', # levelcount->total actually
99 my ($self, $input) = @_;
100 my ($id, $subid) = (substr($input, 0, 15), ord substr($input, 15, 1));
101 my $version = $opt{version} // $MAGICID{$id}
102 or die "File does not match any known WormEdit level header\n";
104 or warn "Unsupported version $subid (expecting $version)\n";
109 when ($_ <= 95 and $_ > 90) {
110 ref $_ and pop @$_ for @{ $FORMAT[11] }; # only 8 moderefs
111 $FORMAT[-1]->[-1]->[0] = '32C'; # less objects
115 $FORMAT[7] = 'Ca64'; # no reserved space after description
116 #ref $_ and $_->[-1] = 'C' for @{ $FORMAT[11] }; # only 9 moderefs
117 $FORMAT[19] = 'Ca255'; # enddata
118 splice @FORMAT, 6, 2 if $subid < 95; # early (sub)version without description
120 when ($_ <= 94 and $_ > 90) {
121 splice @FORMAT, 6, 2; # no description
122 splice @{ $FORMAT[7] }, 4, 2; # no race
123 splice @FORMAT, 16, 2; # no enddata
124 splice @{ $FORMAT[-1] }, 1, 2; # no name
128 splice @FORMAT, 16, 2; # no hiname
129 $FORMAT[-1]->[0] = 64; # constant amount of levels
132 die "Cannot parse data for Wormedit $version\n";
136 # convert to an easily accessible hash
137 my @values = unpack Shiar_Parse::Nested->template(\@FORMAT).'a*', $input;
138 my $data = Shiar_Parse::Nested->convert(\@FORMAT, \@values);
139 warn "Trailing data left unparsed\n" if grep {length} @values;
140 $data->{format} = 'WormEdit';
145 package Shiar_Parse::WormyLevel;
150 use List::Util qw(sum min max);
153 my ($self, $input) = @_;
154 my ($psize, $ptype, $size, $type, $vsize, $dsize, $id, $subid) = unpack q{
155 x11 x42 # file signature and comment
156 S a2 S a2 # file size, type; data size, type
158 S S # var size; content size
162 or die "Not a calculator string, thus cannot be a Wormy level file\n";
164 or warn "File size ($size) does not correspond with data size ($psize)\n";
166 or die "Not a calculator string, thus cannot be a Wormy level file\n";
167 $size == $vsize and $vsize == $dsize+2
168 or warn "Mismatch in string data size declarations\n";
169 # substr($input, -2) eq $CHECKSUM
171 $input = substr $input, 73, -2;
173 or die "Wormy level identifier not found\n";
184 offset => [1, map {$_ => 'S'} @$_], # byte location of start
185 end => [1, map {$_ => 'C'} @$_],
187 [qw/single peaworm tron deathmatch foodmatch multifood timematch race ctf/]
189 theanswer => 'C', # 42
237 ref $_ and splice @$_, -2 for @{ $FORMAT[11] }; # only 8 moderefs
238 splice @FORMAT, 12, 2; # no reserved byte
241 die "Unsupported level version $subid\n";
245 my $data = Shiar_Parse::Nested->unpack(\@FORMAT, $input);
247 my $offsetbase = 0xF080 + @{ $data->{sprite} } + 1;
248 $data->{moderef}->{offset}->{single} == $offsetbase
249 or warn "First singleplayer level is not in front\n";
253 [qw'multi peaworm tron deathmatch foodmatch multifood timematch'],
258 $data->{levels} = [];
259 for my $modes (@VARMODES) {
260 my $variant = shift @$modes;
261 $offset = min(map { $data->{moderef}->{offset}->{$_} } @$modes) - $offsetbase;
262 my $amount = $variant eq 'single' ? 100 : max(map { $data->{moderef}->{end}->{$_} } @$modes);
264 my @varform = @LEVELFORM;
265 $varform[13]->[0] = $variant eq 'single' ? 1 : 4;
266 unshift @varform, name => 'Z*' unless $variant eq 'single';
267 $varform[-1]->[0] = 1 if $variant eq 'race';
268 $varform[-1]->[0] = 2 if $variant eq 'ctf';
270 while ($offset < length $data->{leveldata}) {
271 last if substr($data->{leveldata}, $offset, 1) eq chr(255);
273 # find references to this level offset, and set start number to matching modes
274 while (my ($mode, $location) = each %{ $data->{moderef}->{offset} }) {
275 $location == $offset + $offsetbase or next;
276 $data->{moderef}->{start}->{$mode} = 1 + scalar @{ $data->{levels} };
279 my $level = Shiar_Parse::Nested->unpack(
280 [@varform], substr $data->{leveldata}, $offset
282 my $size = 8 # unpack length (ugh, ugly recalculation)
283 + (defined $level->{name} ? 1 + length $level->{name} : 0)
284 + 3 * (ref $level->{worms} eq 'ARRAY' ? scalar @{$level->{worms}} : 1)
285 + 2 * ($level->{flags} ? ref $level->{flags} eq 'ARRAY' ? scalar @{$level->{flags}} : 1 : 0)
286 + ($level->{sprite} ? scalar @{$level->{sprite}} : 0)
287 + ($level->{balls} ? 3 * scalar @{$level->{balls}} : 0);
288 $level->{size} = $size;
289 $level->{offset} = $offset + $offsetbase;
291 # add objects until terminator
292 $level->{objects} = [];
293 while (my $object = ord substr($data->{leveldata}, $offset+$size, 1)) {
294 push @{ $level->{objects} }, Shiar_Parse::Nested->unpack(
295 [@OBJECTFORM], substr($data->{leveldata}, $offset+$size, 5)
300 # add parsed level and advance
301 push @{ $data->{levels} }, $level;
303 last if ++$data->{levelcount}->{$variant} >= $amount;
308 $data->{moderef}->{end}->{single} > 0, # singleplayer slot if any levels
309 $data->{moderef}->{end}->{peaworm}, # one for each peaworm arena
310 $data->{moderef}->{end}->{tron}, # idem for tron
312 $data->{hinames} = [ unpack '(x2a3)*', substr($data->{leveldata}, -5 * $slots) ];
313 $data->{enddata} = substr delete($data->{leveldata}), $offset, -5 * $slots; #XXX
314 $data->{format} = '86s';
319 package Shiar_Parse::Nested;
322 my ($self, $format) = @_;
323 # total (flattened) unpack template from nested format definitions
324 return join '', map {
325 my $value = $format->[-($_ << 1) - 1];
326 if (ref $value eq 'ARRAY') {
327 my $count = $value->[0];
328 $value = $self->template($value);
329 $value = $count =~ s/^([*\d]+)// ? "$count($value)$1"
330 : $count."X[$count]$count/($value)";
333 $value =~ s/^C(a)(\d+)/$1 . ($2 + 1)/e; # length prefix
336 } reverse 0 .. ($#$format - 1) >> 1;
340 my ($self, $format, $data) = @_;
341 # map flat results into a named and nested hash
343 while (my ($field, $template) = splice @$format, 0, 2) {
344 if (ref $template eq 'ARRAY') {
345 my ($count, @subformat) = @$template;
346 my $max = $count =~ s/^(\d+)// ? $1 : 0;
347 $count = !$count ? $max
348 : $count eq '*' ? $res{levelcount}->{total} : shift @$data;
349 $res{$field}->[$_] = $self->convert([@subformat], $data) for 0 .. ($max || $count)-1;
350 splice @{ $res{$field} }, $count if $max > $count;
351 $res{$field} = $res{$field}->[0] if $max == 1;
354 elsif ($template =~ /^Ca/) {
355 $data->[0] = CORE::unpack 'C/a', $data->[0];
357 $res{$field} = shift @$data;
363 my ($self, $format, $input) = @_;
364 my @data = CORE::unpack $self->template($format), $input;
365 return $self->convert($format, \@data);
371 my @OBJTYPE = ('none', 'line', 'fat line', 'bar', 'circle');
372 my @ENDTYPE = ('none', 'message', 'small message');
376 my @objtypes = map { $_->{type} } @$objects;
378 $count{$_}++ for @objtypes;
379 return (@objtypes > 1 && keys %count == 1 && 'all ') . join(', ',
380 map { $OBJTYPE[$_] ? $OBJTYPE[$_] . ($count{$_} > 1 && 's') : $_ }
385 # read and parse all input data
388 my $rawdata = readline;
389 if (substr($rawdata, 0, 11) eq "**TI86**\032\012\000") {
390 # compiled calculator file
391 $data = Shiar_Parse::WormyLevel->read($rawdata);
393 elsif (substr($rawdata, 0, 8) eq 'WormEdit') {
394 # original wormedit source
395 $data = Shiar_Parse::WormEdit->read($rawdata);
398 die "Unrecognised file type\n";
401 # output with user-preferred formatting
404 my $output = JSON::XS->new->ascii->canonical->pretty->allow_nonref;
405 print $output->encode($data), "\n";
409 print " ($data->{description})" if defined $data->{description};
411 printf "File version: %s\n", "$data->{format} v$data->{version}";
412 printf "Defaults: %s\n", join('; ',
413 'sprite ' . scalar @{ $data->{sprite} },
414 defined $data->{hiname} ? 'hiscore by ' . $data->{hiname} : (),
418 for my $variant (qw/single multi race ctf/) {
419 my $count = $data->{levelcount}->{$variant};
421 printf '%s (%s)', ucfirst $variant, $count // 'invalid';
424 for (0 .. $count - 1) {
425 my $level = $data->{levels}->[$_ + $startnr];
426 printf("\n- %-22s%4s:%3s+%2s%3s %3sx%-3s%s",
427 $level->{id} || $level->{name} || '#'.($_+1),
428 @$level{qw/size bsize growth/},
429 $variant eq 'single' && "x$level->{peas}",
430 @$level{qw/width height/},
431 join(';', map {" $_"} grep {$_}
432 @{$level->{objects}} && sprintf('%2d object%s (%s)',
433 scalar @{$level->{objects}}, @{$level->{objects}} != 1 && 's',
434 objsummary($level->{objects}),
436 $level->{sprite} && @{$level->{sprite}} && sprintf('sprite %d',
437 scalar @{$level->{sprite}},
439 $level->{balls} && @{$level->{balls}} && sprintf('%d bounc%s',
440 scalar @{$level->{balls}}, @{$level->{balls}} == 1 ? 'y' : 'ies',
449 printf("-- %-21s%4s: %s (%s)\n",
451 defined $data->{enddata} ? length $data->{enddata} : '?',
452 defined $data->{endtype} ? $ENDTYPE[$data->{endtype}] || 'unknown' : 'code',
453 $data->{endstr} // '?',
454 ) if $variant eq 'single';
462 parse-wormedit - Wormy level data parser
466 parse-wormedit [--raw] <input.lvl>
470 Reads Wormy levels (either original WormEdit source or compiled TI-86 string)
471 from STDIN or given file, and outputs contents, summarised or in full.
475 Mischa POSLAWSKY <wormy@shiar.org>