82ae3deac712401936da29c833750324f518f0c2
[wormy.git] / lib / Games / Wormy / TICalcLevels.pm
1 package Games::Wormy::TICalcLevels;
2
3 use 5.010;
4 use strict;
5 use warnings;
6
7 use List::Util qw(sum min max);
8 use Data::Dumper;
9 use Parse::Binary::Nested qw(unpackf);
10
11 our $VERSION = '1.00';
12
13 sub read {
14         my ($self, $input, $override) = @_;
15         my ($psize, $ptype, $size, $type, $vsize, $dsize, $id, $version) = unpack q{
16                 x11 x42    # file signature and comment
17                 S a2 S a2  # file size, type; data size, type
18                 x8         # var name
19                 S S        # var size; content size
20                 CC         # wormy header
21         }, $input;
22         $ptype eq "\014\000"
23                 or die "Not a calculator string, thus cannot be a Wormy level file\n";
24         $size == $psize - 16
25                 or warn "File size ($size) does not correspond with data size ($psize)\n";
26         $type eq "\014\010"
27                 or die "Not a calculator string, thus cannot be a Wormy level file\n";
28         $size == $vsize and $vsize == $dsize+2
29                 or warn "Mismatch in string data size declarations\n";
30 #       substr($input, -2) eq $CHECKSUM
31
32         $input = substr $input, 73, -2;
33         $id eq ord 'w'
34                 or die "Wormy level identifier not found\n";
35
36         if ($override) {
37                 warn "Override version $version to $override\n";
38                 $version = $override;
39         }
40         elsif ($version == 95) {
41                 # level offset instead of description byte
42                 $version-- if (unpack('x2Z*x2xC', $input))[1] == 0xF4;
43                 warn "Ambiguous file version 95; guessing subversion $version\n";
44         }
45
46         my @FORMAT = (
47                 magic       => 'a1',
48                 version     => 'C',
49                 name        => 'Z*',
50                 description => 'Z*',
51                 levelcount  => [1,
52                         total  => 'S',
53                 ],
54                 moderef     => [1,
55                         map { (
56                                 offset => [1, map {$_ => 'S'} @$_], # byte location of start
57                                 end    => [1, map {$_ => 'C'} @$_],
58                         ) }
59                         [qw/single peaworm tron deathmatch foodmatch multifood timematch race ctf/]
60                 ],
61                 theanswer => 'x', # 42
62                 sprite     => ['C',
63                         line => 'B8',
64                 ],
65                 leveldata => 'a*',
66                 #levels
67                 #finish code
68                 #levels-multi
69                 #hinames
70         );
71         my @LEVELFORM = (
72                 peas       => 'C',
73                 delay      => 'C',
74                 growth     => 'C',
75                 bsize      => 'C',
76                 sprite     => ['C',
77                         line => 'B8',
78                 ],
79                 balls      => ['C',
80                         y   => 'C',
81                         x   => 'C',
82                         dir => 'C',
83                 ],
84                 worms      => [1,
85                         d => 'C',
86                         y => 'C',
87                         x => 'C',
88                 ],
89                 width      => 'C',
90                 height     => 'C',
91                 flags      => [0,
92                         y => 'C',
93                         x => 'C',
94                 ],
95                 objects    => ['?0',
96                         type => 'C',
97                         x1   => 'C',
98                         y1   => 'C',
99                         x2   => 'C',
100                         y2   => 'C',
101                 ],
102         );
103         my $offsetbase = 0xF080;
104
105         my @VARMODES = (
106                 [qw'single  single'],
107                 [qw'multi   peaworm tron deathmatch foodmatch multifood timematch'],
108                 [qw'race    race'],
109                 [qw'ctf     ctf'],
110         );
111
112         given ($version) {
113                 when (97) {
114                         # current @FORMAT
115                 }
116                         $offsetbase = 0xF400;
117                 when (96) {}
118                         ref $_ and splice(@$_, -8, 2) for @{ $FORMAT[11] }; # no multifood
119                         splice @FORMAT, 12, 2;  # no reserved byte
120                 when (95) {}
121                         splice @FORMAT, 6, 2;  # no description
122                 when (94) {}
123                 when (90) {
124                         $FORMAT[5] = 'C/a';  # length-preceding name
125                         splice @FORMAT, 10, 2;  # no default sprite
126                         ref $_ and do {
127                                 $_->[5] = $_->[7];  # no tron; deathmatch instead
128                                 $_->[7] = $_->[9];  # foodmatch instead
129                                 $_->[9] = 'linkmatch';  # replaces timematch
130                                 $_->[11] = $_->[13];  # race
131                                 $_->[13] = $_->[15];  # ctf
132                                 $_->[15] = 'domination';
133                         } for @{ $FORMAT[9] }; # no multifood
134                         splice @LEVELFORM, -2;
135                         push @LEVELFORM, "objects$_" => ['C',
136                                 type => "=$_",
137                                 map {$_ => 'C'} qw(x1 y1 x2 y2)
138                         ] for 2, 3;
139                         # peaworm/tron mode do not take multiplayer levels
140                         splice @VARMODES, 1, 0, ['peaworm', splice @{ $VARMODES[1] }, 1, 2];
141                 }
142                 default {
143                         die "Unsupported level version $version\n";
144                 }
145         }
146
147         my $data = unpackf(\@FORMAT, $input);
148         my $offset = 0;
149         $offsetbase += 1 + @{ $data->{sprite} } if $data->{sprite};
150         $data->{moderef}->{offset}->{single} == $offsetbase
151                 or warn "First singleplayer level is not in front\n";
152
153         my $slots = sum(grep {defined}
154                 $data->{moderef}->{end}->{single} > 0,  # singleplayer slot if any levels
155                 $data->{moderef}->{end}->{peaworm},     # one for each peaworm arena
156                 $data->{moderef}->{end}->{tron},        # idem for tron
157         );
158         $data->{hinames} = [ unpack '(x2a3)*', substr($data->{leveldata}, -5 * $slots) ];
159         $data->{format} = '86s';
160
161         $data->{levels} = [];
162         for my $modes (@VARMODES) {
163                 my $variant = shift @$modes;
164                 my @modeoffsets = grep {defined} #TODO: comment
165                         map { $data->{moderef}->{offset}->{$_} } @$modes;
166                 @modeoffsets or next;
167                 $data->{levelcount}->{$variant} = 0;
168                 $offset = min(grep {$_} @modeoffsets) or next;
169                 $offset -= $offsetbase;
170                 my $amount = $variant eq 'single' ? 100
171                         : max(grep {defined} map { $data->{moderef}->{end}->{$_} } @$modes);
172
173                 my @varform = @LEVELFORM;
174                 $varform[13]->[0] = $variant ~~ ['single', 'peaworm'] ? 1 : 4; # worms
175                 unshift @varform, name => 'Z*' unless $variant eq 'single' or $version <= 91;
176                 $varform[-3]->[0] = 1 if $variant eq 'race' and $version > 91;
177                 $varform[-3]->[0] = 2 if $variant eq 'ctf';
178                 push @varform, size => '=.';
179                 my $parselevel = Parse::Binary::Nested->new(\@varform);
180
181                 while ($offset < length $data->{leveldata}) {
182                         last if substr($data->{leveldata}, $offset, 1) eq chr(255);
183
184                         # find references to this level offset, and set start number to matching modes
185                         while (my ($mode, $location) = each %{ $data->{moderef}->{offset} }) {
186                                 $location == $offset + $offsetbase or next;
187                                 $data->{moderef}->{start}->{$mode} = 1 + scalar @{ $data->{levels} };
188                         }
189
190                         my $level = $parselevel->unpackf(substr $data->{leveldata}, $offset);
191                         $level->{offset} = $offset + $offsetbase;
192
193                         # add objects until terminator
194                         $level->{objects} = [];
195                 if ($version <= 91) {
196                         ref $_ eq 'ARRAY' and push @{ $level->{objects} }, @$_
197                                 for map { delete $level->{"objects$_"} } 2, 3;
198                 }
199
200                         # add parsed level and advance
201                         push @{ $data->{levels} }, $level;
202                         $offset += $level->{size};
203                         last if ++$data->{levelcount}->{$variant} >= $amount;
204                 }
205
206                 if ($variant eq 'single') {
207                         $offset++;
208                         $data->{finish}->{code} =
209                         my $code = substr $data->{leveldata}, $offset, -5*$slots;
210
211                         my %FINISHCODE = (
212                                 0 => chr 0xC9, # ret
213                                 1 => join('',
214                                         chr 0x21,  # ld hl, MESSAGE
215                                         pack('v', $offsetbase + $offset + 9),
216                                         (map {chr}
217                                                 0xCD, 0x37, 0x4A,  # call _puts
218                                                 0xC3, 0xAA, 0x55,  # jp _getkey
219                                         ),
220                                 ),
221                                 2 => join('',
222                                         (map {chr}
223                                                 0x21, 0, 0x1C,  # ld hl, $POS
224                                                 0x22, 0x7C, 0xC3, # ld (_penCol), hl
225                                                 0x21,  # ld hl, MESSAGE
226                                         ),
227                                         pack('v', $offsetbase + $offset + 15),
228                                         (map {chr}
229                                                 0xCD, 0xA5, 0x4A, # call _vputs
230                                                 0xC3, 0xAA, 0x55, # jp _getkey
231                                         ),
232                                 ),
233                         );
234                         while (my ($finish, $match) = each %FINISHCODE) {
235                                 $match eq substr $code, 0, length $match or next;
236                                 $data->{finish}->{type} = $finish and
237                                 $data->{finish}->{message} = unpack 'Z*', substr($code, length $match);
238                                 last;
239                         }
240                 }
241         }
242
243         return $data;
244 }
245
246 1;
247
248 __END__
249
250 =head1 NAME
251
252 Games::Wormy::TICalcLevels - Read Wormy levelset from a compiled TI-86 file
253
254 =head1 SYNOPSIS
255
256         my $levelset = Games::Wormy::TICalcLevels->read($filecontents);
257         print $levelset->{name};
258
259 =head1 AUTHOR
260
261 Mischa POSLAWSKY <perl@shiar.org>
262
263 =head1 LICENSE
264
265 LGPL version 3.
266