=head1 NAME Crossfire::Protocol - client protocol module =head1 SYNOPSIS use base Crossfire::Protocol; # you have to subclass =head1 DESCRIPTION Base class to implement a corssfire client. =over 4 =cut package Crossfire::Protocol; our $VERSION = '0.1'; use strict; use AnyEvent; use IO::Socket::INET; =item new Crossfire::Rptocol host => ..., port => ... =cut sub new { my $class = shift; my $self = bless { @_ }, $class; $self->{fh} = new IO::Socket::INET PeerHost => $self->{host}, PeerPort => $self->{port} or die "$self->{host}:$self->{port}: $!"; $self->{fh}->blocking (0); # stupid nonblock default my $buf; $self->{w} = AnyEvent->io (fh => $self->{fh}, poll => 'r', cb => sub { if (sysread $self->{fh}, $buf, 16384, length $buf) { for (;;) { last unless 2 <= length $buf; my $len = unpack "n", $buf; last unless $len + 2 <= length $buf; substr $buf, 0, 2, ""; $self->feed (substr $buf, 0, $len, ""); } } else { delete $self->{w}; close $self->{fh}; } }); $self->send ("version 1023 1027 perlclient"); $self->send ("setup sound 1 exp 1 map1acmd 1 itemcmd 2 darkness 1 mapsize 63x63 newmapcmd 1 facecache 1 extendedMapInfos 1 extendedTextInfos 1"); $self->send ("addme"); $self } sub feed { my ($self, $data) = @_; $data =~ s/^(\S+)\s// or return; my $command = "feed_$1"; $self->$command ($data); } sub feed_version { my ($self, $version) = @_; } sub feed_setup { my ($self, $data) = @_; $data =~ s/^ +//; $self->{setup} = { split / +/, $data }; ($self->{mapw}, $self->{maph}) = split /x/, $self->{setup}{mapsize}; $self->feed_newmap; } =item $conn->query ($flags, $prompt) [OVERWRITE] =cut sub query { die "query is abstract" } sub feed_query { my ($self, $data) = @_; my ($flags, $prompt) = split /\s+/, $data, 2; if ($flags == 0 && $prompt =~ /^What is your name\?\s+:$/ && length $self->{user}) { $self->send ("reply $self->{user}"); } elsif ($flags == 4 && $prompt =~ /^What is your password\?\s+:$/ && length $self->{pass}) { $self->send ("reply $self->{pass}"); } elsif ($flags == 4 && $prompt =~ /^Please type your password again\.\s+:$/ && length $self->{pass}) { $self->send ("reply $self->{pass}"); } else { $self->query ($flags, $prompt); } } =item $conn->player_update ($player) [OVERWRITE] tag, weight, face, name =cut sub player_update { } sub feed_player { my ($self, $data) = @_; my ($tag, $weight, $face, $name) = unpack "NNN C/a", $data; $self->player_update ($self->{player} = { tag => $tag, weight => $weight, face => $face, name => $name, }); $self->feed_newmap;#d# why??? } =item $conn->stats_update ($stats) [OVERWRITE] =cut sub stats_update { } sub feed_stats { my ($self, $data) = @_; while (length $data) { my $stat = unpack "C", substr $data, 0, 1, ""; my $value; if ($stat == 26 || $stat == 29 || $stat == 30 || $stat == 31 || $stat == 11) { $value = unpack "N", substr $data, 0, 4, ""; } elsif ($stat == 17 || $stat == 19) { $value = (1 / 100000) * unpack "N", substr $data, 0, 4, ""; } elsif ($stat == 20 || $stat == 21) { my $len = unpack "C", substr $data, 0, 1, ""; $value = substr $data, 0, $len, ""; } elsif ($stat == 28) { my ($lo, $hi) = unpack "NN", substr $data, 0, 8, ""; $value = $hi * 2**32 + $lo; } elsif ($stat >= 118 && $stat <= 129) { my ($level, $lo, $hi) = unpack "CNN", substr $data, 0, 9, ""; $value = [$level, $hi * 2**32 + $lo]; } else { $value = unpack "n", substr $data, 0, 2, ""; } $self->{stat}{$stat} = $value; } $self->stats_update ($self->{stat}); } sub feed_face1 { my ($self, $data) = @_; my ($num, $chksum, $name) = unpack "nNa*", $data; my $face = $self->{face}[$num] = { name => $name, chksum => $chksum }; if (my $data = $self->face_find ($face)) { $face->{image} = $data; $self->face_update ($face); } else { $self->send ("askface $num"); } } =item $conn->anim_update ($num) [OVERWRITE] =cut sub anim_update { } sub feed_anim { my ($self, $data) = @_; my ($num, @faces) = unpack "n*", $data; $self->{anim}[$num] = \@faces; $self->anim_update ($num); } sub feed_drawinfo { my ($self, $data) = @_; # warn "<$data>\n"; } sub feed_delinv { my ($self, $data) = @_; } sub feed_item2 { my ($self, $data) = @_; } sub feed_map1a { my ($self, $data) = @_; my $map = $self->{map} ||= []; my @dirty; my ($coord, $x, $y, $darkness, $fa, $fb, $fc, $cell); while (length $data) { $coord = unpack "n", substr $data, 0, 2, ""; $x = ($coord >> 10) & 63; $y = ($coord >> 4) & 63; $cell = $map->[$x][$y] ||= []; $cell->[3] = $coord & 8 ? unpack "C", substr $data, 0, 1, "" : 255; $cell->[0] = unpack "n", substr $data, 0, 2, "" if $coord & 4; $cell->[1] = unpack "n", substr $data, 0, 2, "" if $coord & 2; $cell->[2] = unpack "n", substr $data, 0, 2, "" if $coord & 1; @$cell = () unless $coord & 15; push @dirty, [$x, $y]; } $self->map_update (\@dirty); } sub feed_map_scroll { my ($self, $data) = @_; my ($dx, $dy) = split / /, $data; my $map = $self->{map} ||= []; $self->{mapx} += $dx; $self->{mapy} += $dy; if ($dy < 0) { unshift @$_, ([]) x -$dy for @$map; } elsif ($dy > 0) { splice @$_, 0, $dy, () for @$map; } if ($dx < 0) { unshift @$map, ([]) x -$dx; } elsif ($dx > 0) { splice @$map, 0, $dx, (); } $self->map_scroll ($dx, $dy); } sub feed_newmap { my ($self) = @_; $self->{map} = []; $self->{mapx} = 0; $self->{mapy} = 0; $self->map_clear; } sub feed_image { my ($self, $data) = @_; my ($num, $len, $data) = unpack "NNa*", $data; $self->{face}[$num]{image} = $data; $self->face_update ($self->{face}[$num]); my @dirty; for my $x (0..$self->{mapw} - 1) { for my $y (0..$self->{maph} - 1) { push @dirty, [$x, $y] if grep $_ == $num, @{$self->{map}[$x][$y] || []}; } } $self->map_update (\@dirty); } =item $conn->map_clear [OVERWRITE] Called whenever the map is to be erased completely. =cut sub map_clear { } =item $conn->map_update ([ [x,y], [x,y], ...]) [OVERWRITE] Called with a list of x|y coordinate pairs (as arrayrefs) for cells that have been updated and need refreshing. =cut sub map_update { } =item $conn->map_scroll ($dx, $dy) [OVERWRITE] Called whenever the map has been scrolled. =cut sub map_scroll { } =item $conn->face_update ($face) [OVERWRITE] Called with the face number of face structure whenever a face image has changed. =cut sub face_update { } =item $conn->face_find ($face) [OVERWRITE] Find and return the png image for the given face, or the empty list if no face could be found, in which case it will be requested from the server. =cut sub face_find { } =item $conn->send ($data) Send a single packet/line to the server. =cut sub send { my ($self, $data) = @_; print "SEND<$data>\n";#d# $data = pack "na*", length $data, $data; syswrite $self->{fh}, $data; } =back =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ Robin Redeker http://www.ta-sa.org/ =cut 1