POE::Component::Client::Halo - an implementation of the Halo query


POE-Component-Client-Halo documentation Contained in the POE-Component-Client-Halo distribution.

Index


Code Index:

NAME

Top

  POE::Component::Client::Halo -- an implementation of the Halo query 
  protocol.

SYNOPSIS

Top

  use Data::Dumper; # for the sample below
  use POE qw(Component::Client::Halo);

  my $halo = new POE::Component::Client::Halo(
        Alias => 'halo',
        Timeout => 15,
        Retry => 2,
  );

  $kernel->post('halo', 'info', '127.0.0.1', 2302, 'pbhandler', 'ident');

  $kernel->post('halo', 'detail', '127.0.0.1', 2302, 'pbhandler', 'ident');

  sub postback_handler {
      my ($ip, $port, $command, $identifier, $response) = @_;
      print "Halo query $command_executed on ";
      print " at $ip:$port";
      print " had a identifier of $identifier" if defined $identifier;
      print " returned from the server with:";
      print Dumper($response), "\n\n";
  }

DESCRIPTION

Top

POE::Component::Client::Halo is an implementation of the Halo query protocol. It was reverse engineered with a sniffer and two cups of coffee. This is a preliminary release, based version 1.00.01.0580 of the dedicated server (the first public release). It is capable of handling multiple requests of different types in parallel.

PoCo::Client::Halo new can take a few parameters:

Alias => $alias_name

Alias sets the name of the Halo component with which you will post events to. By default, this is 'halo'.

Timeout => $timeout_in_seconds

Timeout specifies the number of seconds to wait for each step of the query procedure. The number of steps varies depending on the server being accessed.

Retry => $number_of_times_to_retry

Retry sets the number of times PoCo::Client::Halo should retry query requests. Since queries are UDP based, there is always the chance of your packets being dropped or lost. After the number of retries has been exceeded, an error is posted back to the session you specified to accept postbacks.

METHODS

Top

There are two methods that can be exported through the tag ':flags' -- halo_player_flag() and halo_game_flag(). They can be used to translate a specific game flag into its English equivalent.

  $english_value = halo_player_flag($flag_name, $flag_value);
  $english_value = halo_game_flag($flag_name, $flag_value);

EVENTS

Top

You can send two types of events to PoCo::Client::Halo.

info

This will request the basic info block from the Halo server. In the postback, you will get 4 or 5 arguments, depending on whether or not you had a postback. ARG0 is the IP, ARG1 is the port, ARG3 is the command (for info queries, this will always be 'info'), ARG4 is a hashref with the returned data, and ARG5 is your unique identifier as set during your original post. Here are the fields you'll get back in ARG4:

* Map
* Teamplay
* Classic
* Mode
* MaxPlayers
* Hostname
* Password
* Version
* Dedicated
* Players

detail

This request more detailed information about the server, as well as its rules, player information, and team score. Like 'info', you'll get 4-5 arguments passed to your postback function. ARG4 contains a HoHoH's: { 'Score' => { 'Red' => { 'team' => 'Red', 'score' => '17' }, 'Blue' => { 'team' => 'Blue', 'score' => '17' } }, 'Players' => { 'ZETA' => { 'score' => '0', 'team' => '0', 'ping' => '', 'player' => 'ZETA' }, 'badmofo' => { 'score' => '3', 'team' => '0', 'ping' => '', 'player' => 'badmofo' }, }, 'Rules' => { 'gametype' => 'Slayer', 'hostport' => '', 'fraglimit' => '50', 'mapname' => 'dangercanyon', 'gamever' => '01.00.01.0580', 'teamplay' => '1', 'password' => '0', 'game_flags' => '26', 'player_flags' => '1941966980,2', 'game_classic' => '0', 'gamevariant' => 'Team Slayer', 'gamemode' => 'openplaying', 'hostname' => 'DivoNetworks', 'maxplayers' => '16', 'dedicated' => '1', 'numplayers' => '2' } };

At the time of this module's release, ping information was not available within the info packets. They might be made public later on, so I left them in the response.

You can translate the player and game flags with the two methods mentioned above. Take a look at the sample program to see how it's done.

ERRORS

Top

The errors listed below are ones that will be posted back to you in the 'response' field.

* ERROR: Timed out waiting for response

Even after retrying, there was no response to your request command.

There are other fatal errors that are handled with croak().

BUGS

Top

No tests are distributed with the module yet. There is a sample, though.

ACKNOWLEDGEMENTS

Top

Rocco Caputo

Yay!

Divo Networks

Thanks for loaning me servers to test against.

