File Coverage

blib/lib/POE/Component/Client/Halo.pm
Criterion Covered Total %
statement 21 193 10.8
branch 0 66 0.0
condition 0 56 0.0
subroutine 7 21 33.3
pod 2 13 15.3
total 30 349 8.6


line stmt bran cond sub pod time code
1             package POE::Component::Client::Halo;
2              
3 1     1   682 use strict;
  1         2  
  1         30  
4              
5 1     1   5 use vars qw($VERSION);
  1         2  
  1         66  
6             $VERSION = '0.2';
7              
8             sub DEBUG () { 0 };
9              
10 1     1   4 use Carp qw(croak);
  1         5  
  1         60  
11 1     1   854 use Socket;
  1         3472  
  1         596  
12 1     1   3275 use Data::Dumper;
  1         19519  
  1         77  
13 1     1   1753 use POE qw(Session Wheel::SocketFactory);
  1         81197  
  1         11  
14              
15 1         3166 use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS
16 1     1   122079 $player_flags $game_flags);
  1         3  
17             @ISA = 'Exporter';
18             @EXPORT_OK = qw(halo_player_flag halo_game_flag);
19             %EXPORT_TAGS = (
20             'flags' => [qw(halo_player_flag halo_game_flag)],
21             );
22              
23             $player_flags = {
24             'NumberOfLives' => ['Infinite', 1, 3, 5],
25             'MaximumHealth' => ['50%', '100%', '150%', '200%', '300%', '400%'],
26             'Shields' => [1, 0],
27             'RespawnTime' => [0, 5, 10, 15],
28             'RespawnGrowth' => [0, 5, 10, 15],
29             'OddManOut' => [0, 1],
30             'InvisiblePlayers' => [0, 1],
31             'SuicidePenalty' => [0, 5, 10, 15],
32             'InfiniteGrenades' => [0, 1],
33             'WeaponSet' => ['Normal', 'Pistols', 'Rifles', 'Plasma', 'Sniper',
34             'No Sniping', 'Rocket Launchers', 'Shotguns',
35             'Short Range', 'Human', 'Covenant', 'Classic',
36             'Heavy Weapons'],
37             'StartingEquipment' => ['Custom', 'Generic'],
38             'Indicator' => ['Motion Tracker', 'Nav Points', 'None'],
39             'OtherPlayersOnRadar' => ['No', 'All', undef, 'Friends'],
40             'FriendIndicators' => [0, 1],
41             'FriendlyFire' => ['Off', 'On', 'Shields Only', 'Explosives Only'],
42             'FriendlyFirePenalty' => [0, 5, 10, 15],
43             'AutoTeamBalance' => [0, 1],
44              
45             # Team Flags
46             'VehicleRespawn' => [0, 30, 60, 90, 120, 180, 300],
47             'RedVehicleSet' => ['Default', undef, 'Warthogs', 'Ghosts',
48             'Scorpions', 'Rocket Warthogs', 'Banshees',
49             'Gun Turrets', 'Custom'],
50             'BlueVehicleSet' => ['Default', undef, 'Warthogs', 'Ghosts',
51             'Scorpions', 'Rocket Warthogs', 'Banshees',
52             'Gun Turrets', 'Custom'],
53             };
54              
55             $game_flags = {
56             'GameType' => ['Capture the Flag', 'Slayer', 'Oddball',
57             'King of the Hill', 'Race'],
58             # CTF
59             'Assault' => [0, 1],
60             'FlagMustReset' => [0, 1],
61             'FlagAtHomeToScore' => [0, 1],
62             'SingleFlag' => [0, 60, 120, 180, 300, 600],
63             # Slayer
64             'DeathBonus' => [1, 0],
65             'KillPenalty' => [1, 0],
66             'KillInOrder' => [0, 1],
67             # Oddball
68             'RandomStart' => [0, 1],
69             'SpeedWithBall' => ['Slow', 'Normal', 'Fast'],
70             'TraitWithBall' => ['None', 'Invisible', 'Extra Damage', 'Damage Resistant'],
71             'TraitWithoutBall' => ['None', 'Invisible', 'Extra Damage', 'Damage Resistant'],
72             'BallType' => ['Normal', 'Reverse Tag', 'Juggernaut'],
73             'BallSpawnCount' => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
74             # King of the Hill
75             'MovingHill' => [0, 1],
76             # Race
77             'RaceType' => ['Normal', 'Any Order', 'Rally'],
78             'TeamScoring' => ['Minimum', 'Maximum', 'Sum'],
79             };
80              
81             sub new {
82 0     0 0   my $type = shift;
83 0           my $self = bless {}, $type;
84              
85 0 0         croak "$type requires an event number of parameters" if @_ % 2;
86              
87 0           my %params = @_;
88              
89 0           my $alias = delete $params{Alias};
90 0 0         $alias = 'halo' unless defined $alias;
91              
92 0           my $timeout = delete $params{Timeout};
93 0 0 0       $timeout = 15 unless defined $timeout and $timeout >= 0;
94              
95 0           my $retry = delete $params{Retry};
96 0 0 0       $retry = 2 unless defined $retry and $retry >= 0;
97              
98 0 0         croak "$type doesn't know these parameters: ", join(', ', sort(keys(%params))) if scalar(keys(%params));
99              
100 0           POE::Session->create(
101             inline_states => {
102             _start => \&_start,
103             info => \&info,
104             detail => \&detail,
105              
106             got_socket => \&got_socket,
107             got_response => \&got_response,
108             response_timeout => \&response_timeout,
109             debug_heap => \&debug_heap,
110              
111             got_error => \&got_error,
112             },
113             args => [ $timeout, $retry, $alias ],
114             );
115              
116 0           return $self;
117             }
118              
119             sub got_error {
120 0     0 0   my ($operation, $errnum, $errstr, $wheel_id, $heap) = @_[ARG0..ARG3,HEAP];
121 0           warn "Wheel $wheel_id generated $operation error $errnum: $errstr\n";
122 0           delete $heap->{w_jobs}->{$wheel_id}; # shut down that wheel
123             }
124              
125             sub debug_heap {
126 0     0 0   my ($kernel, $heap) = @_[KERNEL, HEAP];
127 0 0         open(F, ">/tmp/halo-debug") || return;
128 0           print F Dumper($heap);
129 0 0         close(F) || return;
130 0           $kernel->delay('debug_heap', 10);
131             }
132              
133             sub _start {
134 0     0     my ($kernel, $heap, $timeout, $retry, $alias) = @_[KERNEL, HEAP, ARG0..ARG3];
135 0           $heap->{timeout} = $timeout;
136 0           $heap->{retry} = $retry;
137 0           $kernel->alias_set($alias);
138 0           print STDERR "Halo object started.\n" if DEBUG;
139 0           $kernel->yield('debug_heap') if DEBUG;
140             }
141              
142             sub info {
143 0     0 1   my ($kernel, $heap, $sender, $ip, $port, $postback) = @_[KERNEL, HEAP, SENDER, ARG0..ARG2];
144 0 0         my ($identifier) = defined($_[ARG3]) ? $_[ARG3] : undef;
145 0           print STDERR "Got request for $ip:$port info with postback $postback\n" if DEBUG;
146 0 0         croak "IP address required to execute a query" unless defined $ip;
147 0 0 0       croak "Port requred to execute a query" if !defined $port || $port !~ /^\d+$/;
148 0           my $wheel = POE::Wheel::SocketFactory->new(
149             RemoteAddress => $ip,
150             RemotePort => $port,
151             SocketProtocol => 'udp',
152             SuccessEvent => 'got_socket',
153             FailureEvent => 'got_error',
154             );
155 0           $heap->{w_jobs}->{$wheel->ID()} = {
156             ip => $ip,
157             port => $port,
158             postback => $postback,
159             session => $sender->ID(),
160             wheel => $wheel,
161             identifier => $identifier,
162             try => 1, # number of tries...
163             action => 'info',
164             };
165 0           return undef;
166             }
167              
168             sub detail {
169 0     0 1   my ($kernel, $heap, $sender, $ip, $port, $postback) = @_[KERNEL, HEAP, SENDER, ARG0..ARG2];
170 0 0         my ($identifier) = defined($_[ARG3]) ? $_[ARG3] : undef;
171 0           print STDERR "Got request for $ip:$port players with postback $postback\n" if DEBUG;
172 0 0         croak "IP address required to execute a query" unless defined $ip;
173 0 0 0       croak "Port requred to execute a query" if !defined $port || $port !~ /^\d+$/;
174 0           my $wheel = POE::Wheel::SocketFactory->new(
175             RemoteAddress => $ip,
176             RemotePort => $port,
177             SocketProtocol => 'udp',
178             SuccessEvent => 'got_socket',
179             FailureEvent => 'got_error',
180             );
181 0           $heap->{w_jobs}->{$wheel->ID()} = {
182             ip => $ip,
183             port => $port,
184             postback => $postback,
185             session => $sender->ID(),
186             wheel => $wheel,
187             identifier => $identifier,
188             try => 1, # number of tries...
189             action => 'detail',
190             };
191 0           return undef;
192             }
193              
194             sub got_socket {
195 0     0 0   my ($kernel, $heap, $socket, $wheelid) = @_[KERNEL, HEAP, ARG0, ARG3];
196              
197 0           $heap->{jobs}->{$socket} = delete($heap->{w_jobs}->{$wheelid});
198 0           $kernel->select_read($socket, 'got_response');
199 0           my $query = '';
200 0 0         if($heap->{jobs}->{$socket}->{action} eq 'info') {
    0          
201 0           $query = "\x9c\xb7\x70\x02\x0a\x01\x03\x08\x0a\x05\x06\x13\x33\x36\x0c\x00\x00";
202             } elsif($heap->{jobs}->{$socket}->{action} eq 'detail') {
203 0           $query = "\x33\x8f\x02\x00\xff\xff\xff";
204             } else {
205 0           die("Unknown action!");
206             }
207 0           send($socket, "\xFE\xFD\x00" . $query, 0);
208 0           $heap->{jobs}->{$socket}->{timer} = $kernel->delay_set('response_timeout', $heap->{timeout}, $socket);
209 0           print STDERR "Wheel $wheelid got socket and sent request\n" if DEBUG;
210             }
211              
212             sub got_response {
213 0     0 0   my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];
214              
215 0           my $action = $heap->{jobs}->{$socket}->{action};
216              
217 0           $kernel->select_read($socket);
218 0 0         $kernel->alarm_remove($heap->{jobs}->{$socket}->{timer}) if defined $heap->{jobs}->{$socket}->{timer};
219 0           delete $heap->{jobs}->{$socket}->{timer};
220 0           my $rsock = recv($socket, my $response = '', 16384, 0);
221              
222 0           my %data;
223 0 0         if($response eq '') {
    0          
    0          
224 0           $data{ERROR} = 'DOWN';
225             } elsif($action eq 'info') {
226 0           $response = substr($response, 5);
227 0           my @parts = split(/\x00/, $response);
228 0           $data{'Hostname'} = $parts[0];
229 0           $data{'Version'} = $parts[1];
230 0           $data{'Players'} = $parts[2];
231 0           $data{'MaxPlayers'} = $parts[3];
232 0           $data{'Map'} = $parts[4];
233 0           $data{'Mode'} = $parts[5];
234 0           $data{'Password'} = $parts[6];
235 0           $data{'Dedicated'} = $parts[7];
236 0           $data{'Classic'} = $parts[8];
237 0           $data{'Teamplay'} = $parts[9];
238             } elsif($action eq 'detail') {
239 0           $response =~ s/\x00+$//;
240 0           my ($rules, $players, $score) = ($response =~ /^.{5}(.+?)\x00{3}[\x00-\x10](.+)\x00{2}[\x02\x00](.+$)/);
241 0           my @parts = split(/\x00/, $response);
242 0           %{$data{'Rules'}} = split(/\x00/, $rules);
  0            
243 0           $data{'PlayerFlags'} = decode_player_flags($data{'Rules'}{'player_flags'});
244 0           $data{'GameFlags'} = decode_game_flags($data{'Rules'}{'game_flags'});
245 0           $data{'Players'} = process_segment($players);
246 0           $data{'Score'} = process_segment($score);
247             } else {
248 0           die("Unknown request!");
249             }
250              
251 0           $kernel->post($heap->{jobs}->{$socket}->{session},
252             $heap->{jobs}->{$socket}->{postback},
253             $heap->{jobs}->{$socket}->{ip},
254             $heap->{jobs}->{$socket}->{port},
255             $heap->{jobs}->{$socket}->{action},
256             $heap->{jobs}->{$socket}->{identifier},
257             \%data);
258 0           delete($heap->{jobs}->{$socket});
259             }
260              
261             sub decode_player_flags {
262 0     0 0   my $str = shift;
263 0           my $flags = { };
264 0 0 0       return $flags if $str eq '' || $str !~ /^\d+\,\d+$/;
265              
266 0           my ($player, $vehicle) = split(/\,/, $str);
267              
268 0           $flags->{'Player'}->{'NumberOfLives'} = $player & 3;
269 0           $flags->{'Player'}->{'MaximumHealth'} = ($player >> 2) & 7;
270 0           $flags->{'Player'}->{'Shields'} = ($player >> 5) & 1;
271 0           $flags->{'Player'}->{'RespawnTime'} = ($player >> 6) & 3;
272 0           $flags->{'Player'}->{'RespawnGrowth'} = ($player >> 8) & 3;
273 0           $flags->{'Player'}->{'OddManOut'} = ($player >> 10) & 1;
274 0           $flags->{'Player'}->{'InvisiblePlayers'} = ($player >> 11) & 1;
275 0           $flags->{'Player'}->{'SuicidePenalty'} = ($player >> 12) & 3;
276 0           $flags->{'Player'}->{'InfiniteGrenades'} = ($player >> 14) & 1;
277 0           $flags->{'Player'}->{'WeaponSet'} = ($player >> 15) & 15;
278 0           $flags->{'Player'}->{'StartingEquipment'} = ($player >> 19) & 1;
279 0           $flags->{'Player'}->{'Indicator'} = ($player >> 20) & 3;
280 0           $flags->{'Player'}->{'OtherPlayersOnRadar'} = ($player >> 22) & 3;
281 0           $flags->{'Player'}->{'FriendIndicators'} = ($player >> 24) & 1;
282 0           $flags->{'Player'}->{'FriendlyFire'} = ($player >> 25) & 3;
283 0           $flags->{'Player'}->{'FriendlyFirePenalty'} = ($player >> 27) & 3;
284 0           $flags->{'Player'}->{'AutoTeamBalance'} = ($player >> 29) & 1;
285              
286 0           $flags->{'Team'}->{'VehicleRespawn'} = ($vehicle & 7);
287 0           $flags->{'Team'}->{'RedVehicleSet'} = ($vehicle >> 3) & 15;
288 0           $flags->{'Team'}->{'BlueVehicleSet'} = ($vehicle >> 7) & 15;
289              
290 0           return $flags;
291             }
292              
293             sub decode_game_flags {
294 0     0 0   my $str = shift;
295 0           my $flags = { };
296 0 0 0       return $flags if $str eq '' || $str !~ /^\d+$/;
297              
298 0           $flags->{'GameType'} = $str & 7;
299 0 0         if($flags->{'GameType'} == 1) { # CTF
    0          
    0          
    0          
    0          
300 0   0       $flags->{'Assault'} = ($str >> 3) && 1;
301 0   0       $flags->{'FlagMustReset'} = ($str >> 5) && 1;
302 0   0       $flags->{'FlagAtHomeToScore'} = ($str >> 6) && 1;
303 0   0       $flags->{'SingleFlag'} = ($str >> 7) && 7;
304             } elsif($flags->{'GameType'} == 2) { # Slayer
305 0   0       $flags->{'DeathBonus'} = ($str >> 3) && 1;
306 0   0       $flags->{'KillPenalty'} = ($str >> 5) && 1;
307 0   0       $flags->{'KillInOrder'} = ($str >> 6) && 1;
308             } elsif($flags->{'GameType'} == 3) { # Oddball
309 0   0       $flags->{'RandomStart'} = ($str >> 3) && 1;
310 0   0       $flags->{'SpeedWithBall'} = ($str >> 5) && 3;
311 0   0       $flags->{'TraitWithBall'} = ($str >> 7) && 3;
312 0   0       $flags->{'TraitWithoutBall'} = ($str >> 9) && 3;
313 0   0       $flags->{'BallType'} = ($str >> 11) && 3;
314 0   0       $flags->{'BallSpawnCount'} = ($str >> 13) && 31;
315             } elsif($flags->{'GameType'} == 4) { # Hill
316 0   0       $flags->{'MovingHill'} = ($str >> 3) && 1;
317             } elsif($flags->{'GameType'} == 5) { # Race
318 0   0       $flags->{'RaceType'} = ($str >> 3) && 3;
319 0   0       $flags->{'TeamScoring'} = ($str >> 5) && 3;
320             }
321              
322 0           return $flags;
323             }
324              
325             sub halo_player_flag {
326 0     0 0   my ($flag_name, $flag_value) = (shift, shift);
327              
328 0 0 0       if(defined($player_flags->{$flag_name}) &&
329             defined($player_flags->{$flag_name}->[$flag_value])) {
330 0           return $player_flags->{$flag_name}->[$flag_value];
331             } else {
332 0           return undef;
333             }
334             }
335              
336             sub halo_game_flag {
337 0     0 0   my ($flag_name, $flag_value) = (shift, shift);
338              
339 0 0 0       if(defined($game_flags->{$flag_name}) &&
340             defined($game_flags->{$flag_name}->[$flag_value])) {
341 0           return $game_flags->{$flag_name}->[$flag_value];
342             } else {
343 0           return undef;
344             }
345             }
346              
347             sub response_timeout {
348 0     0 0   my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];
349 0 0         if($heap->{jobs}->{$socket}->{try} > ($heap->{retry} + 1)) {
350 0           $kernel->post($heap->{jobs}->{$socket}->{session}, $heap->{jobs}->{$socket}->{postback},
351             $heap->{jobs}->{$socket}->{ip},
352             $heap->{jobs}->{$socket}->{port},
353             $heap->{jobs}->{$socket}->{action},
354             $heap->{jobs}->{$socket}->{identifier},
355             { 'ERROR' => 'Timed out waiting for a response.'});
356 0           delete($heap->{jobs}->{$socket});
357             } else {
358 0           print STDERR "Query timed out for $socket. Retrying.\n" if DEBUG;
359 0           my $query = '';
360 0 0         if($heap->{jobs}->{$socket}->{action} eq 'info') {
    0          
361 0           $query = "\x9c\xb7\x70\x02\x0a\x01\x03\x08\x0a\x05\x06\x13\x33\x36\x0c\x00\x00";
362             } elsif($heap->{jobs}->{$socket}->{action} eq 'detail') {
363 0           $query = "\x33\x8f\x02\x00\xff\xff\xff";
364             } else {
365 0           die("Unknown action!");
366             }
367 0           send($socket, "\xFE\xFD\x00" . $query, 0);
368 0           $heap->{jobs}->{$socket}->{timer} = $kernel->delay_set('response_timeout', $heap->{timeout}, $socket);
369 0           $heap->{jobs}->{$socket}->{try}++;
370             }
371             }
372              
373             sub process_segment {
374 0     0 0   my $str = shift;
375              
376 0           my @parts = split(/\x00/, $str);
377 0           my @fields = ();
378 0           foreach(@parts) {
379 0 0         last if $_ eq '';
380 0           s/_.*$//;
381 0           push(@fields, $_);
382             }
383 0           my $info = {};
384 0           my $ctr = 0;
385 0           my $cur_item = '';
386 0           foreach(splice(@parts, scalar(@fields) + 1)) {
387 0 0         if($ctr % scalar(@fields) == 0) {
388 0           $cur_item = $_;
389 0           $info->{$cur_item}->{$fields[0]} = $cur_item;
390             } else {
391 0           $info->{$cur_item}->{$fields[$ctr % scalar(@fields)]} = $_;
392             }
393 0           $ctr++;
394             }
395 0           return $info;
396             }
397              
398             1;
399              
400             __END__