File Coverage

blib/lib/Games/EternalLands/Loader.pm
Criterion Covered Total %
statement 59 276 21.3
branch 0 76 0.0
condition 1 6 16.6
subroutine 16 39 41.0
pod 4 4 100.0
total 80 401 19.9


line stmt bran cond sub pod time code
1             package Games::EternalLands::Loader;
2              
3 1     1   21642 use 5.006;
  1         3  
  1         31  
4 1     1   4 use strict;
  1         2  
  1         39  
5 1     1   6 use warnings;
  1         6  
  1         33  
6 1     1   5 use vars qw($VERSION);
  1         1  
  1         90  
7              
8             $VERSION = '0.03';
9              
10             =head1 NAME
11              
12             Games::EternalLands::Loader - Access Eternal Lands content files
13              
14             =head1 SYNOPSIS
15              
16             use Games::EternalLands::Loader;
17              
18             my $loader = Games::EternalLands::Loader->new;
19             $loader->content_path('/usr/share/games/eternal-lands');
20            
21             use Data::Dumper;
22              
23             my $map = $loader->load('maps/startmap.elm');
24             print Dumper($map);
25              
26             my $entity = $loader->load('3dobjects/bag1.e3d');
27             print Dumper($entity);
28              
29             ...
30              
31             =head1 ABSTRACT
32              
33             This module reads binary content files for the online game Eternal Lands
34             and unpacks them into perl data structures.
35              
36             =cut
37              
38 1     1   6 use Carp qw(croak confess);
  1         2  
  1         63  
39 1     1   1460 use Convert::Binary::C;
  1         1580  
  1         31  
40 1     1   8 use Digest::MD5 qw(md5_hex);
  1         2  
  1         52  
41 1     1   928 use IO::File;
  1         10415  
  1         133  
42 1     1   5359 use IO::Uncompress::AnyUncompress;
  1         185278  
  1         59  
43 1     1   19 use File::Basename;
  1         4  
  1         86  
44 1     1   1195 use File::Spec::Functions;
  1         1150  
  1         108  
45 1     1   1298 use Games::EternalLands::Binary::Float16;
  1         924  
  1         54  
46 1     1   1309 use Games::EternalLands::Binary::Unitvec16;
  1         1162  
  1         80  
47              
48             use constant {
49 1         4170 _COMP_EXTS => [qw(gz zip bz2 Z xz)],
50             _MAP_SIG => 'elmf',
51             _ENT_SIG => 'e3dx',
52 1     1   21 };
  1         3  