Brian Hurley

He decoded all the player and game flags after several long nights. Thanks.

AUTHOR & COPYRIGHTS

Top

POE::Component::Client::Halo is Copyright 2001-2003 by Andrew A. Chen <achen-poe-halo@divo.net>. All rights are reserved. POE::Component::Client::Halo is free software; you may redistribute it and/or modify it under the same terms as Perl itself.


POE-Component-Client-Halo documentation Contained in the POE-Component-Client-Halo distribution.

package POE::Component::Client::Halo;

use strict;

use vars qw($VERSION);
$VERSION = '0.2';

sub DEBUG ()  { 0 };

use Carp qw(croak);
use Socket;
use Data::Dumper;
use POE qw(Session Wheel::SocketFactory);

use vars qw(@ISA @EXPORT_OK %EXPORT_TAGS
            $player_flags $game_flags);
@ISA = 'Exporter';
@EXPORT_OK = qw(halo_player_flag halo_game_flag);
%EXPORT_TAGS = (
    'flags' => [qw(halo_player_flag halo_game_flag)],
);

$player_flags = {
    'NumberOfLives'     => ['Infinite', 1, 3, 5],
    'MaximumHealth'     => ['50%', '100%', '150%', '200%', '300%', '400%'],
    'Shields'           => [1, 0],
    'RespawnTime'       => [0, 5, 10, 15],
    'RespawnGrowth'     => [0, 5, 10, 15],
    'OddManOut'         => [0, 1],
    'InvisiblePlayers'  => [0, 1],
    'SuicidePenalty'    => [0, 5, 10, 15],
    'InfiniteGrenades'  => [0, 1],
    'WeaponSet'         => ['Normal', 'Pistols', 'Rifles', 'Plasma', 'Sniper', 
                            'No Sniping', 'Rocket Launchers', 'Shotguns', 
                            'Short Range', 'Human', 'Covenant', 'Classic', 
                            'Heavy Weapons'],
    'StartingEquipment' => ['Custom', 'Generic'],
    'Indicator'         => ['Motion Tracker', 'Nav Points', 'None'],
    'OtherPlayersOnRadar'   => ['No', 'All', undef, 'Friends'],
    'FriendIndicators'  => [0, 1],
    'FriendlyFire'      => ['Off', 'On', 'Shields Only', 'Explosives Only'],
    'FriendlyFirePenalty'   => [0, 5, 10, 15],
    'AutoTeamBalance'   => [0, 1],

    # Team Flags
    'VehicleRespawn'    => [0, 30, 60, 90, 120, 180, 300],
    'RedVehicleSet'     => ['Default', undef, 'Warthogs', 'Ghosts', 
                            'Scorpions', 'Rocket Warthogs', 'Banshees', 
                            'Gun Turrets', 'Custom'],
    'BlueVehicleSet'     => ['Default', undef, 'Warthogs', 'Ghosts', 
                            'Scorpions', 'Rocket Warthogs', 'Banshees', 
                            'Gun Turrets', 'Custom'],
};

$game_flags = {
    'GameType'          => ['Capture the Flag', 'Slayer', 'Oddball', 
                            'King of the Hill', 'Race'],
    # CTF
    'Assault'           => [0, 1],
    'FlagMustReset'     => [0, 1],
    'FlagAtHomeToScore' => [0, 1],
    'SingleFlag'        => [0, 60, 120, 180, 300, 600],
    # Slayer
    'DeathBonus'        => [1, 0],
    'KillPenalty'       => [1, 0],
    'KillInOrder'       => [0, 1],
    # Oddball
    'RandomStart'       => [0, 1],
    'SpeedWithBall'     => ['Slow', 'Normal', 'Fast'],
    'TraitWithBall'     => ['None', 'Invisible', 'Extra Damage', 'Damage Resistant'],
    'TraitWithoutBall'  => ['None', 'Invisible', 'Extra Damage', 'Damage Resistant'],
    'BallType'          => ['Normal', 'Reverse Tag', 'Juggernaut'],
    'BallSpawnCount'    => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
    # King of the Hill
    'MovingHill'        => [0, 1],
    # Race
    'RaceType'          => ['Normal', 'Any Order', 'Rally'],
    'TeamScoring'       => ['Minimum', 'Maximum', 'Sum'],
};

