=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::Protocol host => ..., port => ... =cut sub new { my $class = shift; my $self = bless { mapw => 13, maph => 13, max_outstanding => 2, token => "a0", @_ }, $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->{setup_req} = { sound => 1, exp64 => 1, map1acmd => 1, itemcmd => 2, darkness => 1, facecache => 1, newmapcmd => 1, mapinfocmd => 1, plugincmd => 1, extendedTextInfos => 1, }; $self->send ("version 1023 1027 perlclient"); $self->send_setup; $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 }; my ($mapw, $maph) = split /x/, $self->{setup}{mapsize}; if ($mapw != $self->{mapw} || $maph != $self->{maph}) { ($self->{mapw}, $self->{maph}) = ($mapw, $maph); $self->send_setup; } else { $self->send ("addme"); } $self->feed_newmap; } sub feed_addme_success { my ($self, $data) = @_; } sub feed_addme_failure { my ($self, $data) = @_; # maybe should notify user } =back =head2 METHODS THAT CAN/MUST BE OVERWRITTEN =over 4 =cut 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 ($num, $face)) { $face->{image} = $data; $self->face_update ($num, $face); } else { $self->send_queue ("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); } =item $conn->play_sound ($x, $y, $soundnum, $type) =cut sub sound_play { } sub feed_sound { my ($self, $data) = @_; $self->sound_play (unpack "CCnC", $data); } =item $conn->query ($flags, $prompt) =cut sub query { } 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->drawinfo ($color, $text) =cut sub drawinfo { } sub feed_drawinfo { my ($self, $data) = @_; $self->drawinfo (split / /, $data, 2); } =item $conn->player_update ($player) 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, }); } =item $conn->stats_update ($stats) =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}); } =item $conn->inventory_clear ($id) =cut sub inventory_clear { } sub feed_delinv { my ($self, $data) = @_; $self->inventory_clear ($data); delete $self->{inventory}[$data]; } =item $conn->items_delete ($tag...) =cut sub items_delete { } sub feed_delitem { my ($self, $data) = @_; $self->items_delete (unpack "n*", $data); } =item $conn->inventory_add ($id, [\%item...]) =cut sub inventory_add { } sub feed_item2 { my ($self, $data) = @_; my ($location, @values) = unpack "N (NNNN C/a* nC Nn)*", $data; my @items; while (@values) { my ($tag, $flags, $weight, $face, $names, $anim, $animspeed, $nrof, $type) = splice @values, 0, 9, (); my ($name, $name_pl) = split /\x000/, $names; push @items, { tag => $tag, flags => $flags, weight => $weight, face => $face, name => $name, name_pl => $name_pl, anim => $anim, animspeed => $animspeed * 0.120, #??? nrof => $nrof, type => $type, }; } $self->inventory_add ($location, \@items); } =item $conn->item_update ($tag) =cut sub item_update { } sub feed_upditem { #todo } sub feed_map1a { my ($self, $data) = @_; my $map = $self->{map} ||= []; my ($dx, $dy) = delete @$self{qw(delayed_scroll_x delayed_scroll_y)}; if ($dx || $dy) { my ($mx, $my, $mw, $mh) = @$self{qw(mapx mapy mapw maph)}; { my @darkness; if ($dx > 0) { push @darkness, [$mx, $my, $dx - 1, $mh]; } elsif ($dx < 0) { push @darkness, [$mx + $mw + $dx + 1, $my, 1 - $dx, $mh]; } if ($dy > 0) { push @darkness, [$mx, $my, $mw, $dy - 1]; } elsif ($dy < 0) { push @darkness, [$mx, $my + $mh + $dy + 1, $mw, 1 - $dy]; } for (@darkness) { my ($x0, $y0, $w, $h) = @$_; for my $x ($x0 .. $x0 + $w) { for my $y ($y0 .. $y0 + $h) { my $cell = $map->[$x][$y] or next; $cell->[0] = -1; } } } } # now scroll $self->{mapx} += $dx; $self->{mapy} += $dy; # shift in new space if moving to "negative indices" if ($self->{mapy} < 0) { unshift @$_, (undef) x -$self->{mapy} for @$map; $self->{mapy} = 0; } if ($self->{mapx} < 0) { unshift @$map, (undef) x -$self->{mapx}; $self->{mapx} = 0; } $self->map_scroll ($dx, $dy); } 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) + $self->{mapx}; $y = (($coord >> 4) & 63) + $self->{mapy}; $cell = $map->[$x][$y] ||= []; if ($coord & 15) { @$cell = () if $cell->[0] < 0; $cell->[0] = $coord & 8 ? unpack "C", substr $data, 0, 1, "" : 255; $cell->[1] = unpack "n", substr $data, 0, 2, "" if $coord & 4; $cell->[2] = unpack "n", substr $data, 0, 2, "" if $coord & 2; $cell->[3] = unpack "n", substr $data, 0, 2, "" if $coord & 1; } else { $cell->[0] = -1; } push @dirty, [$x, $y]; } $self->map_update (\@dirty); } sub feed_map_scroll { my ($self, $data) = @_; my ($dx, $dy) = split / /, $data; $self->{delayed_scroll_x} += $dx; $self->{delayed_scroll_y} += $dy; $self->map_scroll ($dx, $dy); } sub feed_newmap { my ($self) = @_; $self->{map} = []; $self->{mapx} = 0; $self->{mapy} = 0; delete $self->{delayed_scroll_x}; delete $self->{delayed_scroll_y}; $self->map_clear; } sub feed_mapinfo { my ($self, $data) = @_; my ($token, @data) = split / /, $data; (delete $self->{mapinfo_cb}{$token})->(@data) if $self->{mapinfo_cb}{$token}; $self->map_change (@data) if $token eq "-"; } sub send_mapinfo { my ($self, $data, $cb) = @_; my $token = ++$self->{token}; $self->{mapinfo_cb}{$token} = $cb; $self->send ("mapinfo $token $data"); } sub feed_image { my ($self, $data) = @_; my ($num, $len, $data) = unpack "NNa*", $data; $self->send_queue; $self->{face}[$num]{image} = $data; $self->face_update ($num, $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_change ($mode, ...) [OVERWRITE] current =cut sub map_info { } =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 ($facenum, $facedata) [OVERWRITE] Called with the face number of face structure whenever a face image has changed. =cut sub face_update { } =item $conn->face_find ($facenum, $facedata) [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) = @_; $data = pack "na*", length $data, $data; syswrite $self->{fh}, $data; } sub send_queue { my ($self, $cmd) = @_; if (defined $cmd) { push @{ $self->{send_queue} }, $cmd; } else { --$self->{outstanding}; } if ($self->{outstanding} < $self->{max_outstanding} && @{ $self->{send_queue} }) { ++$self->{outstanding}; $self->send (pop @{ $self->{send_queue} }); } } sub send_setup { my ($self) = @_; my $setup = join " ", setup => %{$self->{setup_req}}, mapsize => "$self->{mapw}x$self->{maph}"; $self->send ($setup); } =back =head1 AUTHOR Marc Lehmann http://home.schmorp.de/ Robin Redeker http://www.ta-sa.org/ =cut 1