53              
54             sub _fail {
55 0     0   0 my ($s, $m) = @_;
56 0         0 croak "_FAILED_" . $m;
57             }
58              
59             sub _setup_parser {
60 1     1   2 my ($s) = @_;
61 1         51 my $c = new Convert::Binary::C;
62 1         20 $c->configure(
63             ByteOrder => 'LittleEndian',
64             Alignment => 1,
65             CharSize => 1,
66             ShortSize => 2,
67             IntSize => 4,
68             FloatSize => 4,
69             );
70 1         971 $c->parse(<<'EOS');
71             typedef unsigned char byte;
72             typedef unsigned short ushort;
73             typedef unsigned int uint;
74             typedef ushort float16;
75             typedef ushort unitvec16;
76              
77             typedef struct {
78             char signature[4];
79             byte version[4];
80             byte md5_digest[16];
81             uint entity_data_offset;
82             } entity_header;
83              
84             typedef struct {
85             uint vertex_element_count;
86             uint vertex_element_size;
87             uint vertex_data_offset;
88             uint index_element_count;
89             uint index_element_size;
90             uint index_data_offset;
91             uint submesh_element_count;
92             uint submesh_element_size;
93             uint submesh_data_offset;
94             struct {
95             byte normals:1;
96             byte tangents:1;
97             byte twotextures:1;
98             byte colors:1;
99             byte :4;
100             } vertex_field_flags;
101             struct {
102             byte float16_positions:1;
103             byte float16_uv1s:1;
104             byte float16_uv2s:1;
105             byte quantized_unit_vectors:1;
106             byte :4;
107             } vertex_type_flags;
108             byte __unused[2];
109             } mesh_header;
110              
111             typedef struct {
112             char entity_name[80];
113             float position[3];
114             float rotation[3];
115             byte lighting_disabled;
116             byte blending_level;
117             byte __unused[2];
118             float color[3];
119             float scale;
120             byte __unused[20];
121             } mesh_object;
122              
123             typedef struct {
124             char entity_name[80];
125             float position[3];
126             float rotation[3];
127             byte __unused[24];
128             } quad_object;
129              
130             typedef struct {
131             float position[3];
132             float color[3];
133             byte __unused[16];
134             } light_object;
135              
136             typedef struct {
137             char entity_name[80];
138             float position[3];
139             byte __unused[12];
140             } fuzz_object;
141              
142             typedef struct {
143             char signature[4];
144             uint terrain_map_length;
145             uint terrain_map_breadth;
146             uint terrain_map_offset;
147             uint tile_map_offset;
148             uint mesh_object_size;
149             uint mesh_object_count;
150             uint mesh_data_offset;
151             uint quad_object_size;
152             uint quad_object_count;
153             uint quad_data_offset;
154             uint light_object_size;
155             uint light_object_count;
156             uint light_data_offset;
157             byte flag_indoors;
158             byte __unused[3];
159             float ambient_light[3];
160             uint fuzz_object_size;
161             uint fuzz_object_count;
162             uint fuzz_data_offset;
163             uint segment_data_offset;
164             byte __unused[36];
165             } map_header;
166             EOS
167 1         60 $c->tag($_, Format => 'String') for qw(
168             entity_header.signature
169             mesh_object.entity_name
170             quad_object.entity_name
171             fuzz_object.entity_name
172             map_header.signature
173             );
174             $c->tag('float16', Hooks => { unpack => sub {
175 0     0   0 Games::EternalLands::Binary::Float16::unpack_float16($_[0])
176 1         39 }});
177             $c->tag('unitvec16', Hooks => { unpack => sub {
178 0     0   0 Games::EternalLands::Binary::Unitvec16::unpack_unitvec16($_[0])
179 1         10 }});
180             $c->tag('entity_header.version', Hooks => { unpack => sub {
181 0     0   0 my $v = $_[0];
182 0         0 return $v->[0] * 1000 + $v->[1] * 100 + $v->[2] * 10 + $v->[3];
183 1         14 }});
184 1         5 $s->{parser} = $c;
185             }
186              
187             sub _locate {
188 0     0   0 my ($s, $n) = @_;
189 0         0 my $a = $s->{contpath};
190 0         0 for my $p (@$a) {
191 0         0 my $b = catfile $p, $n;
192 0 0       0 return $b if -e $b;
193 0         0 for my $e (_COMP_EXTS) {
194 0         0 my $t = "$b.$e";
195 0 0       0 return $t if -e $t;
196             }
197             }
198 0         0 $s->_fail("Failed to find '$n' in search path: @$a");
199             }
200              
201             sub _open {
202 0     0   0 my ($s, $n) = @_;
203 0         0 my $p = $s->_locate($n);
204 0         0 my $f = new IO::Uncompress::AnyUncompress $p;
205 0 0       0 $f = new IO::File $p if not $f;
206 0 0       0 $s->_fail("Failed to open '$p': $!") if not $f;
207 0         0 return $f;
208             }
209              
210             sub _parse_c {
211 0     0   0 my ($s, $x) = @_;
212 0         0 eval {
213 0         0 $s->{parser}->parse($x);
214             };
215 0 0       0 if ($@) {
216 0         0 confess "Failed to parse c code: $@\nCode:\n$x\n";
217             }
218             }
219              
220             sub _unp {
221 0     0   0 my ($s, $t, $d, $o, $x) = @_;
222 0         0 my $c = $s->{parser};
223 0 0       0 $o = 0 if not defined $o;
224 0 0       0 $s->_parse_c($x) if defined $x;
225 0         0 my $l = $c->sizeof($t);
226 0         0 my $r = length $d;
227 0 0       0 $s->_fail("Need to unpack $l bytes at offset $o,"
228             . " but data has length only $r")
229             if $o + $l > $r;
230 0         0 my $p = substr $d, $o, $l;
231 0         0 return $c->unpack($t, $p);
232             }
233              
234             sub _unpa {
235 0     0   0 my ($s, $t, $n, $d, $o) = @_;
236 0         0 my $i = "_${t}_array_$n";
237 0 0       0 my $x = $s->{parser}->def($i) ? undef : "typedef $t $i [$n];";
238 0         0 return $s->_unp($i, $d, $o, $x);
239             }
240              
241             sub _unpack_terrain {
242 0     0   0 my ($s, $m, $h, $d) = @_;
243              
244 0         0 my $rl = $h->{terrain_map_length};
245 0         0 my $rb = $h->{terrain_map_breadth};
246 0         0 my $rc = $rl * $rb;
247 0         0 my $ro = $h->{terrain_map_offset};
248 0         0 my $rm = $s->_unpa(byte => $rc, $d, $ro);
249 0         0 $m->{terrain_length} = $rl;
250 0         0 $m->{terrain_breadth} = $rb;
251 0         0 $m->{terrain_count} = $rc;
252 0         0 $m->{terrain_map} = $rm;
253              
254 0         0 my $tl = $rl * 6;
255 0         0 my $tb = $rb * 6;
256 0         0 my $tc = $tl * $tb;
257 0         0 my $to = $h->{tile_map_offset};
258 0         0 my $tm = $s->_unpa(byte => $tc, $d, $to);
259 0         0 $m->{tile_length} = $tl;
260 0         0 $m->{tile_breadth} = $tb;
261 0         0 $m->{tile_count} = $tc;
262 0         0 $m->{tile_map} = $tm;
263             }
264              
265             sub _unpack_objects {
266 0     0   0 my ($s, $k, $m, $h, $d) = @_;
267 0         0 my $kt = $k.'_object';
268 0         0 my $ka = $k.'_objects';
269 0         0 my $hs = $h->{$kt.'_size'};
270 0         0 my $ts = $s->{parser}->sizeof($kt);
271 0 0       0 $s->_fail("Size mismatch for '$kt': "
272             . "header has $hs, but type is $ts")
273             unless $hs == $ts;
274 0         0 my $kc = $h->{$kt.'_count'};
275 0         0 my $ko = $h->{$k.'_data_offset'};
276 0         0 $m->{$ka} = $s->_unpa($kt, $kc, $d, $ko);
277             }
278              
279             sub _assign_ids {
280 0     0   0 my ($s, $m) = @_;
281 0         0 for my $k (qw(mesh quad)) {
282 0         0 my $i = 0;
283 0         0 for my $o (@{$m->{$k."_objects"}}) {
  0         0  
284 0         0 $o->{id} = $i;
285 0         0 $i++;
286             }
287             }
288             }
289              
290             sub _load_map {
291 0     0   0 my ($s, $n) = @_;
292 0         0 my $f = $s->_open($n);
293 0         0 my $d = join '', <$f>;
294 0         0 my $h = $s->_unp(map_header => $d);
295 0         0 my $g = $h->{signature};
296 0 0       0 $s->_fail("Map '$n' has wrong file signature: \"$g\"")
297             if $g ne _MAP_SIG;
298 0         0 my $m = {
299             name => $n,
300             indoors => $h->{flag_indoors},
301             ambient_light => $h->{ambient_light},
302             };
303 0         0 $s->_unpack_terrain($m, $h, $d);
304 0         0 $s->_unpack_objects(mesh => $m, $h, $d);
305 0         0 $s->_unpack_objects(quad => $m, $h, $d);
306 0         0 $s->_assign_ids($m);
307 0         0 $s->_unpack_objects(light => $m, $h, $d);
308 0         0 $s->_unpack_objects(fuzz => $m, $h, $d);
309 0         0 return $m;
310             }
311              
312             sub _load_quad {
313 0     0   0 my ($s, $n) = @_;
314 0         0 my $f = $s->_open($n);
315 0         0 my %d = split(/\s*[:\n]\s*/, join('', <$f>));
316 0         0 my @v = qw(texture file_x_len file_y_len
317             x_size y_size u_start u_end v_start v_end);
318 0         0 for my $k (@v) {
319 0 0       0 $s->_fail("Missing expected field '$k' in '$n'")
320             unless exists $d{$k};
321             }
322 0         0 my $ix = $d{file_x_len};
323 0         0 my $iy = $d{file_y_len};
324 0         0 my $u0 = $d{u_start} / $ix;
325 0         0 my $v0 = $d{v_start} / $iy;
326 0         0 my $u1 = $d{u_end} / $ix;
327 0         0 my $v1 = $d{v_end} / $iy;
328 0         0 my $sx = $d{x_size};
329 0         0 my $sy = $d{y_size};
330 0         0 my $x0 = - $sx / 2.0;
331 0         0 my $y0 = - $sy / 2.0;
332 0         0 my $x1 = $x0 + $sx;
333 0         0 my $y1 = $y0 + $sy;
334 0         0 my $z = 0.001;
335 0         0 my $nr = [0.0, 0.0, 1.0];
336 0         0 my $e = {
337             texture_name => $d{texture},
338             vertices => [
339             { position => [$x0, $y0, $z], uv => [$u0, $v0], normal => $nr, },
340             { position => [$x0, $y1, $z], uv => [$u0, $v1], normal => $nr, },
341             { position => [$x1, $y1, $z], uv => [$u1, $v1], normal => $nr, },
342             { position => [$x1, $y0, $z], uv => [$u1, $v0], normal => $nr, },
343             ],
344             indices => [ 0, 1, 2, 0, 2, 3 ],
345             };
346 0         0 return $e;
347             }
348              
349             sub _check_header {
350 0     0   0 my ($s, $n, $h, $d) = @_;
351 0         0 my $es = $h->{signature};
352 0 0       0 $s->_fail("Wrong signature in entity file header for '$n': $es")
353             unless $es eq _ENT_SIG;
354 0         0 my $hc = join '', map { sprintf "%02x", $_ } @{$h->{md5_digest}};
  0         0  
  0         0  
355 0         0 my $fc = md5_hex(substr $d, $h->{entity_data_offset});
356 0 0       0 $s->_fail("Checksum mismatch for '$n':\n"
357             . " Expected from header: $hc\n"
358             . " Calculated from file: $fc\n")
359             unless $hc eq $fc;
360 0         0 my $v = $h->{version};
361 0 0 0     0 $s->_fail("Unrecognized entity file version for '$n': $v")
362             unless $v == 1000 or $v == 1100;
363             }
364              
365             sub _adjust_vertex_flags {
366 0     0   0 my ($s, $v, $h) = @_;
367 0         0 my $f = $h->{vertex_field_flags};
368 0         0 my $t = $h->{vertex_type_flags};
369 0 0       0 if ($v == 1000) {
370 0         0 $f->{$_} ^= 1 for keys %$f;
371 0         0 $f->{colors} = 0;
372 0         0 $t->{$_} = 0 for keys %$t;
373             }
374             }
375              
376             sub _make_vertex_type {
377 0     0   0 my ($s, $h) = @_;
378 0         0 my $f = $h->{vertex_field_flags};
379 0         0 my $t = $h->{vertex_type_flags};
380 0         0 my $g = join '_', (values %$f, values %$t);
381 0         0 my $v = "_vertex_type_$g";
382 0 0       0 return $v if $s->{parser}->def($v);
383              
384 0         0 my $f_nor = $f->{normals};
385 0         0 my $f_tan = $f->{tangents};
386 0         0 my $f_uv2 = $f->{twotextures};
387 0         0 my $f_col = $f->{colors};
388 0 0       0 my $t_pos = $t->{float16_positions} ? 'float16' : 'float';
389 0 0       0 my $t_uv1 = $t->{float16_uv1s} ? 'float16' : 'float';
390 0 0       0 my $t_uv2 = $t->{float16_uv2s} ? 'float16' : 'float';
391 0 0       0 my $t_vec = $t->{quantized_unit_vectors} ? 'unitvec16' : 'float';
392 0 0       0 my $a_vec = $t->{quantized_unit_vectors} ? 0 : 3;
393 0         0 my @r = (
394             [1, $t_uv1, 'uv', 2],
395             [$f_uv2, $t_uv2, 'uv2', 2],
396             [$f_nor, $t_vec, 'normal', $a_vec],
397             [$f_tan, $t_vec, 'tangent', $a_vec],
398             [1, $t_pos, 'position', 3],
399             [$f_col, 'byte', 'color', 4],
400             );
401 0         0 my $d = "typedef struct {\n";
402 0         0 for (@r) {
403 0         0 my ($e, $k, $n, $a) = @$_;
404 0 0       0 $d .= " $k $n ".($a?"[$a]":'').";\n" if $e;
    0          
405             }
406 0         0 $d .= "} $v;\n";
407 0         0 $s->_parse_c($d);
408 0         0 return $v;
409             }
410              
411             sub _make_submesh_type {
412 0     0   0 my ($s, $h) = @_;
413 0         0 my $c = $s->{parser};
414 0         0 my $f = $h->{vertex_field_flags};
415 0         0 my $x = $f->{twotextures};
416 0         0 my $t = "_submesh$x";
417 0 0       0 return $t if $c->def($t);
418 0         0 my $d = <<'EOS';
419             typedef struct {
420             uint texture_flags;
421             char texture_name[128];
422             float minimum_position[3];
423             float maximum_position[3];
424             uint minimum_vertex_index;
425             uint maximum_vertex_index;
426             uint index_element_offset;
427             uint index_element_count;
428             EOS
429 0 0       0 $d .= " char texture2_name[128];" if $x;
430 0         0 $d .= "} $t;\n";
431 0         0 $s->_parse_c($d);
432 0         0 my @n = ("$t.texture_name");
433 0 0       0 push @n, "$t.texture2_name" if $x;
434 0         0 $c->tag($_, Format => 'String') for @n;
435 0         0 return $t;
436             }
437              
438             sub _load_mesh {
439 0     0   0 my ($s, $n) = @_;
440 0         0 my $f = $s->_open($n);
441 0         0 my $d = join '', <$f>;
442 0         0 my $c = $s->{parser};
443 0         0 my $e = { name => $n };
444 0         0 my $eh = $s->_unp(entity_header => $d);
445 0         0 $s->_check_header($n, $eh, $d);
446              
447 0         0 my $mh = $s->_unp(mesh_header => $d, $eh->{entity_data_offset});
448 0         0 $s->_adjust_vertex_flags($eh->{version}, $mh);
449              
450 0         0 my $vt = $s->_make_vertex_type($mh);
451 0         0 my $ve = $c->sizeof($vt);
452 0         0 my $vs = $mh->{vertex_element_size};
453 0 0       0 $s->_fail("Unexpected vertex element size"
454             . " for mesh entity '$n': $vs (expected $ve)")
455             unless $ve == $vs;
456 0         0 my $vn = $mh->{vertex_element_count};
457 0         0 my $vo = $mh->{vertex_data_offset};
458 0         0 $e->{vertices} = $s->_unpa($vt, $vn, $d, $vo);
459              
460 0 0       0 my $it = $mh->{index_element_size} == 2 ? 'ushort' : 'uint';
461 0         0 my $in = $mh->{index_element_count};
462 0         0 my $io = $mh->{index_data_offset};
463 0         0 $e->{indices} = $s->_unpa($it, $in, $d, $io);
464              
465 0         0 my $st = $s->_make_submesh_type($mh);
466 0         0 my $se = $c->sizeof($st);
467 0         0 my $ss = $mh->{submesh_element_size};
468 0 0       0 $s->_fail("Unexpected submesh element size"
469             . " for mesh entity '$n': $ss (expected $se)")
470             unless $se == $ss;
471 0         0 my $sn = $mh->{submesh_element_count};
472 0         0 my $so = $mh->{submesh_data_offset};
473 0         0 $e->{submeshes} = $s->_unpa($st, $sn, $d, $so);
474              
475 0         0 return $e;
476             }
477              
478             =head1 METHODS
479              
480             =head2 new
481              
482             Creates a new Games::EternalLands::Loader object.
483              
484             =cut
485              
486             sub new {
487 1     1 1 261 my $proto = shift;
488 1   33     8 my $class = ref $proto || $proto;
489 1         4 my $s = bless {}, $class;
490 1         157 $s->{contpath} = [curdir];
491 1         10 $s->{loaders} = {
492             'elm' => \&_load_map,
493             'e3d' => \&_load_mesh,
494             '2d0' => \&_load_quad,
495             };
496 1         5 $s->{errstr} = '';
497 1         6 $s->_setup_parser;
498 1         5 return $s;
499             }
500              
501             =head2 content_path
502              
503             Sets the directory where the content files are located. The
504             argument may be a string to set a single path, or an array
505             reference to set multiple paths to be used in turn. If no
506             argument is given the current value (an array reference)
507             is returned.
508              
509             =cut
510              
511             sub content_path {
512 0     0 1   my ($s, $p) = @_;
513 0 0         return $s->{contpath} if not defined $p;
514 0 0         if (ref $p eq 'ARRAY') {
    0          
515 0           $s->{contpath} = [@$p];
516             } elsif (ref $p eq '') {
517 0           $s->{contpath} = [$p];
518             } else {
519 0           croak "Expecting a string or array reference.";
520             }
521             }
522              
523             sub _load {
524 0     0     my ($s, $n) = @_;
525 0           my $l;
526 0           for my $e (reverse split /\./, $n) {
527 0 0         last if $l = $s->{loaders}{$e};
528             }
529 0 0         $s->_fail("No loader found for '$n'") unless $l;
530 0           return $s->$l($n);
531             }
532              
533             =head2 load
534              
535             =over
536              
537             =item load NAME
538              
539             Finds, opens, reads and constructs the game asset identified by the
540             given name.
541              
542             In case there is some error opening the file or parsing its contents,
543             undef is returned. A description of the error can be retrieved using
544             the L method.
545              
546             See L for what exactly is returned in each case.
547              
548             =back
549              
550             =cut
551              
552             sub load {
553 0     0 1   my ($s, $n) = @_;
554 0 0         croak "Content name argument expected" if not defined $n;
555 0           my $e = eval { $s->_load($n); };
  0            
556 0 0         if ($@) {
557 0           my $x = $@;
558 0 0         if ($x =~ s/^_FAILED_//) {
559 0           chomp $x;
560 0           $s->{errstr} = $x;
561 0           return;
562             }
563 0           confess $x;
564             }
565 0           return $e;
566             }
567              
568             =head2 errstr
569              
570             Returns a string describing the last error that occurred.
571              
572             =cut
573              
574             sub errstr {
575 0     0 1   return $_[0]->{errstr};
576             }
577              
578             1;
579             __END__