sub new {
    my $type = shift;
    my $self = bless {}, $type;

    croak "$type requires an event number of parameters" if @_ % 2;

    my %params = @_;

    my $alias = delete $params{Alias};
    $alias = 'halo' unless defined $alias;

    my $timeout = delete $params{Timeout};
    $timeout = 15 unless defined $timeout and $timeout >= 0;

    my $retry = delete $params{Retry};
    $retry = 2 unless defined $retry and $retry >= 0;

    croak "$type doesn't know these parameters: ", join(', ', sort(keys(%params))) if scalar(keys(%params));

    POE::Session->create(
        inline_states => {
            _start            => \&_start,
            info            => \&info,
            detail          => \&detail,

            got_socket        => \&got_socket,
            got_response        => \&got_response,
            response_timeout    => \&response_timeout,
            debug_heap        => \&debug_heap,

            got_error        => \&got_error,
        },
        args => [ $timeout, $retry, $alias ],
    );

    return $self;
}

sub got_error {
    my ($operation, $errnum, $errstr, $wheel_id, $heap) = @_[ARG0..ARG3,HEAP];
    warn "Wheel $wheel_id generated $operation error $errnum: $errstr\n";
    delete $heap->{w_jobs}->{$wheel_id}; # shut down that wheel
}

sub debug_heap {
    my ($kernel, $heap) = @_[KERNEL, HEAP];
    open(F, ">/tmp/halo-debug") || return;
    print F Dumper($heap);
    close(F) || return;
    $kernel->delay('debug_heap', 10);
}

sub _start {
    my ($kernel, $heap, $timeout, $retry, $alias) = @_[KERNEL, HEAP, ARG0..ARG3];
    $heap->{timeout} = $timeout;
    $heap->{retry} = $retry;
    $kernel->alias_set($alias);
    print STDERR "Halo object started.\n" if DEBUG;
    $kernel->yield('debug_heap') if DEBUG;
}

sub info {
    my ($kernel, $heap, $sender, $ip, $port, $postback) = @_[KERNEL, HEAP, SENDER, ARG0..ARG2];
    my ($identifier) = defined($_[ARG3]) ? $_[ARG3] : undef;
    print STDERR "Got request for $ip:$port info with postback $postback\n" if DEBUG;
    croak "IP address required to execute a query" unless defined $ip;
    croak "Port requred to execute a query" if !defined $port || $port !~ /^\d+$/;
    my $wheel = POE::Wheel::SocketFactory->new(
            RemoteAddress    => $ip,
            RemotePort    => $port,
            SocketProtocol    => 'udp',
            SuccessEvent    => 'got_socket',
            FailureEvent    => 'got_error',
    );
    $heap->{w_jobs}->{$wheel->ID()} = {
        ip        => $ip,
        port        => $port,
        postback    => $postback,
        session        => $sender->ID(),
        wheel        => $wheel,
        identifier    => $identifier,
        try        => 1,    # number of tries...
        action        => 'info',
    };
    return undef;
}

sub detail {
    my ($kernel, $heap, $sender, $ip, $port, $postback) = @_[KERNEL, HEAP, SENDER, ARG0..ARG2];
    my ($identifier) = defined($_[ARG3]) ? $_[ARG3] : undef;
    print STDERR "Got request for $ip:$port players with postback $postback\n" if DEBUG;
    croak "IP address required to execute a query" unless defined $ip;
    croak "Port requred to execute a query" if !defined $port || $port !~ /^\d+$/;
    my $wheel = POE::Wheel::SocketFactory->new(
            RemoteAddress    => $ip,
            RemotePort    => $port,
            SocketProtocol    => 'udp',
            SuccessEvent    => 'got_socket',
            FailureEvent    => 'got_error',
    );
    $heap->{w_jobs}->{$wheel->ID()} = {
        ip        => $ip,
        port        => $port,
        postback    => $postback,
        session        => $sender->ID(),
        wheel        => $wheel,
        identifier    => $identifier,
        try        => 1,    # number of tries...
        action        => 'detail',
    };
    return undef;
}

sub got_socket {
    my ($kernel, $heap, $socket, $wheelid) = @_[KERNEL, HEAP, ARG0, ARG3];

    $heap->{jobs}->{$socket} = delete($heap->{w_jobs}->{$wheelid});
    $kernel->select_read($socket, 'got_response');
    my $query = '';
    if($heap->{jobs}->{$socket}->{action} eq 'info') {
        $query = "\x9c\xb7\x70\x02\x0a\x01\x03\x08\x0a\x05\x06\x13\x33\x36\x0c\x00\x00";
    } elsif($heap->{jobs}->{$socket}->{action} eq 'detail') {
        $query = "\x33\x8f\x02\x00\xff\xff\xff";
    } else {
        die("Unknown action!");
    }
    send($socket, "\xFE\xFD\x00" . $query, 0);
    $heap->{jobs}->{$socket}->{timer} = $kernel->delay_set('response_timeout', $heap->{timeout}, $socket);
    print STDERR "Wheel $wheelid got socket and sent request\n" if DEBUG;
}

