File Coverage

blib/lib/Games/FEAR/Log.pm
Criterion Covered Total %
statement 19 255 7.4
branch 0 128 0.0
condition 0 30 0.0
subroutine 7 18 38.8
pod 9 9 100.0
total 35 440 7.9


line stmt bran cond sub pod time code
1             package Games::FEAR::Log;
2             {
3              
4 2     2   57297 use warnings;
  2         6  
  2         70  
5 2     2   13 use strict;
  2         4  
  2         172  
6              
7 2     2   14 use Carp;
  2         9  
  2         323  
8 2     2   5288 use DBI;
  2         47528  
  2         154  
9 2     2   2836 use File::Copy;
  2         18897  
  2         184  
10 2     2   2878 use File::Temp;
  2         48565  
  2         7517  
11              
12             ## Object Attributes ##
13              
14             # objects of this class have the following attributes...
15             my %dbh_of; # open database handle of type DBI
16             my %create_of; # indicator of whether to create table if doesnt exist
17             my %table_of; # main stats table in database
18             my %logfile_of; # logfile of the source logfile
19             my %tempfile_of; # temp file via File::Temp (only if truncate is false)
20             my %history_of; # length of time before
21             my %truncate_of; # indicator of whether to truncate logfile after analysis
22              
23             ## Object Methods ##
24              
25             # class constructor, takes hashref of arguments
26             sub new {
27 0     0 1 0 my ($class, $args) = @_;
28            
29             # bless a scalar to instantiate the new object...
30 0         0 my $new_object = bless \do{my $anon_scalar}, $class;
  0         0  
31            
32             # temporary variable (to save on expensive hash accesses, etc)
33 0         0 my $temp_arg;
34            
35             # if no hashref of arguments provided, croak with error
36 0 0       0 if (ref $args ne 'HASH') {
37 0         0 croak "missing hash reference of constructor arguments";
38             }
39            
40             # if logfile argument doesnt exist or is undef
41 0 0 0     0 if ( !exists $args->{'-logfile'} || !defined $args->{'-logfile'} ) {
42             # set logfile and tempfile attributes as undef
43 0         0 $logfile_of{ident $new_object} = undef;
44 0         0 $tempfile_of{ident $new_object} = undef;
45             }
46             else {
47             # if logfile is anything other than a scalar value, scalar ref, or glob ref
48 0         0 $temp_arg = ref $args->{'-logfile'};
49 0 0 0     0 if ($temp_arg ne 'SCALAR' && $temp_arg ne 'GLOB' && $temp_arg ne '') {
      0        
50             # croak on invalid logfile
51 0         0 croak "invalid logfile attribute, must be string, scalar ref, or filehandle ref";
52             }
53            
54             # set logfile as attribute
55 0         0 $logfile_of{ident $new_object} = $args->{'-logfile'};
56            
57             # set tempfile (File::Temp object) as attribute
58 0         0 $tempfile_of{ident $new_object} = new File::Temp();
59             }
60            
61             # if dbh doesnt exist or is undefined
62 0 0 0     0 if ( !exists $args->{'-dbi'} || !defined $args->{'-dbi'} ) {
63             # croak on missing db handle
64 0         0 croak "missing or undefined dbi attribute"
65             }
66            
67             # if anonymous hash of 3 elements (dsn, user, pass) is not provided
68 0         0 my $dbh = $args->{'-dbi'};
69 0 0 0     0 if( ref $dbh ne 'ARRAY' || $#{$dbh} != 2 ) {
  0         0  
70             # croak on invalid arguments
71 0         0 croak "invalid dbi attribute, must be an arrayref of dsn, username, and password";
72             }
73            
74             # get database driver name from the dsn
75 0         0 (undef,$temp_arg,undef) = split /\:/, $dbh->[0],3;
76            
77             # if support for this db driver not found in the %_QUERIES hash
78 0 0       0 if ( ! exists $Games::FEAR::Log::_QUERIES{$temp_arg} ) {
79             # croak with reason
80 0         0 croak "database driver '$temp_arg' is not yet supported";
81             }
82            
83             # create dbi connection
84 0 0       0 $dbh = DBI->connect(
85             $dbh->[0], $dbh->[1], $dbh->[2], # dsn, username, password
86             { PrintError => 0, PrintWarn => 0, RaiseError => 1, AutoCommit => 0}
87             ) or croak "could not connect to database: $DBI::errstr";
88            
89             # set dbi object as attribute
90 0         0 $dbh_of{ident $new_object} = $dbh;
91            
92             # if create is not provided or undefined
93 0 0 0     0 if ( ! exists $args->{'-create'} || ! defined $args->{'-create'} ) {
94             # default to one (true)
95 0         0 $args->{'-create'} = 1;
96             }
97            
98             # if not true
99 0         0 $temp_arg = $args->{'-create'};
100 0 0       0 if (! $temp_arg ) {
101             # set to zero (false)
102 0         0 $temp_arg = 0;
103             }
104            
105             # set create mode as attribute
106 0         0 $create_of{ident $new_object} = $temp_arg;
107            
108             # if stats tablename doesnt exist or is undefined
109 0 0 0     0 if ( ! exists $args->{'-table'} || ! defined $args->{'-table'} || $args->{'-table'} !~ m/\A[a-z0-9_]+\z/) {
      0        
110             # croak on missing table name
111 0         0 croak "missing, undefined, or invalid table attribute"
112             }
113            
114             # test if stats table exists, trapping potential errors in an eval
115 0         0 $temp_arg = $args->{'-table'};
116 0         0 eval {
117             # select with an always false WHERE clause, will return an empty but existing dataset
118 0         0 my $query_ref = $dbh->selectall_arrayref( _build_query('test_exist',$temp_arg) );
119             };
120            
121             # if eval failed
122 0 0       0 if ($@ ne '') {
123             # if table doesnt exist
124 0 0       0 if ($@ =~ m/table .+ exist/imsx) { # mysql error is "table '' doesn't exist"
125            
126             # if indicated NOT to create table if it doesnt exist
127 0 0       0 if (! $create_of{ident $new_object} ) {
128             # croak with reason
129 0         0 croak $@;
130             }
131            
132             # create table (in an eval)
133 0         0 eval {
134 0 0       0 $dbh->do( _build_query('ddl',$temp_arg) ) or die $dbh->errstr;
135 0         0 $dbh->commit;
136             };
137             # if create fails
138 0 0       0 if($@ ne '') {
139             # croak with reason
140 0         0 croak "could not create stats table: $@";
141             }
142             }
143             else {
144             # croak with reason
145 0         0 croak "could not access stats table: $@";
146             }
147             }
148            
149             # set stats table as attribute
150 0         0 $table_of{ident $new_object} = $temp_arg;
151            
152             # if history doesnt exist or is undefined
153 0 0 0     0 if ( ! exists $args->{'-history'} || ! defined $args->{'-history'} ) {
154             # default to zero
155 0         0 $args->{'-history'} = 0;
156             }
157            
158             # if parsing of timespan returns undefined
159 0         0 $temp_arg = _timespan_parse( $args->{'-history'} );
160 0 0       0 if ( ! defined $temp_arg ) {
161             # croak on invalid duration (not zero, not timespan as defined in the docs)
162 0         0 croak "invalid history attribute, must be zero or valid duration (see docs)";
163             }
164            
165             # set truncate mode as attribute
166 0         0 $history_of{ident $new_object} = $temp_arg;
167            
168             # if truncate is not provided or undefined
169 0 0 0     0 if ( ! exists $args->{'-truncate'} || ! defined $args->{'-truncate'}) {
170             # default to zero (false)
171 0         0 $args->{'-truncate'} = 0;
172             }
173            
174             # if not zero (false)
175 0         0 $temp_arg = $args->{'-truncate'};
176 0 0       0 if ( $temp_arg != 0 ) {
177             # set to one (true)
178 0         0 $temp_arg = 1;
179             }
180            
181             # set truncate mode as attribute
182 0         0 $truncate_of{ident $new_object} = $temp_arg;
183            
184 0         0 return $new_object;
185             }
186              
187             # load logfile and build data structure
188             sub process {
189 0     0 1 0 my ($self) = @_;
190            
191             # if no logfile was defined
192 0 0       0 if ( !defined $logfile_of{ident $self} ) {
193             # croak with error
194 0         0 croak "process method called without supplying a logfile";
195             }
196            
197             # make local copies of relevant attributes to save some expensive hash accesses
198 0         0 my $logfile = $logfile_of{ident $self};
199 0         0 my $tempfile = $tempfile_of{ident $self};
200            
201             # put temp file into binary mode and seek to start of file
202 0         0 binmode $tempfile;
203 0         0 seek $tempfile, 0, 0;
204            
205             # if logfile attribute is a scalar reference
206 0 0       0 if(ref $logfile eq 'SCALAR') {
    0          
207             # assume it's a ref to the file *content*, and write to temp file
208 0         0 print { $tempfile } $logfile;
  0         0  
209             }
210             # if logfile attribute is a filehandle reference
211             elsif(ref $logfile eq 'GLOB') {
212             # put into binary mode and seek to start of filehandle (just in case)
213 0         0 binmode $logfile;
214 0         0 seek $logfile, 0, 0;
215            
216             # copy directly from one filehandle to another
217 0 0       0 if ( copy($logfile, $tempfile) != 1 ) {
218             # croak if copy fails for any reason
219 0         0 croak "copy of logfile failed: $!"
220             }
221             }
222             # if logfile attribute is (presumably) scalar value
223             else {
224             # check that file actually exists in filesystem
225 0 0       0 if (! -e $logfile) {
226 0         0 croak "logfile '$logfile' does not exist"
227             }
228            
229             # copy logfile overwriting temp file
230 0 0       0 if ( copy($logfile, $tempfile) != 1 ) {
231             # croak if copy fails for any reason
232 0         0 croak "copy of logfile failed: $!"
233             }
234             }
235            
236             # if source log is to be truncated
237 0 0       0 if ( $truncate_of{ident $self} == 1 ) {
238             # if logfile is scalar reference
239 0 0       0 if (ref $logfile eq 'SCALAR') {
    0          
240             # truncate referenced scalar to an empty string
241 0         0 ${$logfile} = "";
  0         0  
242             }
243             # if filehandle reference
244             elsif (ref $logfile eq 'GLOB') {
245             # truncate filehandle to zero bytes (trapping potential errors)
246 0         0 my $return_val = 0;
247 0         0 eval { $return_val = truncate $logfile, 0; };
  0         0  
248            
249             # if truncate failed on error, croak with eval error
250 0 0       0 if ( $@ ne '' ) {
251 0         0 croak "truncate failed: $@"
252             }
253            
254             # otherwise, if truncate didnt return true, croak with error
255 0 0       0 if (! $return_val) {
256 0         0 croak "truncate failed: $!";
257             }
258             }
259             # if (presumably) scalar value
260             else {
261             # open for destructive write, and immediately close
262 0 0       0 open my $trunc_file, '>', $logfile or carp "couldnt open logfile for truncation: $!";
263 0         0 close $trunc_file;
264             }
265             }
266            
267             # if non-zero history duration...
268 0         0 my $duration = $history_of{ident $self};
269 0         0 my $dbh = $dbh_of{ident $self};
270 0 0       0 if ( $duration != 0 ) {
271             # remove any expired history records
272 0         0 eval {
273 0 0       0 $dbh->do(
274             _build_query('remove_expired',$table_of{ident $self}),
275             undef,
276             $duration
277             ) or croak $dbh->errstr;
278 0 0       0 $dbh->commit or croak $dbh->errstr;
279             };
280            
281             # if delete failed
282 0 0       0 if ( $@ ne '' ) {
283             # croak with reason
284 0         0 croak "deletion of outdated records failed: $@";
285             }
286             }
287            
288             # seek to start of file and turn off binary mode
289 0         0 seek $tempfile, 0, 0;
290 0         0 binmode $tempfile, ':crlf';
291            
292             # loop through temp file, line by line
293 0         0 my(%session, $team);
294 0         0 while( my $line = <$tempfile> )
295             {
296             # if start of a new map, reset team variable
297 0 0       0 if ( $line =~ m/] \*+? Results for Map/i ) {
298 0         0 $team = 0;
299             }
300            
301             # if team entry found, set team
302 0 0       0 if ( $line =~ m/] Team: Team (\d)/i ) {
303 0         0 $team = $1;
304             }
305            
306             # if player entry found, set timestamp, player, and uid
307 0 0       0 if ( $line =~ m/\[(.+?)] Player: ([^ ]+) \(uid: (.+?)\)/i ) {
308 0         0 $session{'timestamp'} = $1;
309 0         0 $session{'player'} = $2;
310 0         0 $session{'uid'} = $3;
311            
312             # adapt timestamp to a SQL-compatible date/time string
313 0         0 $session{'timestamp'} =~ s/\A\w+ (\w+) (\d+) ([\d:]+) (\d+)\z/$4-$Games::FEAR::Log::_MONTHS{$1}-$2 $3/;
314             }
315            
316             # for every stat found, set corresponding stat
317 0         0 foreach my $stat ( 'score', 'kills', 'deaths', 'suicides', 'team kills' ) {
318 0 0       0 if ( $line =~ m/] $stat: (\d+)/i ) {
319 0         0 $session{$stat} = $1;
320 0         0 last;
321             }
322             }
323            
324             # if final stat of an entry found...
325 0 0       0 if( $line =~ m/] objective: (\d+)/i ) {
326             # finalize session stats hash
327 0         0 $session{'objective'} = $1;
328 0         0 $session{'team'} = $team;
329            
330             # select to see if this record already exists
331 0         0 my $count = 0;
332 0         0 eval {
333 0 0       0 ($count) = $dbh->selectrow_array(
334             _build_query('test_dml', $table_of{ident $self}),
335             $session{'timestamp'},
336             $session{'uid'},
337             ) or croak $dbh->errstr;
338             };
339            
340             # if select indicates record doesnt exist
341 0 0       0 if($count < 1) {
342            
343             # insert into database, trapping in an eval
344 0         0 eval {
345 0 0       0 $dbh->do(
346             _build_query('dml', $table_of{ident $self}),
347             $session{'timestamp'},
348             $session{'uid'},
349             $session{'player'},
350             $session{'team'},
351             $session{'score'},
352             $session{'kills'},
353             $session{'deaths'},
354             $session{'teamkills'},
355             $session{'suicides'},
356             $session{'objective'}
357             ) or croak $dbh->errstr;
358             };
359            
360             # if error during insert
361 0 0       0 if ($@ ne '') {
362             # croak with reason
363 0         0 croak "an error occurred inserting records: $@";
364             }
365            
366             }
367              
368             # clear session hash
369 0         0 %session = ();
370             }
371             }
372            
373             # commit changes made, trapping in an eval
374 0         0 eval {
375 0 0       0 $dbh->commit or croak $dbh->errstr;
376             };
377            
378             # if error during commit
379 0 0       0 if ($@ ne '') {
380             # croak with reason
381 0         0 croak "an error occurred committing database changes: $@";
382             }
383            
384             # return true on success
385 0         0 return 1;
386             }
387              
388             sub get_uids {
389 0     0 1 0 my $self = @_;
390            
391             # store dbh to save hash accesses
392 0         0 my $dbh = $dbh_of{ident $self};
393            
394             # build uid column from SELECT into an array reference, trapping in an eval
395 0         0 my $array_ref;
396 0         0 eval {
397 0         0 $array_ref = $dbh->selectcol_arrayref(
398             _build_query('get_uids', $table_of{ident $self})
399             );
400             };
401            
402             # if error occurred during select
403 0 0       0 if ( $@ ne '' ) {
404             # croak with reason
405 0         0 croak "listing of uids failed: $@";
406             }
407            
408             # if nonfatal error indicated
409 0 0       0 if ( defined $dbh->err ) {
410 0         0 croak "listing of uids failed: ", $dbh->errstr;
411             }
412            
413             # return dereferenced array
414 0         0 return @{$array_ref};
  0         0  
415             }
416              
417             sub get_playernames {
418 0     0 1 0 my $self = shift;
419 0         0 my $uid = shift;
420            
421             # store dbh to save hash accesses
422 0         0 my $dbh = $dbh_of{ident $self};
423            
424             # build player names for the given uid into an array reference, in order of commonality
425 0         0 my $array_ref;
426 0         0 eval {
427 0         0 $array_ref = $dbh->selectcol_arrayref(
428             _build_query('get_players', $table_of{ident $self}),
429             undef,
430             $uid
431             );
432             };
433            
434             # if error occurred during select
435 0 0       0 if ( $@ ne '' ) {
436             # croak with reason
437 0         0 croak "listing of playernames failed: $@";
438             }
439            
440             # if nonfatal error indicated
441 0 0       0 if ( defined $dbh->err ) {
442 0         0 croak "listing of playernames failed: ", $dbh->errstr;
443             }
444            
445             # return dereferenced array
446 0         0 return @{$array_ref};
  0         0  
447             }
448              
449             sub get_stats {
450 0     0 1 0 my $self = shift;
451 0         0 my $uid = shift;
452            
453             # store dbh to save hash accesses
454 0         0 my $dbh = $dbh_of{ident $self};
455            
456             # build anonymous hash of averaged and totalled statistics
457 0         0 my $hash_ref;
458 0         0 eval {
459 0         0 $hash_ref = $dbh->selectrow_hashref(
460             _build_query('get_stats', $table_of{ident $self}),
461             undef,
462             $uid
463             );
464             };
465            
466             # if error occurred during select
467 0 0       0 if ( $@ ne '' ) {
468             # croak with reason
469 0         0 croak "listing of stats failed: $@";
470             }
471            
472             # if nonfatal error indicated
473 0 0       0 if ( defined $dbh->err ) {
474 0         0 croak "listing of stats failed: ", $dbh->errstr;
475             }
476            
477             # return hash reference
478 0         0 return $hash_ref;
479             }
480              
481             sub get_history {
482 0     0 1 0 my $self = shift;
483 0         0 my $uid = shift;
484            
485             # store dbh to save hash accesses
486 0         0 my $dbh = $dbh_of{ident $self};
487            
488             # build anonymous hash of hashrefs of game records
489 0         0 my $hash_ref;
490 0         0 eval {
491 0         0 $hash_ref = $dbh->selectall_hashref(
492             _build_query('get_history', $table_of{ident $self}),
493             'timestamp',
494             undef,
495             $uid
496             );
497             };
498            
499             # if error occurred during select
500 0 0       0 if ( $@ ne '' ) {
501             # croak with reason
502 0         0 croak "listing of history failed: $@";
503             }
504            
505             # if nonfatal error indicated
506 0 0       0 if ( defined $dbh->err ) {
507 0         0 croak "listing of history failed: ", $dbh->errstr;
508             }
509            
510             # return hash reference
511 0         0 return $hash_ref;
512             }
513              
514             sub get_game {
515 0     0 1 0 my $self = shift;
516 0         0 my $timestamp = shift;
517            
518             # store dbh to save hash accesses
519 0         0 my $dbh = $dbh_of{ident $self};
520            
521             # build anonymous hash of hashrefs of game records
522 0         0 my $hash_ref;
523 0         0 eval {
524 0         0 $hash_ref = $dbh->selectall_hashref(
525             _build_query('get_game', $table_of{ident $self}),
526             'uid',
527             undef,
528             $timestamp
529             );
530             };
531            
532             # if error occurred during select
533 0 0       0 if ( $@ ne '' ) {
534             # croak with reason
535 0         0 croak "listing of games failed: $@";
536             }
537            
538             # if nonfatal error indicated
539 0 0       0 if ( defined $dbh->err ) {
540 0         0 croak "listing of games failed: ", $dbh->errstr;
541             }
542            
543             # return hash reference
544 0         0 return $hash_ref;
545             }
546              
547             sub build_scoreboard {
548 0     0 1 0 my $self = shift;
549            
550 0         0 my($offset,$length) = @_;
551            
552             # for each argument, use default if no value provided, or croak if invalid
553            
554             # offset to start resultset at
555 0 0       0 $offset = 0 if ! defined $offset;
556 0 0       0 croak "invalid offset, not a positive integer: '$offset'" if $offset !~ m/^[0-9]+$/;
557            
558             # number of records in resultset
559 0 0       0 $length = 0 if ! defined $length;
560 0 0       0 croak "invalid length, not a positive integer: '$length'" if $offset !~ m/^[0-9]+$/;
561            
562             # get local reference to db handle
563 0         0 my $dbh = $dbh_of{ident $self};
564            
565 0         0 my $sth;
566 0 0       0 if($length) {
567             # limit
568 0         0 $sth = $dbh->prepare(
569             _build_query('build_scoreboard_limit', $table_of{ident $self}),
570             undef,
571             $length, $offset
572             );
573             }
574             else {
575             # no limit
576 0         0 $sth = $dbh->prepare(
577             _build_query('build_scoreboard', $table_of{ident $self})
578             );
579             }
580            
581             # retrieve resultset, trapping any errors in an eval
582 0         0 my @rowset;
583 0         0 eval {
584             # execute prepared statement
585 0         0 $sth->execute();
586            
587             # fetch rows as hashrefs, push to array
588 0         0 my $row_ref;
589 0         0 while( $row_ref = $sth->fetchrow_hashref ) {
590 0         0 push @rowset, $row_ref;
591             }
592             };
593            
594             # get playernames for all, duplicating entries if multiple playernames found
595 0         0 my @results;
596 0         0 foreach my $row_ref (@rowset) {
597             #
598 0         0 my @players = $self->get_playernames( $row_ref->{'uid'} );
599            
600 0         0 foreach my $player (@players) {
601 0         0 $row_ref->{'player'} = $player;
602 0         0 push(@results, $row_ref);
603             }
604 0         0 $row_ref = undef;
605             }
606 0         0 undef @rowset;
607            
608 0         0 return @results;
609             }
610              
611             # class destructor
612             sub DESTROY {
613 0     0   0 my $self = shift;
614            
615             # close created db handle
616 0         0 $dbh_of{ident $self}->disconnect;
617            
618             # deallocate inside-out object attributes
619 0         0 delete $dbh_of{ident $self};
620 0         0 delete $create_of{ident $self};
621 0         0 delete $logfile_of{ident $self};
622 0         0 delete $history_of{ident $self};
623 0         0 delete $table_of{ident $self};
624 0         0 delete $tempfile_of{ident $self};
625 0         0 delete $truncate_of{ident $self};
626            
627 0         0 return;
628             }
629              
630             ## Public subroutines ##
631              
632             sub supported_dbds {
633             # return database driver names with explicit support
634 1     1 1 12 return keys(%Games::FEAR::Log::_QUERIES);
635             }
636              
637             ## Utility Subroutines ##
638              
639             # takes a string representing a timespan (ex: "1M" is 1 month) and returns
640             # a representative number of seconds
641             sub _timespan_parse {
642 0     0     my $span = shift;
643            
644 0           my $span_types = join '|', keys(%Games::FEAR::Log::_SPANS);
645 0 0         if ($span =~ m/\A \+? (\d+) ($span_types) \z/xms) {
    0          
646 0           return $1 * $Games::FEAR::Log::_SPANS{$2};
647             }
648             elsif ($span eq '0') {
649 0           return 0;
650             }
651             else {
652 0           return;
653             }
654            
655             }
656              
657             sub _build_query {
658 0     0     my $queryname = shift;
659 0           my $tablename = shift;
660            
661             #if(exists $_QUERIES{ $dbd_of{ident $self} }->{$queryname}) {
662             #return join $tablename, @{ $_QUERIES{ $dbd_of{ident $self} }->{$queryname} };
663 0 0         if( exists $Games::FEAR::Log::_QUERIES{ 'mysql' }->{$queryname} ) {
664 0           return join $tablename, @{ $Games::FEAR::Log::_QUERIES{ 'mysql' }->{$queryname} };
  0            
665             }
666             }
667              
668             ## Utility Variables ##
669              
670             # serial month to numeric month conversion table
671             my %_MONTH = (
672             'jan' => '01', 'feb' => '02', 'mar' => '03', 'apr' => '04',
673             'may' => '05', 'jun' => '06', 'jul' => '07', 'aug' => '08',
674             'sep' => '09', 'oct' => '10', 'nov' => '11', 'dec' => '12',
675             );
676              
677             # timespan format to second format conversion table
678             my %_SPANS = (
679             's' => 1, # seconds
680             'm' => 60, # minutes
681             'h' => 60*60, # hours
682             'd' => 60*60*24, # days
683             'M' => 60*60*24*30, # months (roughly, 30 days specifically)
684             'y' => 60*60*24*365 # years (roughly, 365 days specifically)
685             );
686              
687             # declare hash to store SQL queries (for definitions, see near end of file)
688             my %_QUERIES;
689              
690             # for DBD::mysql (MySQL)
691             $_QUERIES{'mysql'} = {
692             'ddl' => [
693             'CREATE TABLE `',
694             '` (
695             `gametime` bigint NOT NULL,
696             `uid` varchar(255) NOT NULL,
697             `player` varchar(255) NOT NULL,
698             `team` int(11) UNSIGNED NOT NULL,
699             `score` int(11) NOT NULL,
700             `kills` int(11) UNSIGNED NOT NULL,
701             `deaths` int(11) UNSIGNED NOT NULL,
702             `teamkills` int(11) UNSIGNED NOT NULL,
703             `suicides` int(11) UNSIGNED NOT NULL,
704             `objective` int(11) UNSIGNED NOT NULL,
705             PRIMARY KEY(`gametime`, `uid`)
706             )'
707             ],
708             'dml' => [
709             'INSERT INTO `',
710             '`
711             (`gametime`, `uid`, `player`, `team`, `score`, `kills`, `deaths`, `teamkills`, `suicides`, `objective`)
712             VALUES
713             (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
714             ],
715             'test_dml' => [
716             'SELECT COUNT(*) FROM `',
717             '` WHERE `gametime` = ? AND `uid` = ?'
718             ],
719             'test_exist' => [
720             'SELECT * FROM `',
721             '` WHERE 1=0'
722             ],
723             'remove_expired' => [
724             'DELETE FROM `',
725             '` WHERE `gametime` < (UNIX_TIMESTAMP() - ?)'
726             ],
727             'get_uids' => [
728             'SELECT DISTINCT `uid` FROM `',
729             '` WHERE 1'
730             ],
731             'get_players' => [
732             'SELECT `player`, COUNT(`player`) as `occurances` FROM `',
733             '` WHERE `uid` = ? GROUP BY `player` ORDER BY `occurances` DESC'
734             ],
735             'get_stats' => [
736             'SELECT COUNT(`gametime`) as `game_count`,
737             AVG(`score`) as `avg_score`, AVG(`kills`) as `avg_kills`,
738             AVG(`deaths`) as `avg_deaths`, AVG(`suicides`) as `avg_suicides`,
739             AVG(`teamkills`) as `avg_teamkills`, AVG(`objective`) as `avg_objective`,
740             SUM(`score`) as `tot_score`, SUM(`kills`) as `tot_kills`,
741             SUM(`deaths`) as `tot_deaths`, SUM(`suicides`) as `tot_suicides`,
742             SUM(`teamkills`) as `tot_teamkills`, SUM(`objective`) as `tot_objective`
743             FROM `',
744             '` WHERE `uid` = ? LIMIT 1'
745             ],
746             'get_history' => [
747             'SELECT `gametime`, `team`, `player`, `score`, `kills`, `deaths`, `teamkills`, `suicides`, `objective` FROM `',
748             '` WHERE `uid` = ?'
749             ],
750             'get_game' => [
751             'SELECT `uid`, `team`, `player`, `score`, `kills`, `deaths`, `teamkills`, `suicides`, `objective` FROM `',
752             '` WHERE `gametime` = ?'
753             ],
754             'build_scoreboard' => [
755             'SELECT `uid`,
756             ROUND(AVG(`score`)) as `avg_score`, ROUND(AVG(`kills`)) as `avg_kills`,
757             ROUND(AVG(`deaths`)) as `avg_deaths`, ROUND(AVG(`suicides`)) as `avg_suicides`,
758             ROUND(AVG(`teamkills`)) as `avg_teamkills`, ROUND(AVG(`objective`)) as `avg_objective`,
759             SUM(`score`) as `tot_score`, SUM(`kills`) as `tot_kills`,
760             SUM(`deaths`) as `tot_deaths`, SUM(`suicides`) as `tot_suicides`,
761             SUM(`teamkills`) as `tot_teamkills`, SUM(`objective`) as `tot_objective`
762             FROM `',
763             '` GROUP BY `uid`'
764             ],
765             'build_scoreboard_limit' => [
766             'SELECT `uid`,
767             ROUND(AVG(`score`)) as `avg_score`, ROUND(AVG(`kills`)) as `avg_kills`,
768             ROUND(AVG(`deaths`)) as `avg_deaths`, ROUND(AVG(`suicides`)) as `avg_suicides`,
769             ROUND(AVG(`teamkills`)) as `avg_teamkills`, ROUND(AVG(`objective`)) as `avg_objective`,
770             SUM(`score`) as `tot_score`, SUM(`kills`) as `tot_kills`,
771             SUM(`deaths`) as `tot_deaths`, SUM(`suicides`) as `tot_suicides`,
772             SUM(`teamkills`) as `tot_teamkills`, SUM(`objective`) as `tot_objective`
773             FROM `',
774             '` GROUP BY `uid` LIMIT ? OFFSET ?'
775             ],
776             };
777              
778             # for DBD::pg (PostgreSQL)
779             $_QUERIES{'pg'} = {
780             'ddl' => [
781             'CREATE TABLE "',
782             '"
783             (
784             "gametime" integer NOT NULL,
785             "uid" varchar NOT NULL,
786             "player" varchar NOT NULL,
787             "team" integer NOT NULL,
788             "score" integer NOT NULL,
789             "kills" integer NOT NULL,
790             "deaths" integer NOT NULL,
791             "teamkills" integer NOT NULL,
792             "suicides" integer NOT NULL,
793             "objective" integer NOT NULL,
794             CONSTRAINT "PRIMARY" PRIMARY KEY ("gametime", "uid")
795             ) WITHOUT OIDS'
796             ],
797             'dml' => [
798             'INSERT INTO "',
799             '"
800             ("gametime", "uid", "player", "team", "score", "kills", "deaths", "teamkills", "suicides", "objective")
801             VALUES
802             (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
803             ],
804             'test_dml' => [
805             'SELECT COUNT(*) FROM `',
806             '` WHERE `gametime` = ? AND `uid` = ?'
807             ],
808             'test_exist' => [
809             'SELECT * FROM "',
810             '" WHERE 1=0'
811             ],
812             'remove_expired' => [
813             'DELETE FROM "',
814             '" WHERE "gametime" < (EXTRACT(EPOCH FROM TIMESTAMP WITH TIME ZONE NOW()) - ?)'
815             ],
816             'get_uids' => [
817             'SELECT DISTINCT "uid" FROM "',
818             '" WHERE 1'
819             ],
820             'get_players' => [
821             'SELECT "player", COUNT("player") as "occurances" FROM "',
822             '" WHERE "uid" = ? GROUP BY "player" ORDER BY "occurances" DESC'
823             ],
824             'get_stats' => [
825             'SELECT COUNT("gametime") as "game_count",
826             AVG("score") as "avg_score", AVG("kills") as "avg_kills",
827             AVG("deaths") as "avg_deaths", AVG("suicides") as "avg_suicides",
828             AVG("teamkills") as "avg_teamkills", AVG("objective") as "avg_objective",
829             SUM("score") as "tot_score", SUM("kills") as "tot_kills",
830             SUM("deaths") as "tot_deaths", SUM("suicides") as "tot_suicides",
831             SUM("teamkills") as "tot_teamkills", SUM("objective") as "tot_objective"
832             FROM "',
833             '" WHERE "uid" = ? LIMIT 1'
834             ],
835             'get_history' => [
836             'SELECT "gametime", "team", "player", "score", "kills", "deaths", "teamkills", "suicides", "objective" FROM "',
837             '" WHERE "uid" = ?'
838             ],
839             'get_game' => [
840             'SELECT "uid", "team", "player", "score", "kills", "deaths", "teamkills", "suicides", "objective" FROM "',
841             '" WHERE "gametime" = ?'
842             ],
843             'build_scoreboard' => [
844             'SELECT "uid",
845             ROUND(AVG("score")) as "avg_score", ROUND(AVG("kills")) as "avg_kills",
846             ROUND(AVG("deaths")) as "avg_deaths", ROUND(AVG("suicides")) as "avg_suicides",
847             ROUND(AVG("teamkills")) as "avg_teamkills", ROUND(AVG("objective")) as "avg_objective",
848             SUM("score") as "tot_score", SUM("kills") as "tot_kills",
849             SUM("deaths") as "tot_deaths", SUM("suicides") as "tot_suicides",
850             SUM("teamkills") as "tot_teamkills", SUM("objective") as "tot_objective"
851             FROM "',
852             '" GROUP BY "uid"'
853             ],
854             'build_scoreboard_limit' => [
855             'SELECT "uid",
856             ROUND(AVG("score"),0) as "avg_score", ROUND(AVG("kills"),0) as "avg_kills",
857             ROUND(AVG("deaths"),0) as "avg_deaths", ROUND(AVG("suicides"),0) as "avg_suicides",
858             ROUND(AVG("teamkills"),0) as "avg_teamkills", ROUND(AVG("objective"),0) as "avg_objective",
859             SUM("score") as "tot_score", SUM("kills") as "tot_kills",
860             SUM("deaths") as "tot_deaths", SUM("suicides") as "tot_suicides",
861             SUM("teamkills") as "tot_teamkills", SUM("objective") as "tot_objective"
862             FROM "',
863             '" GROUP BY "uid" LIMIT ? OFFSET ?'
864             ],
865             };
866              
867             =head1 NAME
868              
869             Games::FEAR::Log - Log analysis tool for F.E.A.R. dedicated servers
870              
871             =head1 VERSION
872              
873             Version 0.02
874              
875             =cut
876              
877             our $VERSION = '0.02';
878              
879             =head1 SYNOPSIS
880              
881             use Games::FEAR::Log;
882            
883             # instantiate new object, passing a hash reference of options
884             my $log_obj = Games::FEAR::Log->new( {
885             # database information: a dsn, username, and password
886             -dbi => [
887             'DBI:mysql:database=scoreboard;host=localhost;port=3306',
888             'scoreboard_admin',
889             'scoreboard_password'
890             ],
891             # table to store info
892             -table => 'deathmatch1',
893             # create table if it doesnt exist
894             -create => ,
895             # full path to logfile
896             -logfile => '/var/log/FEAR/mp_scores.log',
897             # empty the source logfile after reading it
898             -truncate => 1,
899             # delete any records older than 30 days
900             -history => '30d'
901             } );
902            
903             # process log file, importing new entries
904             $log_obj->process() or die 'processing failed';
905            
906             # get ID of first user
907             my @uids = $log_obj->get_uids();
908            
909             # get playernames this user goes by
910             my @names = $log_obj->get_playernames( $uid[0] );
911            
912             # get stats for this user
913             my $stats = $log_obj->get_stats( $uid[0] );
914            
915             # get history for this user
916             my $history = $log_obj->get_history( $uid[0] );
917             my @gametimes = keys %{$history};
918            
919             # get information for a game played by said user
920             my $game = $log_obj->get_game( $gametimes[0] );
921            
922             # get scoreboard-structured informatuon
923             my @scores = build_scoreboard('player', 'asc');
924              
925             =head1 DESCRIPTION
926              
927             This module allows the parsing of a F.E.A.R. multiplayer server log into a
928             manageable database format, and provides an easy to use object-oriented
929             interface to access that information. This information could then be used
930             to create a CGI scoreboard application, such as the one included in the
931             C directory.
932              
933             The underlying system uses a SQL relational database to store and retrieve
934             game information. Initially, this implimentation is built to use a MySQL or
935             PostgreSQL database, but I can add support for other database systems if
936             there is a demand.
937              
938             Ideally, there could be two different 'pieces' to an application using this
939             module, an administrative interface to import new log entries into the
940             database, and a public interface to display and/or cross-reference already
941             imported information.
942              
943             If performance is not a concern, however, it could be a one-piece
944             application where new entries are checked for and added every time the
945             interface is viewed.
946              
947             =head1 METHODS
948              
949             =head2 new()
950              
951             Creates and returns a new object. Takes a single argument, a hash reference
952             containing configuration options. The available options are as follows:
953              
954             =over
955              
956             =item * C<-dbi>
957              
958             An anonymous array of a DSN (data source name), username, and password for
959             connecting to the database. See the L docs for an explanation and
960             syntax of a DSN. An error will be thrown if this option is not found or
961             invalid.
962              
963             [ 'DBI:mysql:database=test;host=localhost', 'devuser', 'devpass' ]
964              
965             =item * C<-table>
966              
967             Name of the database table to use for this set of statistics. If stats are
968             being kept for multiple game servers, each one should have its own seperate
969             table. An error will be thrown if this option is not found or invalid.
970              
971             =item * C<-create>
972              
973             Indicate whether the given table should be created if it does not already
974             exist. A true value creates the table if necessary, while a false value
975             throws an error if the table doesnt exist. The default is to create the
976             table.
977              
978             =item * C<-logfile>
979              
980             Source of the log entries. This is not a required parameter unless you
981             plan on calling the C method. If a scalar value is passed, it is
982             assumed to be a filename. If a scalar reference is passed, it is assumed
983             to be the contents of the log, and will be dereferenced and processed. If
984             a glob reference is passed, it is assumed to be an open filehandle to the
985             logfile (note that it should be opened for read I write operations).
986              
987             =item * C<-history>
988              
989             Length of time to keep records, specified in a format similar to that used
990             by the L module, with a numeric quantity followed by a one-letter
991             unit indicator:
992              
993             86400s # 1 day specified in seconds
994             1440m # 1 day specified in minutes
995             24h # 1 day specified in hours
996             90d # 3 months specified in days
997             12M # 1 year specified in months
998             2y # 2 years specified in years
999              
1000             The default is to keep them forever, and this can be specified by passing an empty or undefined scalar, or a value of zero.
1001              
1002             =item * C<-truncate>
1003              
1004             Indicate whether the source log should be truncated to zero bytes (and in
1005             effect emptied). This is useful if you are reading from a live log file
1006             and don't want to waste resources reprocessing old log entries. Note, of
1007             course, that if a logfile is already locked by the server process, any
1008             attempted writes to it will fail. A non-zero value turns on log truncating,
1009             and a zero value turns it off. The default is off.
1010              
1011             =back
1012              
1013             =head2 process()
1014              
1015             Truncates log file if the C option is set to true, deletes expired
1016             records if C option is specified, and processes any new entries.
1017             Returns 1 on success. If a C option is not specified, an error will
1018             be thrown.
1019              
1020             $log_obj->process();
1021              
1022             =head2 get_uids()
1023              
1024             Returns an array of all unique UIDs found in the current database table.
1025             See the L section for an explanation of UIDs in the FEAR
1026             server logs.
1027              
1028             @uids = $log_obj->get_uids();
1029              
1030             =head2 get_playernames(UID)
1031              
1032             Returns an array of all unique playernames found for the given UID. They
1033             are ordered by frequency of use.
1034              
1035             @names = $log_obj->get_playernames($uid);
1036              
1037             =head2 get_stats(UID)
1038              
1039             Returns a hash reference containing averaged and totalled stats for the
1040             given UID. The data structure returned is as follows:
1041              
1042             {
1043             game_count => $game_count,
1044             tot_score => $tot_score,
1045             avg_score => $avg_score,
1046             tot_kills => $tot_kills,
1047             avg_kills => $avg_kills,
1048             tot_deaths => $tot_deaths,
1049             avg_deaths => $avg_deaths,
1050             tot_suicides => $tot_suicides,
1051             avg_suicides => $avg_suicides,
1052             tot_teamkills => $tot_teamkills,
1053             avg_teamkills => $avg_teamkills,
1054             tot_objective => $tot_objective,
1055             avg_objective => $avg_objective,
1056             }
1057              
1058             =head2 get_history(UID)
1059              
1060             Returns a hashref of hashrefs of games played by the given UID, each
1061             keyed to the game time. Note that C is a unix timestamp as
1062             would be returned by the C builtin. The data structure returned
1063             is as follows:
1064              
1065             {
1066             $gametime => {
1067             gametime => $gametime,
1068             team => $team,
1069             player => $player,
1070             score => $score,
1071             kills => $kills,
1072             deaths => $deaths,
1073             teamkills => $teamkills,
1074             suicides => $suicides,
1075             objective => $objective,
1076             },
1077             }
1078              
1079             =head2 get_game(GAMETIME)
1080              
1081             Returns a hashref of hashrefs of players in the game at the given game
1082             time, each keyed to a UID. Note that C is a unix timestamp as
1083             would be returned by the C builtin. The data structure returned
1084             is as follows:
1085              
1086             {
1087             $uid => {
1088             uid => $uid,
1089             team => $team,
1090             player => $player,
1091             score => $score,
1092             kills => $kills,
1093             deaths => $deaths,
1094             teamkills => $teamkills,
1095             suicides => $suicides,
1096             objective => $objective,
1097             }
1098             }
1099              
1100             =head2 build_scoreboard(OFFSET,LENGTH)
1101              
1102             Returns an array of hashrefs ideal for displaying a summary scoreboard.
1103             Takes two optional arguments:
1104              
1105             =over
1106              
1107             =item * OFFSET
1108              
1109             How many records into the start of a resultset to begin retrieving results,
1110             akin to a SQL I clause. The default is C<0>.
1111              
1112             =item * LENGTH
1113              
1114             How many records to retrieve from a resultset, akin to a SQL I clause.
1115             The default is C<0> which is interpreted as 'no limit'.
1116              
1117             =back
1118              
1119             The data structure returned is as follows:
1120              
1121             (
1122             {
1123             uid => $uid,
1124             player => $player,
1125             avg_score => $avg_score,
1126             avg_kills => $avg_kills,
1127             avg_deaths => $avg_deaths,
1128             avg_suicides => $avg_suicides,
1129             avg_teamkills => $avg_teamkills,
1130             avg_objective => $avg_objective,
1131             tot_score => $tot_score,
1132             tot_kills => $tot_kills,
1133             tot_deaths => $tot_deaths,
1134             tot_suicides => $tot_suicides,
1135             tot_teamkills => $tot_teamkills,
1136             tot_objective => $tot_objective,
1137             }
1138             )
1139              
1140             =head2 DESTROY()
1141              
1142             The class destructor that, when called, closes the database connection
1143             and any open filehandles, and destroys the object.
1144              
1145             =head1 FUNCTIONS
1146              
1147             =head2 supported_dbds()
1148              
1149             Returns a list of the currently supported DBI drivers. This function
1150             can be called from an instantiated object, or directly.
1151              
1152             # called from an object
1153             @drivers = $log_object->supported_dbds();
1154            
1155             # called directly from the module namespace
1156             @drivers = Games::FEAR::Log::supported_dbds();
1157              
1158             =head1 JARGON
1159              
1160             Here, a few of the terms used throughout this documentation are briefly
1161             defined.
1162              
1163             =head2 UID
1164              
1165             The UID found in the FEAR multiplayer log is used to uniquely identify a
1166             user. It is calculated as a hexadecimal
1167             MD5 hash of their CD key. For example, the UID for a CD key of
1168             C would be C.
1169              
1170             =head2 Player Name
1171              
1172             The player name is, as it suggests, a name picked by the user and is how
1173             they appear in-game. It is not suitable for tracking statistics since it
1174             can be changed at the user's discretion, so we use the UID for that purpose.
1175              
1176             =head2 Game Time
1177              
1178             The game time is a timestamp of precisely when a specific game ended. By
1179             matching up different players with the same game times, you can determine
1180             the participants of any specific game.
1181              
1182             =head1 DEPENDANCIES
1183              
1184             L - Used by the test suite during C
1185              
1186             L - Used for database connectivity
1187              
1188             L - Used to copy log during processing
1189              
1190             L - Used to create temp file during processing
1191              
1192             =head1 AUTHOR
1193              
1194             Evan Kaufman, C<< >>
1195              
1196             =head1 BUGS
1197              
1198             Please report any bugs or feature requests to
1199             C, or through the web interface at
1200             L.
1201             I will be notified, and then you'll automatically be notified of progress on
1202             your bug as I make changes.
1203              
1204             =head1 ACKNOWLEDGEMENTS
1205              
1206             The fine folks at PerlMonks.org, always willing to lend a helping hand to a
1207             struggling programmer.
1208              
1209             =head1 COPYRIGHT
1210              
1211             Copyright 2007 Evan Kaufman, all rights reserved.
1212              
1213             This program is free software; you can redistribute it and/or modify it
1214             under the same terms as Perl itself.
1215              
1216             =cut
1217              
1218             }
1219             1; # End of Games::FEAR::Log