File Coverage

blib/lib/Game/Asset.pm
Criterion Covered Total %
statement 81 85 95.2
branch 11 18 61.1
condition 2 2 100.0
subroutine 19 19 100.0
pod 2 2 100.0
total 115 126 91.2


line stmt bran cond sub pod time code
1             # Copyright (c) 2016 Timm Murray
2             # All rights reserved.
3             #
4             # Redistribution and use in source and binary forms, with or without
5             # modification, are permitted provided that the following conditions are met:
6             #
7             # * Redistributions of source code must retain the above copyright notice,
8             # this list of conditions and the following disclaimer.
9             # * Redistributions in binary form must reproduce the above copyright
10             # notice, this list of conditions and the following disclaimer in the
11             # documentation and/or other materials provided with the distribution.
12             #
13             # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
14             # AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
15             # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
16             # ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
17             # LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18             # CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19             # SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20             # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21             # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22             # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
23             # POSSIBILITY OF SUCH DAMAGE.
24             package Game::Asset;
25             $Game::Asset::VERSION = '0.6';
26             # ABSTRACT: Load assets (images, music, etc.) for games
27 8     8   136477 use v5.010;
  8         34  
28 8     8   54 use strict;
  8         21  
  8         212  
29 8     8   67 use warnings;
  8         30  
  8         264  
30 8     8   3678 use Moose;
  8         3404770  
  8         61  
31 8     8   67214 use namespace::autoclean;
  8         50342  
  8         47  
32              
33 8     8   3572 use Game::Asset::Type;
  8         24  
  8         254  
34 8     8   2913 use Game::Asset::Null;
  8         36  
  8         354  
35 8     8   3661 use Game::Asset::PerlModule;
  8         30  
  8         274  
36 8     8   3374 use Game::Asset::PlainText;
  8         28  
  8         262  
37 8     8   3149 use Game::Asset::YAML;
  8         34  
  8         278  
38 8     8   3514 use Game::Asset::MultiExample;
  8         32  
  8         322  
39              
40 8     8   5586 use Archive::Zip qw( :ERROR_CODES );
  8         686341  
  8         871  
41 8     8   76 use YAML ();
  8         20  
  8         6811  
42              
43              
44             has 'file' => (
45             is => 'ro',
46             isa => 'Str',
47             required => 1,
48             );
49             has 'mappings' => (
50             is => 'ro',
51             isa => 'HashRef[Game::Asset::Type]',
52             default => sub {{}},
53             auto_deref => 1,
54             );
55             has 'entries' => (
56             is => 'ro',
57             isa => 'ArrayRef[Game::Asset::Type]',
58             default => sub {[]},
59             auto_deref => 1,
60             );
61             has '_entries_by_shortname' => (
62             traits => ['Hash'],
63             is => 'ro',
64             isa => 'HashRef[Game::Asset::Type]',
65             default => sub {{}},
66             handles => {
67             _get_by_name => 'get',
68             },
69             );
70             has '_zip' => (
71             is => 'ro',
72             isa => 'Archive::Zip',
73             );
74              
75              
76             sub BUILDARGS
77             {
78 7     7 1 25 my ($class, $args) = @_;
79 7         23 my $file = $args->{file};
80              
81 7         29 my $zip = $class->_read_zip( $file );
82 7         25 $args->{'_zip'} = $zip;
83              
84 7   100     48 my $index = $class->_read_index( $zip, $file ) // {};
85             $args->{mappings} = {
86 7         76 yml => 'Game::Asset::YAML',
87             txt => 'Game::Asset::PlainText',
88             pm => 'Game::Asset::PerlModule',
89             %$index,
90             };
91              
92             my ($entries, $entries_by_shortname) = $class->_build_entries( $zip,
93 7         67 $args->{mappings} );
94 7         24 $args->{entries} = $entries;
95 7         20 $args->{'_entries_by_shortname'} = $entries_by_shortname;
96              
97 7         237 return $args;
98             }
99              
100              
101             sub get_by_name
102             {
103 6     6 1 56 my ($self, $name, @args) = @_;
104 6         247 my $entry = $self->_get_by_name( $name );
105              
106 6 50       27 if( $entry ) {
107 6         213 my $full_name = $entry->full_name;
108 6         181 my $contents = $self->_zip->contents( $full_name );
109 6         5950 $entry->process_content( $contents, @args );
110             }
111              
112 6         34 return $entry;
113             }
114              
115              
116             sub _read_zip
117             {
118 7     7   21 my ($class, $file) = @_;
119              
120 7         67 my $zip = Archive::Zip->new;
121 7         309 my $read_result = $zip->read( $file );
122 7 50       16757 if( $read_result == AZ_STREAM_END ) {
    50          
    50          
    50          
123 0         0 die "Hit end of stream unexpectedly in '$file'\n";
124             }
125             elsif( $read_result == AZ_ERROR ) {
126 0         0 die "Generic error while reading '$file'\n";
127             }
128             elsif( $read_result == AZ_FORMAT_ERROR ) {
129 0         0 die "Formatting error while reading '$file'\n";
130             }
131             elsif( $read_result == AZ_IO_ERROR ) {
132 0         0 die "IO error while reading '$file'\n";
133             }
134              
135 7         29 return $zip;
136             }
137              
138             sub _read_index
139             {
140 7     7   28 my ($class, $zip, $file) = @_;
141 7         46 my $index_contents = $zip->contents( 'index.yml' );
142 7 50       9106 die "Could not find index.yml in '$file'\n" unless $index_contents;
143              
144 7         48 my $index = YAML::Load( $index_contents );
145 7         97465 return $index;
146             }
147              
148             sub _build_entries
149             {
150 7     7   29 my ($class, $zip, $mappings) = @_;
151 7         59 my %mappings = %$mappings;
152              
153 7         26 my (@entries, %entries_by_shortname);
154 7         64 foreach my $member ($zip->memberNames) {
155 44 100       818 next if $member eq 'index.yml'; # Ignore index
156 37 100       166 next if $member =~ m!\/ \z!x;
157 31         196 my ($short_name, $ext) = $member =~ /\A (.*) \. (.*?) \z/x;
158             die "Could not find mapping for '$ext' (full name: $member)\n"
159 31 50       135 if ! exists $mappings{$ext};
160              
161 31         72 my $entry_class = $mappings{$ext};
162 31         1230 my $entry = $entry_class->new({
163             name => $short_name,
164             full_name => $member,
165             });
166 31         97 push @entries, $entry;
167 31         108 $entries_by_shortname{$short_name} = $entry;
168             }
169              
170 7         46 return (\@entries, \%entries_by_shortname);
171             }
172              
173              
174              
175 8     8   76 no Moose;
  8         24  
  8         98  