sub got_response {
    my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];

    my $action = $heap->{jobs}->{$socket}->{action};

    $kernel->select_read($socket);
    $kernel->alarm_remove($heap->{jobs}->{$socket}->{timer}) if defined $heap->{jobs}->{$socket}->{timer};
    delete $heap->{jobs}->{$socket}->{timer};
    my $rsock = recv($socket, my $response = '', 16384, 0);

    my %data;
    if($response eq '') {
        $data{ERROR} = 'DOWN';
    } elsif($action eq 'info') {
        $response = substr($response, 5);
        my @parts = split(/\x00/, $response);
        $data{'Hostname'} = $parts[0];
        $data{'Version'} = $parts[1];
        $data{'Players'} = $parts[2];
        $data{'MaxPlayers'} = $parts[3];
        $data{'Map'} = $parts[4];
        $data{'Mode'} = $parts[5];
        $data{'Password'} = $parts[6];
        $data{'Dedicated'} = $parts[7];
        $data{'Classic'} = $parts[8];
        $data{'Teamplay'} = $parts[9];
    } elsif($action eq 'detail') {
        $response =~ s/\x00+$//;
        my ($rules, $players, $score) = ($response =~ /^.{5}(.+?)\x00{3}[\x00-\x10](.+)\x00{2}[\x02\x00](.+$)/);
        my @parts = split(/\x00/, $response);
        %{$data{'Rules'}} = split(/\x00/, $rules);
        $data{'PlayerFlags'} = decode_player_flags($data{'Rules'}{'player_flags'});
        $data{'GameFlags'} = decode_game_flags($data{'Rules'}{'game_flags'});
        $data{'Players'} = process_segment($players);
        $data{'Score'} = process_segment($score);
    } else {
        die("Unknown request!");
    }

    $kernel->post($heap->{jobs}->{$socket}->{session}, 
              $heap->{jobs}->{$socket}->{postback}, 
              $heap->{jobs}->{$socket}->{ip},
              $heap->{jobs}->{$socket}->{port},
              $heap->{jobs}->{$socket}->{action},
              $heap->{jobs}->{$socket}->{identifier},
              \%data);
    delete($heap->{jobs}->{$socket});
}

sub decode_player_flags {
    my $str = shift;
    my $flags = { };
    return $flags if $str eq '' || $str !~ /^\d+\,\d+$/;

    my ($player, $vehicle) = split(/\,/, $str);

    $flags->{'Player'}->{'NumberOfLives'} = $player & 3;
    $flags->{'Player'}->{'MaximumHealth'} = ($player >> 2) & 7;
    $flags->{'Player'}->{'Shields'} = ($player >> 5) & 1;
    $flags->{'Player'}->{'RespawnTime'} = ($player >> 6) & 3;
    $flags->{'Player'}->{'RespawnGrowth'} = ($player >> 8) & 3;
    $flags->{'Player'}->{'OddManOut'} = ($player >> 10) & 1;
    $flags->{'Player'}->{'InvisiblePlayers'} = ($player >> 11) & 1;
    $flags->{'Player'}->{'SuicidePenalty'} = ($player >> 12) & 3;
    $flags->{'Player'}->{'InfiniteGrenades'} = ($player >> 14) & 1;
    $flags->{'Player'}->{'WeaponSet'} = ($player >> 15) & 15;
    $flags->{'Player'}->{'StartingEquipment'} = ($player >> 19) & 1;
    $flags->{'Player'}->{'Indicator'} = ($player >> 20) & 3;
    $flags->{'Player'}->{'OtherPlayersOnRadar'} = ($player >> 22) & 3;
    $flags->{'Player'}->{'FriendIndicators'} = ($player >> 24) & 1;
    $flags->{'Player'}->{'FriendlyFire'} = ($player >> 25) & 3;
    $flags->{'Player'}->{'FriendlyFirePenalty'} = ($player >> 27) & 3;
    $flags->{'Player'}->{'AutoTeamBalance'} = ($player >> 29) & 1;

    $flags->{'Team'}->{'VehicleRespawn'} = ($vehicle & 7);
    $flags->{'Team'}->{'RedVehicleSet'} = ($vehicle >> 3) & 15;
    $flags->{'Team'}->{'BlueVehicleSet'} = ($vehicle >> 7) & 15;

    return $flags;
}

