File Coverage

blib/lib/Games/Lacuna/Task/Role/Stars.pm
Criterion Covered Total %
statement 4 6 66.6
branch n/a
condition n/a
subroutine 2 2 100.0
pod n/a
total 6 8 75.0


line stmt bran cond sub pod time code
1             package Games::Lacuna::Task::Role::Stars;
2              
3 1     1   1505 use 5.010;
  1         3  
  1         54  
4             our $VERSION = $Games::Lacuna::Task::VERSION;
5              
6 1     1   481 use Moose::Role;
  0            
  0            
7              
8             use List::Util qw(max min);
9              
10             use Games::Lacuna::Task::Utils qw(normalize_name distance);
11              
12             use LWP::Simple;
13             use Text::CSV;
14              
15              
16              
17             after 'BUILD' => sub {
18             my ($self) = @_;
19            
20             my ($star_count) = $self->client->storage_selectrow_array('SELECT COUNT(1) FROM star');
21            
22             if ($star_count == 0) {
23             $self->fetch_all_stars(0);
24             }
25             };
26              
27             sub fetch_all_stars {
28             my ($self,$check) = @_;
29            
30             $check //= 1;
31              
32             my $server = $self->client->client->uri;
33             return
34             unless $server =~ /^https?:\/\/([^.]+)\./;
35            
36             # Fetch starmap from server
37             my $starmap_uri = 'http://'.$1.'.lacunaexpanse.com.s3.amazonaws.com/stars.csv';
38            
39             $self->log('info',"Fetching star map from %s. This will only happen once and might take a while.",$starmap_uri);
40             my $content = get($starmap_uri);
41            
42             # Create temp table
43             $self->storage_do('CREATE TEMPORARY TABLE temporary_star (id INTEGER NOT NULL)');
44            
45             # Prepare sql statements
46             my $sth_check = $self->storage_prepare('SELECT last_checked, is_probed, is_known FROM star WHERE id = ?');
47             my $sth_insert = $self->storage_prepare('INSERT INTO star (id,x,y,name,zone,last_checked,is_probed,is_known) VALUES (?,?,?,?,?,?,?,?)');
48             my $sth_temp = $self->storage_prepare('INSERT INTO temporary_star (id) VALUES (?)');
49            
50             # Parse star map
51             $self->log('debug',"Parsing new star map");
52             my $csv = Text::CSV->new();
53             open my $fh, "<:encoding(utf8)", \$content;
54             $csv->column_names( $csv->getline($fh) );
55            
56             # Process star map
57             my $count = 0;
58             while( my $row = $csv->getline_hr( $fh ) ){
59             $count++;
60             my ($last_checked,$is_probed,$is_known);
61             if ($check) {
62             $sth_check->execute($row->{id});
63             ($last_checked,$is_probed,$is_known) = $sth_check->fetchrow_array();
64             $sth_check->finish();
65             }
66              
67             $sth_temp->execute($row->{id});
68            
69             $sth_insert->execute(
70             $row->{id},
71             $row->{x},
72             $row->{y},
73             $row->{name},
74             $row->{zone},
75             $last_checked,
76             $is_probed,
77             $is_known
78             );
79            
80             $self->log('debug',"Importing %i stars",$count)
81             if $count % 500 == 0;
82             }
83             $self->log('debug',"Finished imporing %i stars",$count);
84            
85             # Cleanup star table
86             $self->storage_do('DELETE FROM star WHERE id NOT IN (SELECT id FROM temporary_star)');
87             $self->storage_do('DELETE FROM body WHERE star NOT IN (SELECT id FROM star)');
88             $self->storage_do('DROP TABLE temporary_star');
89            
90             return;
91             }
92              
93             sub _get_body_cache_for_star {
94             my ($self,$star_data) = @_;
95            
96             my $star_id = $star_data->{id};
97            
98             return
99             unless defined $star_id;
100            
101             my $sth = $self->storage_prepare('SELECT
102             body.id,
103             body.star,
104             body.x,
105             body.y,
106             body.orbit,
107             body.size,
108             body.name,
109             body.type,
110             body.water,
111             body.ore,
112             body.empire,
113             body.is_excavated,
114             empire.id AS empire_id,
115             empire.name AS empire_name,
116             empire.alignment AS empire_alignment,
117             empire.is_isolationist AS empire_is_isolationist
118             FROM body
119             LEFT JOIN empire ON (empire.id = body.empire)
120             WHERE body.star = ?'
121             );
122            
123             $sth->execute($star_data->{id});
124            
125             my @bodies;
126             while (my $body = $sth->fetchrow_hashref) {
127             push (@bodies,$self->_inflate_body($body,$star_data));
128             }
129            
130             return @bodies;
131             }
132              
133             sub _get_star_cache {
134             my ($self,$query,@params) = @_;
135            
136             return
137             unless defined $query;
138            
139             # Get star from cache
140             my $star_cache = $self->client->storage_selectrow_hashref('SELECT
141             star.id,
142             star.x,
143             star.y,
144             star.name,
145             star.zone,
146             star.last_checked,
147             star.is_probed,
148             star.is_known
149             FROM star
150             WHERE '.$query,
151             @params
152             );
153            
154             return
155             unless defined $star_cache;
156            
157             return $self->_inflate_star($star_cache)
158             }
159              
160             sub _inflate_star {
161             my ($self,$star_cache) = @_;
162            
163             # Build star data
164             my $star_data = {
165             (map { $_ => $star_cache->{$_} } qw(id x y zone name is_probed is_known last_checked)),
166             cache_ok => 0,
167             };
168            
169             # Star was not checked yet
170             return $star_data
171             unless defined $star_cache->{last_checked};
172            
173             # Get cache status
174             $star_data->{cache_ok} = ($star_cache->{last_checked} > (time() - $Games::Lacuna::Task::Constants::MAX_STAR_CACHE_AGE)) ? 1:0;
175            
176             # We have no bodies
177             return $star_data
178             if defined $star_data->{is_known} && $star_data->{is_known} == 0;
179            
180             # Get Bodies from cache
181             my @bodies = $self->_get_body_cache_for_star($star_data);
182            
183             # Bodies ok
184             if (scalar @bodies) {
185             $star_data->{bodies} = \@bodies
186             # Bodies missing
187             } else {
188             $self->log('warn','Inconsitent cache state for star %i',$star_data->{id});
189             $star_data = $self->_get_star_api($star_data->{id},$star_data->{x},$star_data->{y});
190             }
191            
192             return $star_data;
193             }
194              
195             sub _inflate_body {
196             my ($self,$body,$star) = @_;
197            
198             return
199             unless defined $body;
200            
201             my $star_data;
202            
203             if (defined $star) {
204             $star_data = {
205             star_id => $star->{id},
206             star_name => $star->{name},
207             zone => $star->{zone},
208             };
209             } else {
210             $star_data = {
211             star_id => $body->{star_id},
212             star_name => $body->{star_name},
213             zone => $body->{zone},
214             };
215             }
216            
217             my $body_data = {
218             (map { $_ => $body->{$_} } qw(id x y orbit name type water size is_excavated)),
219             ore => $Games::Lacuna::Task::Storage::JSON->decode($body->{ore}),
220             %{$star_data},
221             };
222            
223             if ($body->{empire_id}) {
224             $body_data->{empire} = {
225             alignment => $body->{empire_alignment},
226             is_isolationist => $body->{empire_is_isolationist},
227             name => $body->{empire_name},
228             id => $body->{empire_id},
229             };
230             }
231              
232             return $body_data;
233             }
234              
235             sub _get_body_cache {
236             my ($self,$query,@params) = @_;
237            
238             return
239             unless defined $query;
240            
241             my $body = $self->client->storage_selectrow_hashref('SELECT
242             body.id,
243             body.star,
244             body.x,
245             body.y,
246             body.orbit,
247             body.size,
248             body.name,
249             body.type,
250             body.water,
251             body.ore,
252             body.empire,
253             body.is_excavated,
254             star.id AS star_id,
255             star.name AS star_name,
256             star.zone AS zone,
257             star.last_checked,
258             star.is_probed,
259             star.is_known,
260             empire.id AS empire_id,
261             empire.name AS empire_name,
262             empire.alignment AS empire_alignment,
263             empire.is_isolationist AS empire_is_isolationist
264             FROM body
265             INNER JOIN star ON (star.id = body.star)
266             LEFT JOIN empire ON (empire.id = body.empire)
267             WHERE '.$query,
268             @params
269             );
270            
271             return $self->_inflate_body($body);
272             }
273              
274             sub get_body_by_id {
275             my ($self,$id) = @_;
276            
277             return
278             unless defined $id
279             && $id =~ m/^\d+$/;
280            
281             return $self->_get_body_cache('body.id = ?',$id);
282             }
283              
284             sub get_body_by_name {
285             my ($self,$name) = @_;
286            
287             return
288             unless defined $name;
289            
290             my $body_data = $self->_get_body_cache('body.name = ?',$name);
291            
292             return $body_data
293             if $body_data;
294            
295             return $self->_get_body_cache('body.normalized_name = ?',normalize_name($name));
296             }
297              
298             sub get_body_by_xy {
299             my ($self,$x,$y) = @_;
300            
301             return
302             unless defined $x
303             && defined $y
304             && $x =~ m/^-?\d+$/
305             && $y =~ m/^-?\d+$/;
306            
307             return $self->_get_body_cache('body.x = ? AND body.y = ?',$x,$y);
308            
309             # my ($star_data) = $self->list_stars(
310             # x => $x,
311             # y => $y,
312             # limit => 1,
313             # distance=> 1,
314             # );
315             #
316             # return
317             # unless defined $star_data
318             # && defined $star_data->{bodies};
319             #
320             # foreach my $body_data (@{$star_data->{bodies}}) {
321             # return $body_data
322             # if $body_data->{x} == $x
323             # && $body_data->{y} == $y;
324             # }
325            
326             return;
327             }
328              
329             sub _find_star {
330             my ($self,$query,@params) = @_;
331            
332             return
333             unless defined $query;
334            
335             # Query starmap/cache
336             my $star_data = $self->_get_star_cache($query,@params);
337            
338             # No hit for query
339             return
340             unless $star_data;
341            
342             # Cache is valid
343             return $star_data
344             if $star_data->{cache_ok};
345             return $self->_get_star_api($star_data->{id},$star_data->{x},$star_data->{y});
346             }
347              
348             sub _get_star_api {
349             my ($self,$star_id,$x,$y) = @_;
350            
351             my $step = int($Games::Lacuna::Task::Constants::MAX_MAP_QUERY / 2);
352            
353             # Fetch x and y unless given
354             unless (defined $x && defined $y) {
355             ($x,$y) = $self->client->storage_selectrow_array('SELECT x,y FROM star WHERE id = ?',$star_id);
356             }
357            
358             return
359             unless defined $x && defined $y;
360            
361             # Get area
362             my $min_x = $x - $step;
363             my $min_y = $y - $step;
364             my $max_x = $x + $step;
365             my $max_y = $y + $step;
366            
367             # Get star from api
368             my $star_list = $self->_get_area_api_by_xy($min_x,$min_y,$max_x,$max_y);
369            
370             # Find star in list
371             my $star_data;
372             foreach my $element (@{$star_list}) {
373             if ($element->{id} == $star_id) {
374             $star_data = $element;
375             last;
376             }
377             }
378            
379             # Get bodies from cache, even if system is not probed
380             if (! defined $star_data->{bodies}
381             && $star_data->{is_known} == 1) {
382            
383             my @bodies = $self->_get_body_cache_for_star($star_data);
384             if (scalar @bodies) {
385             $star_data->{bodies} = \@bodies;
386             } else {
387             $self->log('warn','Inconsitent cache state for star %i',$star_data->{id});
388             $self->storage_do('UPDATE star SET is_known = ?, is_probed = 0 WHERE id = ?',0,0,$star_data->{id});
389             }
390             }
391            
392            
393             return $star_data;
394             }
395              
396              
397             sub get_star_by_name {
398             my ($self,$name) = @_;
399            
400             return
401             unless defined $name;
402            
403             return $self->_find_star('star.name = ?',$name);
404             }
405              
406             sub get_star_by_xy {
407             my ($self,$x,$y) = @_;
408            
409             return
410             unless defined $x
411             && defined $y
412             && $x =~ m/^-?\d+$/
413             && $y =~ m/^-?\d+$/;
414            
415             return $self->_find_star('star.x = ? AND star.y = ?',$x,$y);
416             }
417              
418             sub get_star {
419             my ($self,$star_id) = @_;
420            
421             return
422             unless defined $star_id && $star_id =~ m/^\d+$/;
423            
424             return $self->_find_star('star.id = ?',$star_id);
425             }
426              
427             sub _get_area_api_by_xy {
428             my ($self,$min_x,$min_y,$max_x,$max_y) = @_;
429            
430             my $bounds = $self->get_stash('star_map_size');
431             return
432             if $bounds->{x}[0] >= $max_x || $bounds->{x}[1] <= $min_x;
433             return
434             if $bounds->{y}[0] >= $max_y || $bounds->{y}[1] <= $min_y;
435            
436             $min_x = max($min_x,$bounds->{x}[0]);
437             $max_x = min($max_x,$bounds->{x}[1]);
438             $min_y = max($min_y,$bounds->{y}[0]);
439             $max_y = min($max_y,$bounds->{y}[1]);
440            
441             # Fetch from api
442             my $star_info = $self->request(
443             object => $self->build_object('Map'),
444             params => [ $min_x,$min_y,$max_x,$max_y ],
445             method => 'get_stars',
446             );
447            
448             # Loop all stars in area
449             my @return;
450             foreach my $star_data (@{$star_info->{stars}}) {
451             $self->set_star_cache($star_data);
452             push(@return,$star_data);
453             }
454            
455             return \@return;
456             }
457              
458             sub set_star_cache {
459             my ($self,$star_data) = @_;
460            
461             my $star_id = $star_data->{id};
462            
463             return
464             unless defined $star_id;
465            
466             delete $star_data->{bodies}
467             if (defined $star_data->{bodies} && scalar @{$star_data->{bodies}} == 0);
468             $star_data->{last_checked} ||= time();
469             $star_data->{cache_ok} //= 1;
470             $star_data->{is_probed} //= (defined $star_data->{bodies} ? 1:0);
471             $star_data->{is_known} //= 1
472             if $star_data->{is_probed};
473            
474             unless (defined $star_data->{is_known}) {
475             ($star_data->{is_known}) = $self->client->storage_selectrow_array('SELECT COUNT(1) FROM body WHERE star = ?',$star_id);
476             }
477            
478             # Update star cache
479             $self->storage_do(
480             'UPDATE star SET is_probed = ?, is_known = ?, last_checked = ?, name = ? WHERE id = ?',
481             $star_data->{is_probed},
482             $star_data->{is_known},
483             $star_data->{last_checked},
484             $star_data->{name},
485             $star_id
486             );
487              
488             return
489             unless defined $star_data->{bodies};
490            
491             $self->_set_star_cache_bodies($star_data);
492             }
493              
494             sub _set_star_cache_bodies {
495             my ($self,$star_data) = @_;
496            
497             my $star_id = $star_data->{id};
498            
499             # Get excavate status
500             my %is_excavated;
501             my $sth_excavate = $self->storage_prepare('SELECT id,is_excavated FROM body WHERE star = ? AND is_excavated IS NOT NULL');
502             $sth_excavate->execute($star_id);
503             while (my ($body_id,$is_excavated) = $sth_excavate->fetchrow_array) {
504             $is_excavated{$body_id} = $is_excavated;
505             }
506            
507             # Remove all bodies
508             $self->storage_do('DELETE FROM body WHERE star = ?',$star_id);
509            
510             # Insert or update empire
511             my $sth_empire = $self->storage_prepare('INSERT OR REPLACE INTO empire
512             (id,name,normalized_name,alignment,is_isolationist)
513             VALUES
514             (?,?,?,?,?)');
515            
516             # Insert new bodies
517             my $sth_insert = $self->storage_prepare('INSERT INTO body
518             (id,star,x,y,orbit,size,name,normalized_name,type,water,ore,empire,is_excavated)
519             VALUES
520             (?,?,?,?,?,?,?,?,?,?,?,?,?)');
521            
522             # Cache bodies
523             foreach my $body_data (@{$star_data->{bodies}}) {
524             my $empire = $body_data->{empire} || {};
525            
526             $body_data->{is_excavated} = $is_excavated{$body_data->{id}};
527            
528             my $ore = $body_data->{ore};
529             $ore = $Games::Lacuna::Task::Storage::JSON->encode($ore)
530             if ref $ore eq 'HASH';
531            
532             $sth_insert->execute(
533             $body_data->{id},
534             $star_id,
535             $body_data->{x},
536             $body_data->{y},
537             $body_data->{orbit},
538             $body_data->{size},
539             $body_data->{name},
540             normalize_name($body_data->{name}),
541             $body_data->{type},
542             $body_data->{water},
543             $ore,
544             $empire->{id},
545             $body_data->{is_excavated},
546             );
547            
548             if (defined $empire->{id}) {
549             $sth_empire->execute(
550             $empire->{id},
551             $empire->{name},
552             normalize_name($empire->{name}),
553             $empire->{alignment},
554             $empire->{is_isolationist},
555             );
556             }
557             }
558             }
559              
560             sub search_stars_callback {
561             my ($self,$callback,%params) = @_;
562            
563             my @sql_where;
564             my @sql_params;
565             my @sql_extra;
566             my @sql_fields = qw(star.id star.x star.y star.name star.zone star.last_checked star.is_probed star.is_known);
567            
568             # Order by distance
569             if (defined $params{distance}
570             && defined $params{x}
571             && defined $params{y}) {
572             push(@sql_fields,'distance_func(star.x,star.y,?,?) AS distance');
573             push(@sql_params,$params{x}+0,$params{y}+0);
574             # Does not seem to work for some stronge reason
575             #if (defined $params{min_distance}) {
576             # push(@sql_where,'distance >= ?');
577             # push(@sql_params,$params{min_distance}+0);
578             #}
579             #if (defined $params{max_distance}) {
580             # push(@sql_where,'distance <= ?');
581             # push(@sql_params,$params{max_distance}+0);
582             #}
583             push(@sql_extra," ORDER BY distance ".($params{distance} ? 'ASC':'DESC'));
584             }
585             # Only probed/unprobed or unknown
586             if (defined $params{is_probed}) {
587             push(@sql_where,'(star.last_checked < ? OR star.is_probed = ? OR star.is_probed IS NULL)');
588             push(@sql_params,(time - $Games::Lacuna::Task::Constants::MAX_STAR_CACHE_AGE),$params{is_probed});
589             } elsif (exists $params{is_probed}) {
590             push(@sql_where,'(star.last_checked < ? OR star.is_probed IS NULL)');
591             push(@sql_params,(time - $Games::Lacuna::Task::Constants::MAX_STAR_CACHE_AGE));
592             }
593             # Only known/unknown
594             if (defined $params{is_known}) {
595             push(@sql_where,'(star.is_known = ? OR star.is_known IS NULL)');
596             push(@sql_params,$params{is_known});
597             }
598             # Zone
599             if (defined $params{zone}) {
600             push(@sql_where,'star.zone = ?');
601             push(@sql_params,$params{zone});
602             }
603             ## Limit results
604             #if (defined $params{limit}) {
605             # push(@sql_extra," LIMIT ?");
606             # push(@sql_params,$params{limit});
607             #}
608            
609             # Build sql
610             my $sql = "SELECT ".join(',',@sql_fields). " FROM star ";
611             $sql .= ' WHERE '.join(' AND ',@sql_where)
612             if scalar @sql_where;
613             $sql .= join(' ',@sql_extra);
614            
615             my $sth = $self->storage_prepare($sql);
616             $sth->execute(@sql_params)
617             or $self->abort('Could not execute SQL command "%s": %s',$sql,$sth->errstr);
618            
619             my $count = 0;
620             # Loop all results
621             while (my $star_cache = $sth->fetchrow_hashref) {
622             # Filter distance
623             next
624             if defined $params{min_distance} && $star_cache->{distance} < $params{min_distance};
625             next
626             if defined $params{max_distance} && $star_cache->{distance} > $params{max_distance};
627            
628             # Inflate star data
629             my $star_data;
630             if (defined $star_cache->{last_checked}
631             && $star_cache->{last_checked} > (time - $Games::Lacuna::Task::Constants::MAX_STAR_CACHE_AGE)) {
632             $star_data = $self->_inflate_star($star_cache);
633             } else {
634             $star_data = $self->_get_star_api($star_cache->{id},$star_cache->{x},$star_cache->{y});
635             }
636            
637             # Check definitve probed status
638             next
639             if (defined $params{is_probed} && $star_data->{is_probed} != $params{is_probed});
640            
641             # Check definitve known status
642             next
643             if (defined $params{is_known} && $star_data->{is_known} != $params{is_known});
644            
645             # Set distance
646             $star_data->{distance} = $star_cache->{distance}
647             if defined $star_cache->{distance};
648            
649             $count ++;
650            
651             # Run callback
652             my $return = $callback->($star_data);
653            
654             last
655             unless $return;
656             last
657             if defined $params{limit} && $count >= $params{limit};
658             }
659            
660             $sth->finish();
661            
662             return;
663             }
664              
665             sub set_body_excavated {
666             my ($self,$body_id,$is_excavated) = @_;
667            
668             $is_excavated //= 1;
669             $self->storage_do('UPDATE body SET is_excavated = ? WHERE id = ?',$is_excavated,$body_id);
670             }
671              
672             no Moose::Role;
673             1;
674              
675             =encoding utf8
676              
677             =head1 NAME
678              
679             Games::Lacuna::Task::Role::Stars - Astronomy helper methods
680              
681             =head1 SYNOPSIS
682              
683             package Games::Lacuna::Task::Action::MyTask;
684             use Moose;
685             extends qw(Games::Lacuna::Task::Action);
686             with qw(Games::Lacuna::Task::Role::Stars);
687            
688             =head1 DESCRIPTION
689              
690             This role provides astronomy-related helper methods.
691              
692             =head1 METHODS
693              
694             =head2 get_star
695              
696             $star_data = $self->get_star($star_id);
697              
698             Fetches star data from the API or local cache for the given star id
699              
700             =head2 get_star_by_name
701              
702             $star_data = $self->get_star_by_name($star_name);
703              
704             Fetches star data from the API or local cache for the given star name
705              
706             =head2 get_star_by_xy
707              
708             $star_data = $self->get_star_by_name($x,$y);
709              
710             Fetches star data from the API or local cache for the given star coordinates
711              
712             =head2 fetch_all_stars
713              
714             $self->fetch_all_stars();
715              
716             Populates the star cache. Usually takes several minutes to complete and thus
717             should not be called regularly.
718              
719             =head2 get_body_by_id
720              
721             $body_data = $self->get_body_by_id($body_id);
722              
723             Fetches body data from the local cache for the given body id
724              
725             =head2 get_body_by_name
726              
727             $body_data = $self->get_body_by_name($body_name);
728              
729             Fetches body data from the local cache for the given body name
730             Ignores case and accents so that eg. 'Hà Nôi' equals 'HA NOI'.
731              
732             =head2 get_body_by_xy
733              
734             $body_data = $self->get_body_by_name($x,$y);
735              
736             Fetches body data from the local cache for the given body coordinates
737              
738             =head2 set_body_excavated
739              
740             $self->set_body_excavated($body_id,$is_excavated);
741              
742             Mark body as excavated
743              
744             =head2 set_star_cache
745              
746             $self->set_star_cache($api_star_data);
747              
748             Create star cache for given api response data
749              
750             =head2 search_stars_callback
751              
752             $self->search_stars_callback(
753             sub {
754             my $star_data = shift;
755             ...
756             },
757             %search_params
758             );
759              
760             Searches all stars acording to the given search parameters and executes the
761             callback for every matching star.
762              
763             Valid search options are
764              
765             =over
766              
767             =item * is_probed (0 = unprobed, 1 = probed)
768              
769             =item * is_known (0 = body data not available, 1 = body data available)
770              
771             =item * max_distance
772              
773             =item * min_distance
774              
775             =item * distance (1 = ascending, 0 = descending)
776              
777             =item * zone
778              
779             =item * x,y (refernce coordinates for distance calculations)
780              
781             =back
782              
783             =cut