176             __PACKAGE__->meta->make_immutable;
177             1;
178             __END__
179              
180              
181             =head1 NAME
182              
183             Game::Asset - Load assets (images, music, etc.) for games
184              
185             =head1 SYNOPSIS
186              
187             my $asset = Game::Asset->new({
188             file => 't_data/test1.zip',
189             });
190             my $foo = $asset->get_by_name( 'foo' );
191             my $name = $foo->name;
192             my $type = $foo->type;
193              
194             =head1 DESCRIPTION
195              
196             A common way to handle game assets is to load them in one big zip file. It
197             might end up named with extensions like ".wad" or ".pk3" or even ".jar", but
198             it's a zip file.
199              
200             This module allows you to load up these files and fetch their contents into
201             Perl objects. Each type of file is represented by a class that does the
202             L<Game::Asset::Type> Moose role. Which type class, exactly, is determined with
203             mappings defined in the C<index.yml> file. There are also a few built-in
204             mappings.
205              
206             =head1 THE INDEX FILE
207              
208             A file named C<index.yml> (a L<YAML> file) is required inside the zip file,
209             and resolves to a hash. Keys are the file extensions (without the dot), and
210             values are the Perl class that will handle that type. That class must do
211             the L<Game::Asset::Type> Moose role.
212              
213             The file must exist. If you just want to use the built-in mappings, it can
214             resolve to an empty hash.
215              
216             =head2 Built-in Mappings
217              
218             The following mappings are always available without being in the index file:
219              
220             =over 4
221              
222             =item * txt -- L<Game::Asset::PlainText>
223              
224             =item * yml -- L<Game::Asset::YAML>
225              
226             =item * pm -- L<Game::Asset::PerlModule>
227              
228             =back
229              
230             =head2 Multi-mappings
231              
232             There are times when the given content should be processed by more than one
233             mapping. For instance, a game may want to process a L<Graphics::GVG> vector
234             in both OpenGL and Chipmunk (physics library).
235              
236             This is what multi-mappings are for. See L<Game::Asset::Multi> for details.
237              
238             =head1 ATTRIBUTES
239              
240             =head2 file
241              
242             The path to the zip file.
243              
244             =head2 mappings
245              
246             A hashref (with autoderef). The keys are the file extensions, and the values
247             are the L<Game::Asset::Type> classes that will handle that type.
248              
249             =head2 entries
250              
251             A list of all the assets with their file extensions removed. Note that the
252             C<index.yml> file is filtered out.
253              
254             =head1 METHODS
255              
256             =head2 get_by_name
257              
258             $asset->get_by_name( 'foo' );
259              
260             Pass in a name of an asset (without the extension). Returns an object
261             representing the data in the zip file.
262              
263             Any other arguments passed will be passed to the handling class.
264              
265             =head1 LICENSE
266              
267             Copyright (c) 2016 Timm Murray
268             All rights reserved.
269              
270             Redistribution and use in source and binary forms, with or without
271             modification, are permitted provided that the following conditions are met:
272              
273             * Redistributions of source code must retain the above copyright notice,
274             this list of conditions and the following disclaimer.
275             * Redistributions in binary form must reproduce the above copyright
276             notice, this list of conditions and the following disclaimer in the
277             documentation and/or other materials provided with the distribution.
278              
279             THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
280             AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
281             IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
282             ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
283             LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
284             CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
285             SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
286             INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
287             CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
288             ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
289             POSSIBILITY OF SUCH DAMAGE.
290              
291             =cut