ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra/Deliantra/Protocol.pm
Revision: 1.42
Committed: Thu May 25 02:38:17 2006 UTC (18 years ago) by root
Branch: MAIN
Changes since 1.41: +11 -0 lines
Log Message:
support image_info reply

File Contents

# User Rev Content
1 root 1.1 =head1 NAME
2    
3     Crossfire::Protocol - client protocol module
4    
5     =head1 SYNOPSIS
6    
7     use base Crossfire::Protocol; # you have to subclass
8    
9     =head1 DESCRIPTION
10    
11     Base class to implement a corssfire client.
12    
13     =over 4
14    
15     =cut
16    
17     package Crossfire::Protocol;
18    
19     our $VERSION = '0.1';
20    
21     use strict;
22    
23     use AnyEvent;
24     use IO::Socket::INET;
25    
26 root 1.33 BEGIN {
27     my %CONSTANTS = (
28     TICK => 0.120, # one server tick, not exposed through the protocol of course
29     CS_QUERY_YESNO => 0x1,
30     CS_QUERY_SINGLECHAR => 0x2,
31     CS_QUERY_HIDEINPUT => 0x4,
32     CS_SAY_NORMAL => 0x1,
33     CS_SAY_SHOUT => 0x2,
34     CS_SAY_GSAY => 0x4,
35     FLOAT_MULTI => 100000,
36     FLOAT_MULTF => 100000.0,
37     CS_STAT_HP => 1,
38     CS_STAT_MAXHP => 2,
39     CS_STAT_SP => 3,
40     CS_STAT_MAXSP => 4,
41     CS_STAT_STR => 5,
42     CS_STAT_INT => 6,
43     CS_STAT_WIS => 7,
44     CS_STAT_DEX => 8,
45     CS_STAT_CON => 9,
46     CS_STAT_CHA => 10,
47     CS_STAT_EXP => 11,
48     CS_STAT_LEVEL => 12,
49     CS_STAT_WC => 13,
50     CS_STAT_AC => 14,
51     CS_STAT_DAM => 15,
52     CS_STAT_ARMOUR => 16,
53     CS_STAT_SPEED => 17,
54     CS_STAT_FOOD => 18,
55     CS_STAT_WEAP_SP => 19,
56     CS_STAT_RANGE => 20,
57     CS_STAT_TITLE => 21,
58     CS_STAT_POW => 22,
59     CS_STAT_GRACE => 23,
60     CS_STAT_MAXGRACE => 24,
61     CS_STAT_FLAGS => 25,
62     CS_STAT_WEIGHT_LIM => 26,
63     CS_STAT_EXP64 => 28,
64     CS_STAT_SPELL_ATTUNE => 29,
65     CS_STAT_SPELL_REPEL => 30,
66     CS_STAT_SPELL_DENY => 31,
67     CS_STAT_RESIST_START => 100,
68     CS_STAT_RESIST_END => 117,
69     CS_STAT_RES_PHYS => 100,
70     CS_STAT_RES_MAG => 101,
71     CS_STAT_RES_FIRE => 102,
72     CS_STAT_RES_ELEC => 103,
73     CS_STAT_RES_COLD => 104,
74     CS_STAT_RES_CONF => 105,
75     CS_STAT_RES_ACID => 106,
76     CS_STAT_RES_DRAIN => 107,
77     CS_STAT_RES_GHOSTHIT => 108,
78     CS_STAT_RES_POISON => 109,
79     CS_STAT_RES_SLOW => 110,
80     CS_STAT_RES_PARA => 111,
81     CS_STAT_TURN_UNDEAD => 112,
82     CS_STAT_RES_FEAR => 113,
83     CS_STAT_RES_DEPLETE => 114,
84     CS_STAT_RES_DEATH => 115,
85     CS_STAT_RES_HOLYWORD => 116,
86     CS_STAT_RES_BLIND => 117,
87     CS_STAT_SKILLEXP_START => 118,
88     CS_STAT_SKILLEXP_END => 129,
89     CS_STAT_SKILLEXP_AGILITY => 118,
90     CS_STAT_SKILLEXP_AGLEVEL => 119,
91     CS_STAT_SKILLEXP_PERSONAL => 120,
92     CS_STAT_SKILLEXP_PELEVEL => 121,
93     CS_STAT_SKILLEXP_MENTAL => 122,
94     CS_STAT_SKILLEXP_MELEVEL => 123,
95     CS_STAT_SKILLEXP_PHYSIQUE => 124,
96     CS_STAT_SKILLEXP_PHLEVEL => 125,
97     CS_STAT_SKILLEXP_MAGIC => 126,
98     CS_STAT_SKILLEXP_MALEVEL => 127,
99     CS_STAT_SKILLEXP_WISDOM => 128,
100     CS_STAT_SKILLEXP_WILEVEL => 129,
101     CS_STAT_SKILLINFO => 140,
102     CS_NUM_SKILLS => 50,
103     SF_FIREON => 0x01,
104     SF_RUNON => 0x02,
105     NDI_BLACK => 0,
106     NDI_WHITE => 1,
107     NDI_NAVY => 2,
108     NDI_RED => 3,
109     NDI_ORANGE => 4,
110     NDI_BLUE => 5,
111     NDI_DK_ORANGE => 6,
112     NDI_GREEN => 7,
113     NDI_LT_GREEN => 8,
114     NDI_GREY => 9,
115     NDI_BROWN => 10,
116     NDI_GOLD => 11,
117     NDI_TAN => 12,
118     NDI_MAX_COLOR => 12,
119     NDI_COLOR_MASK => 0xff,
120     NDI_UNIQUE => 0x100,
121     NDI_ALL => 0x200,
122     a_none => 0,
123     a_readied => 1,
124     a_wielded => 2,
125     a_worn => 3,
126     a_active => 4,
127     a_applied => 5,
128     F_APPLIED => 0x000F,
129     F_LOCATION => 0x00F0,
130     F_UNPAID => 0x0200,
131     F_MAGIC => 0x0400,
132     F_CURSED => 0x0800,
133     F_DAMNED => 0x1000,
134     F_OPEN => 0x2000,
135     F_NOPICK => 0x4000,
136     F_LOCKED => 0x8000,
137     CF_FACE_NONE => 0,
138     CF_FACE_BITMAP => 1,
139     CF_FACE_XPM => 2,
140     CF_FACE_PNG => 3,
141     CF_FACE_CACHE => 0x10,
142     FACE_FLOOR => 0x80,
143     FACE_COLOR_MASK => 0xf,
144     UPD_LOCATION => 0x01,
145     UPD_FLAGS => 0x02,
146     UPD_WEIGHT => 0x04,
147     UPD_FACE => 0x08,
148     UPD_NAME => 0x10,
149     UPD_ANIM => 0x20,
150     UPD_ANIMSPEED => 0x40,
151     UPD_NROF => 0x80,
152     UPD_SP_MANA => 0x01,
153     UPD_SP_GRACE => 0x02,
154     UPD_SP_DAMAGE => 0x04,
155     SOUND_NORMAL => 0,
156     SOUND_SPELL => 1,
157     );
158    
159     while (my ($k, $v) = each %CONSTANTS) {
160     eval "sub $k () { $v } 1"
161     or die;
162     }
163     }
164 root 1.29
165 root 1.10 =item new Crossfire::Protocol host => ..., port => ...
166 root 1.1
167     =cut
168    
169     sub new {
170     my $class = shift;
171 root 1.10 my $self = bless {
172 root 1.17 mapw => 13,
173     maph => 13,
174 root 1.33 max_outstanding => 2,
175 root 1.24 token => "a0",
176 root 1.10 @_
177     }, $class;
178 root 1.1
179     $self->{fh} = new IO::Socket::INET PeerHost => $self->{host}, PeerPort => $self->{port}
180     or die "$self->{host}:$self->{port}: $!";
181     $self->{fh}->blocking (0); # stupid nonblock default
182    
183     my $buf;
184    
185     $self->{w} = AnyEvent->io (fh => $self->{fh}, poll => 'r', cb => sub {
186 root 1.35 if (0 < sysread $self->{fh}, $buf, 16384, length $buf) {
187 root 1.8 for (;;) {
188     last unless 2 <= length $buf;
189 root 1.1 my $len = unpack "n", $buf;
190 root 1.8 last unless $len + 2 <= length $buf;
191    
192     substr $buf, 0, 2, "";
193     $self->feed (substr $buf, 0, $len, "");
194 root 1.1 }
195     } else {
196 root 1.35 $self->feed_eof;
197 root 1.1 }
198     });
199    
200 root 1.11 $self->{setup_req} = {
201     sound => 1,
202     exp64 => 1,
203     map1acmd => 1,
204     itemcmd => 2,
205     darkness => 1,
206     facecache => 1,
207     newmapcmd => 1,
208 root 1.22 mapinfocmd => 1,
209     plugincmd => 1,
210 root 1.11 extendedTextInfos => 1,
211 root 1.29 spellmon => 1,
212 root 1.11 };
213    
214 root 1.1 $self->send ("version 1023 1027 perlclient");
215 root 1.11 $self->send_setup;
216 root 1.29 $self->send ("requestinfo skill_info");
217     $self->send ("requestinfo spell_paths");
218 root 1.1
219     $self
220     }
221    
222     sub feed {
223     my ($self, $data) = @_;
224    
225 root 1.11 $data =~ s/^(\S+)(?:\s|$)//
226 root 1.1 or return;
227    
228     my $command = "feed_$1";
229    
230     $self->$command ($data);
231     }
232    
233     sub feed_version {
234     my ($self, $version) = @_;
235     }
236    
237     sub feed_setup {
238     my ($self, $data) = @_;
239    
240     $data =~ s/^ +//;
241    
242 root 1.41 my $prev_setup = $self->{setup};
243    
244 root 1.1 $self->{setup} = { split / +/, $data };
245    
246 root 1.41 if ($self->{setup}{extendedTextInfos} > 0 && !$prev_setup) {
247 root 1.35 $self->send ("toggleextendedtext 1"); # books
248     $self->send ("toggleextendedtext 2"); # cards
249     $self->send ("toggleextendedtext 3"); # papers
250     $self->send ("toggleextendedtext 4"); # signs
251     $self->send ("toggleextendedtext 5"); # monuments
252     #$self->send ("toggleextendedtext 6"); # scripted dialogs (yeah)
253     $self->send ("toggleextendedtext 7"); # motd
254     }
255    
256 root 1.10 my ($mapw, $maph) = split /x/, $self->{setup}{mapsize};
257    
258     if ($mapw != $self->{mapw} || $maph != $self->{maph}) {
259     ($self->{mapw}, $self->{maph}) = ($mapw, $maph);
260 root 1.11 $self->send_setup;
261     } else {
262     $self->send ("addme");
263 root 1.10 }
264 root 1.1
265     $self->feed_newmap;
266     }
267    
268 root 1.35 sub feed_eof {
269     my ($self) = @_;
270    
271     delete $self->{w};
272     close delete $self->{fh};
273    
274     for my $tag (sort { $b <=> $a } %{ $self->{container} || {} }) {
275     $self->_del_items (@{ $self->{container}{$tag} });
276     $self->container_clear ($tag);
277     }
278    
279     $self->eof;
280     }
281    
282 root 1.11 sub feed_addme_success {
283     my ($self, $data) = @_;
284 root 1.30
285     $self->addme_success ($data);
286 root 1.11 }
287    
288     sub feed_addme_failure {
289     my ($self, $data) = @_;
290 root 1.30
291     $self->addme_failure ($data);
292 root 1.11 }
293    
294 root 1.35 sub logout {
295     my ($self) = @_;
296    
297     $self->{fh} or return;
298    
299     $self->feed_eof;
300     }
301    
302     sub destroy {
303     my ($self) = @_;
304    
305     $self->logout;
306    
307     %$self = ();
308     }
309    
310 root 1.14 =back
311    
312     =head2 METHODS THAT CAN/MUST BE OVERWRITTEN
313    
314     =over 4
315    
316 root 1.30 =item $self->addme_success
317    
318     =item $self->addme_failure
319    
320 root 1.35 =item $self->eof
321    
322 root 1.14 =cut
323    
324 root 1.30 sub addme_success { }
325     sub addme_failure { }
326 root 1.35 sub eof { }
327 root 1.30
328 root 1.14 sub feed_face1 {
329     my ($self, $data) = @_;
330    
331     my ($num, $chksum, $name) = unpack "nNa*", $data;
332    
333 root 1.29 $self->need_face ($num, $name, $chksum);
334     }
335    
336     sub need_face {
337     my ($self, $num, $name, $chksum) = @_;
338    
339     return if $self->{face}[$num];
340    
341 root 1.14 my $face = $self->{face}[$num] = { name => $name, chksum => $chksum };
342    
343 root 1.23 if (my $data = $self->face_find ($num, $face)) {
344 root 1.14 $face->{image} = $data;
345 root 1.20 $self->face_update ($num, $face);
346 root 1.14 } else {
347 root 1.17 $self->send_queue ("askface $num");
348 root 1.14 }
349     }
350    
351     =item $conn->anim_update ($num) [OVERWRITE]
352    
353     =cut
354    
355     sub anim_update { }
356    
357     sub feed_anim {
358     my ($self, $data) = @_;
359    
360 root 1.38 my ($num, $flags, @faces) = unpack "n*", $data;
361 root 1.14
362     $self->{anim}[$num] = \@faces;
363    
364     $self->anim_update ($num);
365     }
366    
367 root 1.28 =item $conn->sound_play ($x, $y, $soundnum, $type)
368 root 1.11
369     =cut
370    
371     sub sound_play { }
372    
373     sub feed_sound {
374     my ($self, $data) = @_;
375    
376 root 1.28 $self->sound_play (unpack "ccnC", $data);
377 root 1.11 }
378    
379 root 1.14 =item $conn->query ($flags, $prompt)
380 root 1.6
381     =cut
382    
383 root 1.14 sub query { }
384 root 1.6
385 root 1.1 sub feed_query {
386     my ($self, $data) = @_;
387 root 1.6
388     my ($flags, $prompt) = split /\s+/, $data, 2;
389    
390     if ($flags == 0 && $prompt =~ /^What is your name\?\s+:$/ && length $self->{user}) {
391     $self->send ("reply $self->{user}");
392     } elsif ($flags == 4 && $prompt =~ /^What is your password\?\s+:$/ && length $self->{pass}) {
393     $self->send ("reply $self->{pass}");
394     } elsif ($flags == 4 && $prompt =~ /^Please type your password again\.\s+:$/ && length $self->{pass}) {
395     $self->send ("reply $self->{pass}");
396     } else {
397     $self->query ($flags, $prompt);
398     }
399 root 1.1 }
400    
401 root 1.35 =item $conn->drawextinfo ($color, $type, $subtype, $message)
402    
403 root 1.14 =item $conn->drawinfo ($color, $text)
404    
405     =cut
406    
407 root 1.35 sub drawextinfo { }
408    
409 root 1.14 sub drawinfo { }
410    
411 root 1.35 sub feed_ExtendedTextSet {
412     my ($self, $data) = @_;
413     }
414    
415     sub feed_drawextinfo {
416     my ($self, $data) = @_;
417    
418     my ($color, $type, $subtype, $message) = split /\s+/, $data, 4;
419    
420     $self->drawextinfo ($color, $type, $subtype, $message);
421     }
422    
423 root 1.14 sub feed_drawinfo {
424     my ($self, $data) = @_;
425    
426 root 1.25 my ($flags, $text) = split / /, $data, 2;
427    
428     utf8::decode $text if utf8::valid $text;
429    
430     $self->drawinfo ($flags, $text);
431 root 1.14 }
432    
433     =item $conn->player_update ($player)
434 root 1.6
435     tag, weight, face, name
436    
437     =cut
438    
439     sub player_update { }
440    
441     sub feed_player {
442     my ($self, $data) = @_;
443    
444     my ($tag, $weight, $face, $name) = unpack "NNN C/a", $data;
445    
446     $self->player_update ($self->{player} = {
447     tag => $tag,
448     weight => $weight,
449     face => $face,
450     name => $name,
451     });
452     }
453    
454 root 1.14 =item $conn->stats_update ($stats)
455 root 1.6
456     =cut
457    
458     sub stats_update { }
459    
460 root 1.33 my %stat_32bit = map +($_ => 1),
461     CS_STAT_WEIGHT_LIM,
462     CS_STAT_SPELL_ATTUNE,
463     CS_STAT_SPELL_REPEL,
464     CS_STAT_SPELL_DENY,
465     CS_STAT_EXP;
466    
467 root 1.1 sub feed_stats {
468     my ($self, $data) = @_;
469 root 1.6
470     while (length $data) {
471     my $stat = unpack "C", substr $data, 0, 1, "";
472     my $value;
473    
474 root 1.33 if ($stat_32bit{$stat}) {
475 root 1.6 $value = unpack "N", substr $data, 0, 4, "";
476 root 1.33 } elsif ($stat == CS_STAT_SPEED || $stat == CS_STAT_WEAP_SP) {
477     $value = (1 / FLOAT_MULTF) * unpack "N", substr $data, 0, 4, "";
478     } elsif ($stat == CS_STAT_RANGE || $stat == CS_STAT_TITLE) {
479 root 1.6 my $len = unpack "C", substr $data, 0, 1, "";
480     $value = substr $data, 0, $len, "";
481 root 1.33 } elsif ($stat == CS_STAT_EXP64) {
482 root 1.31 my ($hi, $lo) = unpack "NN", substr $data, 0, 8, "";
483 root 1.6 $value = $hi * 2**32 + $lo;
484 root 1.33 } elsif (($stat >= CS_STAT_SKILLEXP_START && $stat <= CS_STAT_SKILLEXP_END)
485     || ($stat >= CS_STAT_SKILLINFO && $stat < CS_STAT_SKILLINFO + CS_NUM_SKILLS)) {
486 root 1.6 my ($level, $lo, $hi) = unpack "CNN", substr $data, 0, 9, "";
487     $value = [$level, $hi * 2**32 + $lo];
488     } else {
489 root 1.31 $value = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "";
490 root 1.6 }
491    
492     $self->{stat}{$stat} = $value;
493     }
494    
495     $self->stats_update ($self->{stat});
496 root 1.1 }
497    
498 root 1.33 =item $conn->container_add ($id, $item...)
499 root 1.14
500 root 1.33 =item $conn->container_clear ($id)
501 root 1.14
502 root 1.33 =item $conn->item_update ($item)
503 root 1.14
504 root 1.33 =item $conn->item_delete ($item...)
505 root 1.1
506 root 1.33 =cut
507 root 1.3
508 root 1.33 sub container_add { }
509     sub container_clear { }
510     sub item_delete { }
511     sub item_update { }
512 root 1.1
513 root 1.33 sub _del_items {
514     my ($self, @items) = @_;
515 root 1.6
516 root 1.33 for my $item (@items) {
517     delete $self->{item}{$item->{tag}};
518 root 1.34 $self->{container}{$item->{container}} = [
519     grep $_ != $item, @{ $self->{container}{$item->{container}} }
520     ];
521 root 1.33 }
522     }
523 root 1.6
524 root 1.33 sub feed_delinv {
525 root 1.6 my ($self, $data) = @_;
526    
527 root 1.34 $self->_del_items (@{ $self->{container}{$data} });
528 root 1.33 $self->container_clear ($data);
529 root 1.14 }
530    
531 root 1.33 sub feed_delitem {
532     my ($self, $data) = @_;
533 root 1.6
534 root 1.33 my @items = map $self->{item}{$_}, unpack "N*", $data;
535 root 1.36
536 root 1.33 $self->_del_items (@items);
537     $self->item_delete (@items);
538 root 1.6 }
539    
540 root 1.14 sub feed_item2 {
541 root 1.1 my ($self, $data) = @_;
542 root 1.14
543     my ($location, @values) = unpack "N (NNNN C/a* nC Nn)*", $data;
544    
545     my @items;
546    
547     while (@values) {
548     my ($tag, $flags, $weight, $face, $names, $anim, $animspeed, $nrof, $type) =
549     splice @values, 0, 9, ();
550    
551 root 1.39 $weight = unpack "l", pack "L", $weight; # weight can be -1
552    
553 root 1.33 utf8::decode $names if utf8::valid $names;
554     my ($name, $name_pl) = split /\x00/, $names;
555 root 1.14
556 root 1.33 my $item = {
557     container => $location,
558 root 1.14 tag => $tag,
559     flags => $flags,
560     weight => $weight,
561     face => $face,
562     name => $name,
563     name_pl => $name_pl,
564     anim => $anim,
565 root 1.38 animspeed => $animspeed * TICK,
566 root 1.14 nrof => $nrof,
567     type => $type,
568     };
569 root 1.33
570 root 1.36 if (my $prev = $self->{item}{$tag}) {
571     $self->_del_items ($prev);
572     $self->item_delete ($prev);
573     }
574    
575 root 1.33 $self->{item}{$tag} = $item;
576 root 1.34 push @{ $self->{container}{$location} }, $item;
577 root 1.33 push @items, $item;
578 root 1.14 }
579    
580 root 1.33 $self->container_add ($location, \@items);
581 root 1.1 }
582    
583 root 1.33 sub feed_upditem {
584     my ($self, $data) = @_;
585    
586 root 1.36 my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
587 root 1.33
588     my $item = $self->{item}{$tag};
589    
590     if ($flags & UPD_LOCATION) {
591     $self->item_delete ($item);
592 root 1.34 $self->{container}{$item->{container}} = [
593     grep $_ != $item, @{ $self->{container}{$item->{container}} }
594     ];
595 root 1.33
596     $item->{container} = unpack "N", substr $data, 0, 4, "";
597 root 1.14
598 root 1.34 push @{ $self->{container}{$item->{container}} }, $item;
599 root 1.33 $self->container_add ($item->{location}, $item);
600     }
601    
602 root 1.39 $item->{flags} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FLAGS;
603     $item->{weight} = unpack "l", pack "L", unpack "N", substr $data, 0, 4, "" if $flags & UPD_WEIGHT;
604     $item->{face} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FACE;
605 root 1.33
606     if ($flags & UPD_NAME) {
607     my $len = unpack "C", substr $data, 0, 1, "";
608    
609     my $names = substr $data, 0, $len, "";
610     utf8::decode $names if utf8::valid $names;
611     @$item{qw(name name_pl)} = split /\x00/, $names;
612     }
613 root 1.14
614 root 1.33 $item->{anim} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_ANIM;
615     $item->{animspeed} = TICK * unpack "C", substr $data, 0, 1, "" if $flags & UPD_ANIMSPEED;
616     $item->{nrof} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_NROF;
617 root 1.1
618 root 1.33 $self->item_update ($item);
619 root 1.29 }
620    
621     =item $conn->spell_add ($spell)
622    
623     $spell = {
624     tag => ...,
625     level => ...,
626     casting_time => ...,
627     mana => ...,
628     grace => ...,
629     damage => ...,
630     skill => ...,
631     path => ...,
632     face => ...,
633     name => ...,
634     message => ...,
635     };
636    
637     =item $conn->spell_update ($spell)
638    
639     (the default implementation calls delete then add)
640    
641     =item $conn->spell_delete ($spell)
642    
643     =cut
644    
645     sub spell_add { }
646    
647     sub spell_update {
648     my ($self, $spell) = @_;
649    
650     $self->spell_delete ($spell);
651     $self->spell_add ($spell);
652     }
653    
654     sub spell_delete { }
655    
656     sub feed_addspell {
657     my ($self, $data) = @_;
658    
659     my @data = unpack "(NnnnnnCNN C/a n/a)*", $data;
660    
661     while (@data) {
662     my $spell = {
663     tag => (shift @data),
664     level => (shift @data),
665     casting_time => (shift @data),
666     mana => (shift @data),
667     grace => (shift @data),
668     damage => (shift @data),
669     skill => (shift @data),
670     path => (shift @data),
671     face => (shift @data),
672     name => (shift @data),
673     message => (shift @data),
674     };
675    
676     $self->send ("requestinfo image_sums $spell->{face} $spell->{face}")
677     unless $self->{spell_face}[$spell->{face}]++;
678    
679     $self->spell_add ($self->{spell}{$spell->{tag}} = $spell);
680     }
681     }
682    
683     sub feed_updspell {
684     my ($self, $data) = @_;
685    
686     my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
687    
688     # only 1, 2, 4 supported
689     # completely untested
690    
691     my $spell = $self->{spell}{$tag};
692    
693 root 1.33 $spell->{mana} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_MANA;
694     $spell->{grace} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_GRACE;
695     $spell->{damage} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_DAMAGE;
696 root 1.29
697     $self->spell_update ($spell);
698     }
699    
700     sub feed_delspell {
701     my ($self, $data) = @_;
702    
703     $self->spell_delete (delete $self->{spell}{unpack "N", $data});
704 root 1.1 }
705    
706     sub feed_map1a {
707     my ($self, $data) = @_;
708    
709     my $map = $self->{map} ||= [];
710    
711 root 1.14 my ($dx, $dy) = delete @$self{qw(delayed_scroll_x delayed_scroll_y)};
712    
713     if ($dx || $dy) {
714     my ($mx, $my, $mw, $mh) = @$self{qw(mapx mapy mapw maph)};
715    
716     {
717     my @darkness;
718    
719     if ($dx > 0) {
720     push @darkness, [$mx, $my, $dx - 1, $mh];
721     } elsif ($dx < 0) {
722     push @darkness, [$mx + $mw + $dx + 1, $my, 1 - $dx, $mh];
723     }
724    
725     if ($dy > 0) {
726     push @darkness, [$mx, $my, $mw, $dy - 1];
727     } elsif ($dy < 0) {
728     push @darkness, [$mx, $my + $mh + $dy + 1, $mw, 1 - $dy];
729     }
730    
731     for (@darkness) {
732     my ($x0, $y0, $w, $h) = @$_;
733     for my $x ($x0 .. $x0 + $w) {
734     for my $y ($y0 .. $y0 + $h) {
735    
736     my $cell = $map->[$x][$y]
737     or next;
738    
739     $cell->[0] = -1;
740     }
741     }
742     }
743     }
744    
745     # now scroll
746    
747     $self->{mapx} += $dx;
748     $self->{mapy} += $dy;
749    
750     # shift in new space if moving to "negative indices"
751     if ($self->{mapy} < 0) {
752 root 1.16 unshift @$_, (undef) x -$self->{mapy} for @$map;
753 root 1.14 $self->{mapy} = 0;
754     }
755    
756     if ($self->{mapx} < 0) {
757 root 1.16 unshift @$map, (undef) x -$self->{mapx};
758 root 1.14 $self->{mapx} = 0;
759     }
760    
761     $self->map_scroll ($dx, $dy);
762     }
763    
764 root 1.1 my @dirty;
765     my ($coord, $x, $y, $darkness, $fa, $fb, $fc, $cell);
766    
767     while (length $data) {
768     $coord = unpack "n", substr $data, 0, 2, "";
769    
770 root 1.10 $x = (($coord >> 10) & 63) + $self->{mapx};
771     $y = (($coord >> 4) & 63) + $self->{mapy};
772 root 1.1
773     $cell = $map->[$x][$y] ||= [];
774    
775 root 1.10 if ($coord & 15) {
776 root 1.14 @$cell = () if $cell->[0] < 0;
777    
778 root 1.10 $cell->[0] = $coord & 8
779     ? unpack "C", substr $data, 0, 1, ""
780     : 255;
781    
782     $cell->[1] = unpack "n", substr $data, 0, 2, ""
783     if $coord & 4;
784     $cell->[2] = unpack "n", substr $data, 0, 2, ""
785     if $coord & 2;
786     $cell->[3] = unpack "n", substr $data, 0, 2, ""
787     if $coord & 1;
788     } else {
789     $cell->[0] = -1;
790     }
791 root 1.1
792     push @dirty, [$x, $y];
793     }
794    
795     $self->map_update (\@dirty);
796     }
797    
798     sub feed_map_scroll {
799     my ($self, $data) = @_;
800    
801     my ($dx, $dy) = split / /, $data;
802    
803 root 1.14 $self->{delayed_scroll_x} += $dx;
804     $self->{delayed_scroll_y} += $dy;
805 root 1.24
806     $self->map_scroll ($dx, $dy);
807 root 1.1 }
808    
809     sub feed_newmap {
810     my ($self) = @_;
811    
812     $self->{map} = [];
813     $self->{mapx} = 0;
814     $self->{mapy} = 0;
815    
816 root 1.14 delete $self->{delayed_scroll_x};
817     delete $self->{delayed_scroll_y};
818    
819 root 1.1 $self->map_clear;
820     }
821    
822 root 1.22 sub feed_mapinfo {
823     my ($self, $data) = @_;
824 root 1.24
825     my ($token, @data) = split / /, $data;
826    
827     (delete $self->{mapinfo_cb}{$token})->(@data)
828     if $self->{mapinfo_cb}{$token};
829 root 1.22
830 root 1.24 $self->map_change (@data) if $token eq "-";
831     }
832    
833     sub send_mapinfo {
834     my ($self, $data, $cb) = @_;
835    
836     my $token = ++$self->{token};
837    
838 root 1.32 $self->{mapinfo_cb}{$token} = sub {
839     $self->send_queue;
840     $cb->(@_);
841     };
842     $self->send_queue ("mapinfo $token $data");
843 root 1.22 }
844    
845 root 1.1 sub feed_image {
846     my ($self, $data) = @_;
847    
848 root 1.3 my ($num, $len, $data) = unpack "NNa*", $data;
849 root 1.1
850 root 1.22 $self->send_queue;
851 root 1.3 $self->{face}[$num]{image} = $data;
852 root 1.20 $self->face_update ($num, $self->{face}[$num]);
853 root 1.1
854 root 1.3 my @dirty;
855 root 1.2
856     for my $x (0..$self->{mapw} - 1) {
857     for my $y (0..$self->{maph} - 1) {
858     push @dirty, [$x, $y]
859     if grep $_ == $num, @{$self->{map}[$x][$y] || []};
860     }
861     }
862 root 1.6
863 root 1.2 $self->map_update (\@dirty);
864 root 1.1 }
865    
866 root 1.42 =item $conn->image_info ($numfaces, $chksum, [...image-sets])
867    
868     =cut
869    
870     sub image_info { }
871    
872 root 1.29 sub feed_replyinfo {
873     my ($self, $data) = @_;
874    
875     if ($data =~ s/^image_sums \d+ \d+ //) {
876     my ($num, $chksum, $faceset, $name) = unpack "n N C C/Z*", $data;
877    
878     $self->need_face ($num, $name, $chksum);
879 root 1.42
880     } elsif ($data =~ s/^image_info\s+//) {
881     $self->image_info (split /\n/, $data);
882    
883 root 1.29 } elsif ($data =~ s/^skill_info\s+//) {
884     for (split /\012/, $data) {
885     my ($id, $name) = split /:/, $_, 2;
886     $self->{skill_info}{$id} = $name;
887     }
888 root 1.42
889 root 1.29 } elsif ($data =~ s/^spell_paths\s+//) {
890     for (split /\012/, $data) {
891     my ($id, $name) = split /:/, $_, 2;
892     $self->{spell_paths}{$id} = $name;
893     }
894     }
895     }
896    
897 root 1.24 =item $conn->map_change ($mode, ...) [OVERWRITE]
898 root 1.22
899     current <flags> <x> <y> <width> <height> <hashstring>
900    
901     =cut
902    
903     sub map_info { }
904    
905 root 1.1 =item $conn->map_clear [OVERWRITE]
906    
907     Called whenever the map is to be erased completely.
908    
909     =cut
910    
911     sub map_clear { }
912    
913     =item $conn->map_update ([ [x,y], [x,y], ...]) [OVERWRITE]
914    
915     Called with a list of x|y coordinate pairs (as arrayrefs) for cells that
916     have been updated and need refreshing.
917    
918     =cut
919    
920     sub map_update { }
921    
922     =item $conn->map_scroll ($dx, $dy) [OVERWRITE]
923    
924     Called whenever the map has been scrolled.
925    
926     =cut
927    
928     sub map_scroll { }
929    
930 root 1.20 =item $conn->face_update ($facenum, $facedata) [OVERWRITE]
931 root 1.1
932     Called with the face number of face structure whenever a face image has
933     changed.
934    
935     =cut
936    
937     sub face_update { }
938    
939 root 1.23 =item $conn->face_find ($facenum, $facedata) [OVERWRITE]
940 root 1.3
941     Find and return the png image for the given face, or the empty list if no
942     face could be found, in which case it will be requested from the server.
943    
944     =cut
945    
946     sub face_find { }
947    
948 root 1.1 =item $conn->send ($data)
949    
950     Send a single packet/line to the server.
951    
952     =cut
953    
954     sub send {
955     my ($self, $data) = @_;
956    
957     $data = pack "na*", length $data, $data;
958    
959     syswrite $self->{fh}, $data;
960     }
961    
962 root 1.27 =item $conn->send_command ($command)
963    
964     Uses either command or ncom to send a user-level command to the
965     server. Encodes the command to UTF-8.
966    
967     =cut
968    
969 root 1.26 sub send_command {
970     my ($self, $command) = @_;
971    
972     utf8::encode $command;
973     $self->send ("command $command");
974     }
975    
976 root 1.17 sub send_queue {
977     my ($self, $cmd) = @_;
978    
979     if (defined $cmd) {
980     push @{ $self->{send_queue} }, $cmd;
981     } else {
982     --$self->{outstanding};
983     }
984    
985 root 1.19 if ($self->{outstanding} < $self->{max_outstanding} && @{ $self->{send_queue} }) {
986 root 1.17 ++$self->{outstanding};
987 root 1.32 $self->send (shift @{ $self->{send_queue} });
988 root 1.17 }
989     }
990    
991 root 1.11 sub send_setup {
992     my ($self) = @_;
993    
994     my $setup = join " ", setup => %{$self->{setup_req}},
995     mapsize => "$self->{mapw}x$self->{maph}";
996 root 1.15
997 root 1.11 $self->send ($setup);
998     }
999    
1000 root 1.1 =back
1001    
1002     =head1 AUTHOR
1003    
1004     Marc Lehmann <schmorp@schmorp.de>
1005     http://home.schmorp.de/
1006    
1007     Robin Redeker <elmex@ta-sa.org>
1008     http://www.ta-sa.org/
1009    
1010     =cut
1011    
1012     1