File Coverage

blib/lib/Games/AssaultCube/Log/Line.pm
Criterion Covered Total %
statement 24 118 20.3
branch 24 174 13.7
condition 3 9 33.3
subroutine 6 7 85.7
pod 0 4 0.0
total 57 312 18.2


line stmt bran cond sub pod time code
1             # Declare our package
2             package Games::AssaultCube::Log::Line;
3 2     2   3626 use strict; use warnings;
  2     2   5  
  2         92  
  2         18  
  2         5  
  2         84  
4              
5             # Initialize our version
6 2     2   15 use vars qw( $VERSION );
  2         5  
  2         9715  
7             $VERSION = '0.04';
8              
9             # the factory constructor
10             sub new {
11 1     1 0 58 my $class = shift;
12 1         2 my $line = shift;
13 1         3 my $subclass = shift;
14              
15             # sanity checking
16 1 50 33     10 if ( ! defined $line or ! length $line ) {
17 0         0 die "Please supply a valid line";
18             }
19              
20             # Do we have a subclass to hand off the line?
21 1 50       5 if ( defined $subclass ) {
22             # object or coderef?
23 0 0       0 if ( ref( $subclass ) eq 'CODE' ) {
24 0         0 my $result = $subclass->( $line );
25 0 0       0 if ( defined $result ) {
26 0         0 return $result;
27             }
28             } else {
29 0         0 my $result = $subclass->parse( $line );
30 0 0       0 if ( defined $result ) {
31 0         0 return $result;
32             }
33             }
34             }
35              
36             # parse the line!
37 1         6 return parse( $line );
38             }
39              
40             sub create_subclass {
41 1     1 0 3 my $line = shift;
42 1         2 my $event = shift;
43 1         3 my $data = shift;
44              
45             # Load the subclass we need
46 1         100 eval "require Games::AssaultCube::Log::Line::$event"; ## no critic (ProhibitStringyEval)
47 1 50       13 if ( $@ ) {
48 1         8 die "Unable to load our subclass: $@";
49             }
50              
51             # Store the data
52 0         0 $data->{'event'} = $event;
53 0         0 $data->{'line'} = $line;
54              
55             # create the object!
56 0         0 return "Games::AssaultCube::Log::Line::$event"->new( $data );
57             }
58              
59             sub parse {
60 1     1 0 2 my $text = shift;
61 1 50 33     70 if ($text =~ m!^\[([\d\.]*)\]\s+(.+)$!) {
    50 33        
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    50          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
62 0         0 my $ip = $1;
63 0         0 my $etext = $2;
64 0 0       0 if ($ip) {
65 0 0       0 if ($etext =~ m!^([^\s]+)\s+(fragged|gibbed)\s+(his\s+teammate\s+)?(.+)$!) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
    0          
66 0 0       0 return create_subclass($text,'Killed',{
    0          
67             nick => $1,
68             gib => $2 eq 'gibbed' ? 1 : 0,
69             tk => defined $3 ? 1 : 0,
70             victim => $4,
71             ip => $ip,
72             });
73             } elsif ($etext =~ m!^([^\s]+)\s+says(?:\s+to\s+team\s+(\w+))?:\s+\'(.*)\'(,\s+SPAM\s+detected)?$!) {
74             # TODO what should we do with $2, the team name?
75 0 0       0 return create_subclass($text,'Says',{
    0          
76             nick => $1,
77             isteam => defined $2 ? 1 : 0,
78             text => $3,
79             spam => defined $4 ? 1 : 0,
80             ip => $ip,
81             });
82             } elsif ($etext =~ m!^disconnected\s+client\s+(.*)$!) {
83 0 0       0 return create_subclass($text,'ClientDisconnected',{
84             ( defined $1 ? ( nick => $1 ) : () ),
85             ip => $ip,
86             forced => 0,
87             });
88             } elsif ($etext =~ m!^disconnecting\s+client\s+([^\s]*)\s+\((.+)\)$!) {
89 0         0 return create_subclass($text,'ClientDisconnected',{
90             nick => $1,
91             reason => $2,
92             ip => $ip,
93             forced => 1,
94             });
95             } elsif ($etext eq 'client connected') {
96 0         0 return create_subclass($text,'ClientConnected',{
97             ip => $ip,
98             });
99             } elsif ($etext =~ m!^([^\s]+)\s+(dropped|lost|returned|stole)\s+the\s+flag$!) {
100 0         0 return create_subclass($text,'Flag' . ucfirst( $2 ),{
101             nick => $1,
102             ip => $ip,
103             });
104             } elsif ($etext =~ m!^runs\s+AC\s+(\d+)\s+\(defs:\s+(.+)\)$!) {
105 0         0 return create_subclass($text,'ClientVersion',{
106             version => $1,
107             defs => $2,
108             ip => $ip,
109             });
110             } elsif ($etext =~ m!^([^\s]+)\s+scored\s+with\s+the\s+flag\s+for\s+(\w+),\s+new\s+score\s+(-?\d+)$!) {
111 0         0 return create_subclass($text,'FlagScored',{
112             nick => $1,
113             team_name => $2,
114             score => $3,
115             ip => $ip,
116             });
117             } elsif ($etext =~ m!^client\s+([^\s]+)\s+(failed\s+to\s+)?call(?:ed)?\s+a\s+vote:\s+(.*)$!) {
118 0         0 my $nick = $1;
119 0         0 my $failure = $2;
120 0         0 my $vote = $3;
121 0 0       0 if ( defined $failure ) {
122 0         0 $failure = 1;
123             } else {
124 0         0 $failure = 0;
125             }
126              
127             # parse the vote...
128 0 0       0 if (! length $vote) {
    0          
    0          
    0          
    0          
    0          
    0          
    0          
129 0         0 return create_subclass($text,'CallVote',{
130             nick => $nick,
131             type => 'invalid',
132             failure => 1,
133             target => 'invalid',
134             failure_reason => 'empty vote',
135             ip => $ip,
136             });
137             } elsif ($vote =~ m!^load\s+map\s+\'(.*)\'\s+in\s+mode\s+\'(.+)\'(?:\s+\((.+)\))?$!) {
138 0 0       0 return create_subclass($text,'CallVote',{
139             nick => $nick,
140             type => 'loadmap',
141             failure => $failure,
142             target => $1 . ' - ' . $2,
143             ip => $ip,
144             ( defined $3 ? ( failure_reason => $3 ) : () ),
145             });
146             } elsif ($vote =~ m!^(\w+)\s+player\s+([^\s]*)(?:\s+to\s+the\s+enemy\s+team)?(?:\s+\((.+)\))?$!) {
147 0 0       0 return create_subclass($text,'CallVote',{
148             nick => $nick,
149             type => $1,
150             failure => $failure,
151             target => $2,
152             ip => $ip,
153             ( defined $3 ? ( failure_reason => $3 ) : () ),
154             });
155             } elsif ($vote =~ m!^shuffle\s+teams(?:\s+\((.+)\))?$!) {
156 0 0       0 return create_subclass($text,'CallVote',{
157             nick => $nick,
158             type => 'shuffle',
159             failure => $failure,
160             target => 'teams',
161             ip => $ip,
162             ( defined $2 ? ( failure_reason => $2 ) : () ),
163             });
164             } elsif ($vote =~ m!^(enable|disable|remove|stop)\s+([^\(]+)(?:\s+\((.+)\))?$!) {
165 0 0       0 return create_subclass($text,'CallVote',{
166             nick => $nick,
167             type => $1,
168             failure => $failure,
169             target => $2,
170             ip => $ip,
171             ( defined $3 ? ( failure_reason => $3 ) : () ),
172             });
173             } elsif ($vote =~ m!^change\s+(.+)(?:\s+\((.+)\))?$!) {
174 0 0       0 return create_subclass($text,'CallVote',{
175             nick => $nick,
176             type => 'change',
177             failure => $failure,
178             target => $1,
179             ip => $ip,
180             ( defined $2 ? ( failure_reason => $2 ) : () ),
181             });
182             } elsif ($vote =~ m!^set\s+(.+)(?:\s+\((.+)\))?$!) {
183 0 0       0 return create_subclass($text,'CallVote',{
184             nick => $nick,
185             type => 'set',
186             failure => $failure,
187             target => $1,
188             ip => $ip,
189             ( defined $2 ? ( failure_reason => $2 ) : () ),
190             });
191             } elsif ($vote =~ m!^\((.+)\)$!) {
192 0         0 return create_subclass($text,'CallVote',{
193             nick => $nick,
194             type => 'invalid',
195             failure => $failure,
196             target => 'invalid',
197             failure_reason => $1,
198             ip => $ip,
199             });
200             } else {
201 0         0 die "unknown vote type: $text - $vote";
202             }
203             } elsif ($etext =~ m!^(.+)\s+suicided$!) {
204 0         0 return create_subclass($text,'Suicide',{
205             nick => $1,
206             ip => $ip,
207             });
208             } elsif ($etext =~ m!^([^\s]+)\s+changed\s+his\s+name\s+to\s+(.+)$!) {
209 0         0 return create_subclass($text,'ClientNickChange',{
210             oldnick => $1,
211             nick => $2,
212             ip => $ip,
213             });
214             } elsif ($etext =~ m!^([^\s]+)\s+scored,\s+carrying\s+for\s+(\d+)\s+seconds,\s+new\s+score\s+(\d+)$!) {
215 0         0 return create_subclass($text,'FlagScoredKTF',{
216             nick => $1,
217             carried => $2,
218             score => $3,
219             ip => $ip,
220             });
221             } elsif ($etext =~ m!^set\s+role\s+of\s+player\s+([^\s]+)\s+to\s+(\w+)(?:\s+player)?$!) {
222             # AC prints "normal" instead of "default", argh!
223 0         0 my $role = 'ADMIN';
224 0 0       0 if ( $2 eq 'normal' ) {
225 0         0 $role = 'DEFAULT';
226             }
227              
228 0         0 return create_subclass($text,'ClientChangeRole',{
229             nick => $1,
230             role_name => $role,
231             ip => $ip,
232             });
233             } elsif ($etext =~ m!^player\s+([^\s]+)\s+used\s+admin\s+password\s+in\s+line\s+(\d+)$!) {
234 0         0 return create_subclass($text,'ClientAdmin',{
235             nick => $1,
236             password => $2,
237             ip => $ip,
238             });
239             } elsif ($etext =~ m!^([^\s]+)\s+got\s+forced\s+to\s+pickup\s+the\s+flag$!) {
240 0         0 return create_subclass($text,'FlagForcedPickup',{
241             nick => $1,
242             ip => $ip,
243             });
244             } elsif ($etext =~ m!^([^\s]+)\s+failed\s+to\s+score$!) {
245 0         0 return create_subclass($text,'FlagFailedScore',{
246             nick => $1,
247             ip => $ip,
248             });
249             } elsif ($etext =~ m!^logged\s+in\s+using\s+the\s+admin\s+password\s+in\s+line\s+(\d+)(,\s+\(ban\s+removed\))?$!) {
250 0 0       0 return create_subclass($text,'ClientAdmin',{
251             password => $1,
252             ip => $ip,
253             ( defined $2 ? ( unbanned => 1 ) : () ),
254             });
255              
256             # ARGH, sometimes ac_server "overflows" and fails to print the message properly...
257             } elsif ($etext =~ m!^([^\s]+)\s+says(?:\s+to\s+team\s+(\w+))?:\s+\'([^']+)$!) {
258             # TODO what should we do with $2, the team name?
259 0 0       0 return create_subclass($text,'Says',{
260             nick => $1,
261             isteam => defined $2 ? 1 : 0,
262             text => $3,
263             spam => 0,
264             ip => $ip,
265             });
266             } else {
267 0         0 return create_subclass($text,'Unknown',{
268             ip => $ip,
269             text => $etext,
270             });
271             }
272             } else {
273 0         0 return create_subclass($text,'Unknown',{});
274             }
275             } elsif ($text =~ m!^\s*(\d+)\s+([^\s]+)\s+(\w+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(\w+)\s+([\d\.]+)$!) {
276 0 0       0 return create_subclass($text,'ClientStatus',{
277             cn => $1,
278             nick => $2,
279             team_name => $3,
280             frags => $4,
281             deaths => $5,
282             flags => $6,
283             role_name => ( $7 eq 'normal' ? 'DEFAULT' : 'ADMIN' ), # AC prints "normal" instead of "default", argh!
284             ip => $8,
285             });
286             } elsif ($text =~ m!^\s*(\d+)\s+([^\s]+)\s+(\w+)\s+(-?\d+)\s+(-?\d+)\s+(\w+)\s+([\d\.]+)$!) {
287 0 0       0 return create_subclass($text,'ClientStatus',{
288             cn => $1,
289             nick => $2,
290             team_name => $3,
291             frags => $4,
292             deaths => $5,
293             role_name => ( $6 eq 'normal' ? 'DEFAULT' : 'ADMIN' ), # AC prints "normal" instead of "default", argh!
294             ip => $7,
295             });
296             } elsif ($text =~ m!^\s*(\d+)\s+([^\s]+)\s+(-?\d+)\s+(-?\d+)\s+(-?\d+)\s+(\w+)\s+([\d\.]+)$!) {
297 0 0       0 return create_subclass($text,'ClientStatus',{
298             cn => $1,
299             nick => $2,
300             team_name => 'NONE',
301             frags => $3,
302             deaths => $4,
303             flags => $5,
304             role_name => ( $6 eq 'normal' ? 'DEFAULT' : 'ADMIN' ), # AC prints "normal" instead of "default", argh!
305             ip => $7,
306             });
307             } elsif ($text =~ m!^\s*(\d+)\s+([^\s]+)\s+(-?\d+)\s+(-?\d+)\s+(\w+)\s+([\d\.]+)$!) {
308 0 0       0 return create_subclass($text,'ClientStatus',{
309             cn => $1,
310             nick => $2,
311             team_name => 'NONE',
312             frags => $3,
313             deaths => $4,
314             role_name => ( $5 eq 'normal' ? 'DEFAULT' : 'ADMIN' ), # AC prints "normal" instead of "default", argh!
315             ip => $6,
316             });
317             } elsif ($text =~ m!^Team\s+(\w+):\s+(\d+)\s+players,\s+(-?\d+)\s+frags(?:,\s+(-?\d+)\s+flags)?$!) {
318 0 0       0 return create_subclass($text,'TeamStatus',{
319             team_name => $1,
320             players => $2,
321             frags => $3,
322             ( defined $4 ? ( flags => $4 ) : () ),
323             });
324             } elsif ($text =~ m!^Game\s+status:\s+(.+)\s+on\s+([^,]*),\s+(\d+)\s+[^,]+,\s+(\w+)$!) {
325 0 0       0 return create_subclass($text,'GameStatus',{
326             gamemode_fullname => ( $1 eq 'ctf' ? 'capture the flag' : $1 ), # ctf is annoying because of our "substitution" from 'ctf' to "capture the flag"
327             'map' => $2,
328             minutes => $3,
329             mastermode_name => uc( $4 ),
330             finished => 0,
331             });
332             } elsif ($text =~ m!^Game\s+status:\s+(.+)\s+on\s+([^,]+),\s+game finished,\s+(\w+)$!) {
333 0 0       0 return create_subclass($text,'GameStatus',{
334             gamemode_fullname => ( $1 eq 'ctf' ? 'capture the flag' : $1 ), # ctf is annoying because of our "substitution" from 'ctf' to "capture the flag"
335             'map' => $2,
336             minutes => 0,
337             mastermode_name => uc( $3 ),
338             finished => 1,
339             });
340             } elsif ($text =~ m!cn\s+name\s+(?:team\s+)?frag\s+death\s+(?:flags\s+)?role\s+host$!) {
341 0         0 return create_subclass($text,'ScoreboardStart',{});
342             } elsif ($text =~ m!^Status\s+at\s+(\d+)-(\d+)-(\d+)\s+(\d+):(\d+):(\d+):\s+(\d+)\s+remote\s+clients,\s+([\d\.]+)\s+send,\s+([\d\.]+)\s+rec\s+\(K/sec\)$!) {
343 0         0 require DateTime;
344 0         0 my $datetime = DateTime->new(
345             year => $3,
346             month => $2,
347             day => $1,
348             hour => $4,
349             minute => $5,
350             second => $6,
351             );
352 0         0 return create_subclass($text,'Status',{
353             datetime => $datetime,
354             players => $7,
355             sent => $8,
356             'recv' => $9,
357             });
358             } elsif ($text =~ m!^Game\s+start:\s+(.+)\s+on\s+([^,]+),\s+(\d+)\s+[^,]+,\s+(\d+)\s+[^,]+,\s+mastermode\s+(\d+)!) {
359 0 0       0 return create_subclass($text,'GameStart',{
360             gamemode_fullname => ( $1 eq 'ctf' ? 'capture the flag' : $1 ), # ctf is annoying because of our "substitution" from 'ctf' to "capture the flag"
361             'map' => $2,
362             players => $3,
363             minutes => $4,
364             mastermode => $5,
365             });
366             } elsif ($text =~ m!^at-target:\s+(-?\d+),\s+(.+)\s+pick:(\d+)$!) {
367 0         0 return create_subclass($text,'AutoBalance',{
368             target => $1,
369 0         0 players => { map { split( /:/, $_, 2 ) } split( ' ', $2 ) },
370             pick => $3,
371             });
372             } elsif ($text =~ m!^the\s+server\s+reset\s+the\s+flag\s+for\s+team\s+(\w+)$!) {
373 0         0 return create_subclass($text,'FlagReset',{
374             team_name => $1,
375             });
376             } elsif ($text =~ m!^sending\s+request\s+to\s+(.+)...$!) {
377 0         0 return create_subclass($text,'MasterserverRequest',{
378             server => $1,
379             });
380             } elsif ($text =~ m!^masterserver\s+reply:\s+(.*)$!) {
381 0         0 return create_subclass($text,'MasterserverReply',{
382             reply => $1,
383             success => 1,
384             });
385             } elsif ($text eq 'Registration successful. Due to caching it might take a few minutes to see the your server in the serverlist') {
386 0         0 return create_subclass($text,'MasterserverReply',{
387             reply => 'Registration successful. Due to caching it might take a few minutes to see the your server in the serverlist',
388             success => 1,
389             });
390             } elsif ($text eq 'Server not registered, could not ping you. Make sure your server is accessible from the internet.') {
391 0         0 return create_subclass($text,'MasterserverReply',{
392             reply => 'Server not registered, could not ping you. Make sure your server is accessible from the internet.',
393             success => 0,
394             });
395             } elsif ($text eq 'logging local AssaultCube server now..' or $text eq 'dedicated server started, waiting for clients...' or $text eq 'Ctrl-C to exit') {
396 0         0 return create_subclass($text,'StartupText',{});
397             } elsif ($text =~ m!^loaded\s+map\s+([^,]+),\s+(\d+)\s+\+\s+(\d+)\((\d+)\)\s+bytes\.$!) {
398             # cleanup the map name
399 0         0 my( $mapname, $mapsize, $cfgsize, $cfgzsize ) = ( $1, $2, $3, $4 );
400 0 0       0 if ( $mapname =~ /([^\\\/]+)\.cgz$/ ) {
401 0         0 $mapname = $1;
402             } else {
403 0         0 die "unable to parse mapname: $mapname";
404             }
405              
406 0         0 return create_subclass($text,'LoadedMap',{
407             'map' => $mapname,
408             mapsize => $mapsize,
409             cfgsize => $cfgsize,
410             cfgzsize => $cfgzsize,
411             });
412             } elsif ($text =~ m!^read\s+(\d+)\s+\((\d+)\)\s+blacklist\s+entries\s+from\s+(.+)$!) {
413 0         0 return create_subclass($text,'BlacklistEntries',{
414             count => $1,
415             count_secondary => $2,
416             config => $3,
417             });
418             } elsif ($text =~ m!^read\s+(\d+)\s+admin\s+passwords\s+from\s+(.*)$!) {
419 1         9 return create_subclass($text,'AdminPasswords',{
420             count => $1,
421             config => $2,
422             });
423             } elsif ($text =~ m!^looking\s+up\s+(.+)\.\.\.!) {
424 0           return create_subclass($text,'DNSLookup',{
425             host => $1,
426             });
427             } elsif ($text =~ m!^map\s+\"([^\"]+)\"\s+does\s+not\s+support\s+\"([^\"]+)\":\s+(.+)$!) {
428 0 0         return create_subclass($text,'MapError',{
429             'map' => $1,
430             gamemode_fullname => ( $2 eq 'ctf' ? 'capture the flag' : $2 ), # ctf is annoying because of our "substitution" from 'ctf' to "capture the flag"
431             error => $3,
432             });
433             } elsif ($text =~ m!^could\s+not\s+read\s+config\s+file\s+\'(.+)\'$!) {
434 0           return create_subclass($text,'ConfigError',{
435             errortype => 'config read',
436             what => $1,
437             });
438             } elsif ($text =~ m!^maprot\s+error:\s+map\s+\'(.+)\'\s+not\s+found$!) {
439 0           return create_subclass($text,'ConfigError',{
440             errortype => 'maprot missing map',
441             what => $1,
442             });
443             } elsif ($text =~ m!^AssaultCube\s+fatal\s+error:\s+(.*)$!) {
444 0           return create_subclass($text,'FatalError',{
445             error => $1,
446             });
447             } elsif ($text eq 'Demo recording started.' ) {
448 0           return create_subclass($text,'DemoStart',{});
449              
450             # Demo "Tue Feb 17 11:14:58 2009: ctf, bs_dust2_0.6, 1.32MB" recorded.
451             } elsif ($text =~ m!^Demo\s+"\w+\s+(\w+)\s+(\d+)\s+(\d+):(\d+):(\d+)\s+(\d+):\s+([^\,]+),\s+([^\,]+),\s+([\d\.]+)(MB|kB)"\s+recorded.$!) {
452 0           require DateTime;
453 0           my $datetime = DateTime->new(
454             year => $6,
455             month => month2num( $1 ),
456             day => $2,
457             hour => $3,
458             minute => $4,
459             second => $5,
460             );
461              
462             # Figure out the size, ack!
463 0           my $size;
464 0 0         if ( $10 eq 'MB' ) {
465 0           $size = int( $9 * 1024 * 1024 );
466             } else {
467 0           $size = int( $9 * 1024 );
468             }
469              
470 0           return create_subclass($text,'DemoStop',{
471             datetime => $datetime,
472             gamemode_fullname => $7,
473             'map' => $8,
474             size => $size,
475             });
476             } else {
477 0           return create_subclass($text,'Unknown',{
478             text => $text,
479             });
480             }
481             }
482              
483             # TODO find a suitable module for this! ( DateTime::Format::xyz )
484             # We need this for the DemoStop DateTime conversions...
485             {
486             my %month_num = (
487             'Jan' => 1,
488             'Feb' => 2,
489             'Mar' => 3,
490             'Apr' => 4,
491             'May' => 5,
492             'Jun' => 6,
493             'Jul' => 7,
494             'Aug' => 8,
495             'Sep' => 9,
496             'Oct' => 10,
497             'Nov' => 11,
498             'Dec' => 12,
499             );
500             sub month2num {
501 0     0 0   return $month_num{ +shift };
502             }
503             }
504              
505             1;
506             __END__
507              
508             =for stopwords Torsten Raudssus
509             =head1 NAME
510              
511             Games::AssaultCube::Log::Line - Parses an AssaultCube server log line
512              
513             =head1 SYNOPSIS
514              
515             use Games::AssaultCube::Log::Line;
516             open( my $fh, "<", "logfile.log" ) or die "Unable to open logfile: $!";
517             while ( my $line = <$fh> ) {
518             $line =~ s/(?:\n|\r)+//;
519             next if ! length $line;
520             my $log = Games::AssaultCube::Log::Line->new( $line );
521              
522             # play with the data
523             print "LOG: " . $log->event . " happened\n";
524             }
525             close( $fh ) or die "Unable to close logfile: $!";
526              
527             =head1 ABSTRACT
528              
529             Parses an AssaultCube server log line
530              
531             =head1 DESCRIPTION
532              
533             This module takes an AssaultCube logfile line as parameter and converts this into an easily-accessed
534             object. Please look at the subclasses for all possible event types. This is the factory which handles
535             the "generic" stuff and the parsing. The returned object actually is a subclass which inherits from
536             L<Games::AssaultCube::Log::Line::Base> and contains the various accessors suited for that event type.
537              
538             You would need to set up the "fluff" to read the logfile and feed lines into this parser as shown in the
539             SYNOPSIS.
540              
541             =head2 Constructor
542              
543             The constructor for this class is the "new()" method. The constructor accepts only one argument, the
544             log line to parse. The constructor will return an object or die() if the line is undef/zero-length.
545              
546             Furthermore, you can supply an optional second argument: the subclass parser. It can be an object or a
547             coderef. Please review the notes below, L</"Subclassing the parser">.
548              
549             It is important to remember that this class is simply a "factory" that parses the log line and hands the
550             data off to the appropriate subclass. It is this subclass that is actually returned from the constructor.
551             This means this class has no methods/subs/attributes attached to it and you should study the subclasses for
552             details :)
553              
554             =head2 Subclassing the parser
555              
556             Since AssaultCube is open source, it is feasible for people to modify the server code to output other
557             forms of logs. It makes sense for us to provide a way for others to make use of this code, and extend
558             it to take their log format into account. What follows will be a description on how this is achieved.
559              
560             In order to subclass the parser, all you need to do is provide either an object or a coderef to the
561             constructor. It will be called before executing the normal parsing code contained in this class. If it's
562             an object, the "parse()" method will be called on it; otherwise the coderef will be called.
563              
564             The subclass can either return undef or a defined result. If it returns undef then we will continue with the
565             normal code. If it was defined, then it will be returned directly to the caller, bypassing the parsing
566             code. That way your subclass can return anything, from an object to a hashref to a simple "1". The implication
567             of this is that your subclass will be called every time this class is instantiated. The arguments is simply
568             the line to be parsed, just like the constructor of this class. From there your subclass can use the
569             L<Games::AssaultCube::Log::Line::Base> object if desired for it's events or anything else.
570              
571             NOTE: Since the subclass would process every line, it is desirable to be very fast. It would be smart to
572             design your log extensions so they are immediately detected by the subclass. A normal AssaultCube log line
573             would look something like:
574              
575             Team RVSF: 9 players, 63 frags, 0 flags
576             Status at 18-02-2009 10:02:56: 19 remote clients, 84.3 send, 5.2 rec (K/sec)
577             [199.203.37.253] abcde fragged fghi
578              
579             It would be extremely beneficial if your "extended" log format includes an easily-recognizable prefix. Some
580             examples would be something like this:
581              
582             *LOG* Player "abcde" scored a 10-kill streak
583             |EXTLOG| Server restarted
584             == Player fghi captured the flag at 3:45 into the game
585              
586             Then, your subclass's parse() method could check the first few characters of the line and immediately return
587             before doing any extensive regex/split/code on it. Here's a sample parse() method for a subclass that uses
588             the object style and the "==" extended log prefix:
589              
590             sub parse {
591             my( $self, $line ) = @_;
592             if ( substr( $line, 0, 2 ) ne '==' ) {
593             return;
594             } else {
595             # Process our own extended log format
596             # Be sure to return a defined result!
597             return 1;
598             }
599             }
600              
601             We assume that if you know enough to extend the AssaultCube sources and add your own logs, you know enough
602             to not cause conflicts in the log formats :) Have fun playing around with AssaultCube!
603              
604             =head1 AUTHOR
605              
606             Apocalypse E<lt>apocal@cpan.orgE<gt>
607              
608             Torsten Raudssus E<lt>torsten@raudssus.deE<gt>
609              
610             Props goes to the BS clan for the support!
611              
612             This project is sponsored by L<http://cubestats.net>
613              
614             =head1 COPYRIGHT AND LICENSE
615              
616             Copyright 2009 by Apocalypse
617              
618             This library is free software; you can redistribute it and/or modify
619             it under the same terms as Perl itself.
620              
621             =cut
622              
623             UNKNOWN log lines: ( none as of now, ha! )
624              
625             Parsing 7666 logfiles...
626             The events in descending order:
627             [Killed] -> 6685565
628             [ClientStatus] -> 1850559
629             [Says] -> 511759
630             [TeamStatus] -> 464180
631             [ClientDisconnected] -> 448772
632             [ClientConnected] -> 348626
633             [GameStatus] -> 265458
634             [ScoreboardStart] -> 264980
635             [FlagStole] -> 262218
636             [Status] -> 252995
637             [ClientVersion] -> 196760
638             [FlagLost] -> 174810
639             [FlagReturned] -> 140226
640             [FlagScored] -> 69272
641             [GameStart] -> 40216
642             [Suicide] -> 30879
643             [AutoBalance] -> 22432
644             [CallVote-loadmap] -> 19789
645             [FlagReset] -> 16775
646             [ClientNickChange] -> 16334
647             [CallVote-kick] -> 15584
648             [MasterserverReply] -> 12085
649             [MasterserverRequest] -> 12038
650             [StartupText] -> 9094
651             [CallVote-ban] -> 6625
652             [LoadedMap] -> 4286
653             [FlagScoredKTF] -> 4220
654             [CallVote-shuffle] -> 4144
655             [CallVote-force] -> 1460
656             [ClientChangeRole] -> 1224
657             [ClientAdmin] -> 1092
658             [BlacklistEntries] -> 761
659             [CallVote-enable] -> 745
660             [AdminPasswords] -> 682
661             [CallVote-remove] -> 674
662             [FlagDropped] -> 596
663             [FlagForcedPickup] -> 548
664             [DNSLookup] -> 498
665             [CallVote-invalid] -> 413
666             [MapError] -> 218
667             [FlagFailedScore] -> 157
668             [CallVote-disable] -> 140
669             [ConfigError] -> 114
670             [FatalError] -> 84
671             [CallVote-change] -> 46
672             [CallVote-stop] -> 11
673             [CallVote-set] -> 2
674             Thank you for waiting!
675              
676             real 25m16.932s
677             user 23m28.120s
678             sys 0m5.220s