sub decode_game_flags {
    my $str = shift;
    my $flags = { };
    return $flags if $str eq '' || $str !~ /^\d+$/;

    $flags->{'GameType'} = $str & 7;
    if($flags->{'GameType'} == 1) { # CTF
        $flags->{'Assault'} = ($str >> 3) && 1;
        $flags->{'FlagMustReset'} = ($str >> 5) && 1;
        $flags->{'FlagAtHomeToScore'} = ($str >> 6) && 1;
        $flags->{'SingleFlag'} = ($str >> 7) && 7;
    } elsif($flags->{'GameType'} == 2) {    # Slayer
        $flags->{'DeathBonus'} = ($str >> 3) && 1;
        $flags->{'KillPenalty'} = ($str >> 5) && 1;
        $flags->{'KillInOrder'} = ($str >> 6) && 1;
    } elsif($flags->{'GameType'} == 3) {    # Oddball
        $flags->{'RandomStart'} = ($str >> 3) && 1;
        $flags->{'SpeedWithBall'} = ($str >> 5) && 3;
        $flags->{'TraitWithBall'} = ($str >> 7) && 3;
        $flags->{'TraitWithoutBall'} = ($str >> 9) && 3;
        $flags->{'BallType'} = ($str >> 11) && 3;
        $flags->{'BallSpawnCount'} = ($str >> 13) && 31;
    } elsif($flags->{'GameType'} == 4) {    # Hill
        $flags->{'MovingHill'} = ($str >> 3) && 1;
    } elsif($flags->{'GameType'} == 5) {    # Race
        $flags->{'RaceType'} = ($str >> 3) && 3;
        $flags->{'TeamScoring'} = ($str >> 5) && 3;
    }

    return $flags;
}

sub halo_player_flag {
    my ($flag_name, $flag_value) = (shift, shift);

    if(defined($player_flags->{$flag_name}) && 
       defined($player_flags->{$flag_name}->[$flag_value])) {
        return $player_flags->{$flag_name}->[$flag_value];
    } else {
        return undef;
    }
}

sub halo_game_flag {
    my ($flag_name, $flag_value) = (shift, shift);

    if(defined($game_flags->{$flag_name}) && 
       defined($game_flags->{$flag_name}->[$flag_value])) {
        return $game_flags->{$flag_name}->[$flag_value];
    } else {
        return undef;
    }
}

sub response_timeout {
    my ($kernel, $heap, $socket) = @_[KERNEL, HEAP, ARG0];
    if($heap->{jobs}->{$socket}->{try} > ($heap->{retry} + 1)) {
        $kernel->post($heap->{jobs}->{$socket}->{session}, $heap->{jobs}->{$socket}->{postback},
                $heap->{jobs}->{$socket}->{ip},
                $heap->{jobs}->{$socket}->{port},
                $heap->{jobs}->{$socket}->{action},
                $heap->{jobs}->{$socket}->{identifier},
                { 'ERROR' => 'Timed out waiting for a response.'});
        delete($heap->{jobs}->{$socket});
    } else {
        print STDERR "Query timed out for $socket.  Retrying.\n" if DEBUG;
        my $query = '';
        if($heap->{jobs}->{$socket}->{action} eq 'info') {
            $query = "\x9c\xb7\x70\x02\x0a\x01\x03\x08\x0a\x05\x06\x13\x33\x36\x0c\x00\x00";
        } elsif($heap->{jobs}->{$socket}->{action} eq 'detail') {
            $query = "\x33\x8f\x02\x00\xff\xff\xff";
        } else {
            die("Unknown action!");
        }
        send($socket, "\xFE\xFD\x00" . $query, 0);
        $heap->{jobs}->{$socket}->{timer} = $kernel->delay_set('response_timeout', $heap->{timeout}, $socket);
        $heap->{jobs}->{$socket}->{try}++;
    }
}

sub process_segment {
    my $str = shift;

    my @parts = split(/\x00/, $str);
    my @fields = ();
    foreach(@parts) {
        last if $_ eq '';
        s/_.*$//;
        push(@fields, $_);
    }
    my $info = {};
    my $ctr = 0;
    my $cur_item = '';
    foreach(splice(@parts, scalar(@fields) + 1)) {
        if($ctr % scalar(@fields) == 0) {
            $cur_item = $_;
            $info->{$cur_item}->{$fields[0]} = $cur_item;
        } else {
            $info->{$cur_item}->{$fields[$ctr % scalar(@fields)]} = $_;
        }
        $ctr++;
    }
    return $info;
}

1;

__END__