ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra/Deliantra/Protocol.pm
Revision: 1.40
Committed: Tue May 23 22:47:20 2006 UTC (18 years ago) by elmex
Branch: MAIN
Changes since 1.39: +0 -4 lines
Log Message:
removed a workaround

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     $self->{setup} = { split / +/, $data };
243    
244 root 1.35 if ($self->{setup}{extendedTextInfos} > 0) {
245     $self->send ("toggleextendedtext 1"); # books
246     $self->send ("toggleextendedtext 2"); # cards
247     $self->send ("toggleextendedtext 3"); # papers
248     $self->send ("toggleextendedtext 4"); # signs
249     $self->send ("toggleextendedtext 5"); # monuments
250     #$self->send ("toggleextendedtext 6"); # scripted dialogs (yeah)
251     $self->send ("toggleextendedtext 7"); # motd
252     }
253    
254 root 1.10 my ($mapw, $maph) = split /x/, $self->{setup}{mapsize};
255    
256     if ($mapw != $self->{mapw} || $maph != $self->{maph}) {
257     ($self->{mapw}, $self->{maph}) = ($mapw, $maph);
258 root 1.11 $self->send_setup;
259     } else {
260     $self->send ("addme");
261 root 1.10 }
262 root 1.1
263     $self->feed_newmap;
264     }
265    
266 root 1.35 sub feed_eof {
267     my ($self) = @_;
268    
269     delete $self->{w};
270     close delete $self->{fh};
271    
272     for my $tag (sort { $b <=> $a } %{ $self->{container} || {} }) {
273     $self->_del_items (@{ $self->{container}{$tag} });
274     $self->container_clear ($tag);
275     }
276    
277     $self->eof;
278     }
279    
280 root 1.11 sub feed_addme_success {
281     my ($self, $data) = @_;
282 root 1.30
283     $self->addme_success ($data);
284 root 1.11 }
285    
286     sub feed_addme_failure {
287     my ($self, $data) = @_;
288 root 1.30
289     $self->addme_failure ($data);
290 root 1.11 }
291    
292 root 1.35 sub logout {
293     my ($self) = @_;
294    
295     $self->{fh} or return;
296    
297     $self->feed_eof;
298     }
299    
300     sub destroy {
301     my ($self) = @_;
302    
303     $self->logout;
304    
305     %$self = ();
306     }
307    
308 root 1.14 =back
309    
310     =head2 METHODS THAT CAN/MUST BE OVERWRITTEN
311    
312     =over 4
313    
314 root 1.30 =item $self->addme_success
315    
316     =item $self->addme_failure
317    
318 root 1.35 =item $self->eof
319    
320 root 1.14 =cut
321    
322 root 1.30 sub addme_success { }
323     sub addme_failure { }
324 root 1.35 sub eof { }
325 root 1.30
326 root 1.14 sub feed_face1 {
327     my ($self, $data) = @_;
328    
329     my ($num, $chksum, $name) = unpack "nNa*", $data;
330    
331 root 1.29 $self->need_face ($num, $name, $chksum);
332     }
333    
334     sub need_face {
335     my ($self, $num, $name, $chksum) = @_;
336    
337     return if $self->{face}[$num];
338    
339 root 1.14 my $face = $self->{face}[$num] = { name => $name, chksum => $chksum };
340    
341 root 1.23 if (my $data = $self->face_find ($num, $face)) {
342 root 1.14 $face->{image} = $data;
343 root 1.20 $self->face_update ($num, $face);
344 root 1.14 } else {
345 root 1.17 $self->send_queue ("askface $num");
346 root 1.14 }
347     }
348    
349     =item $conn->anim_update ($num) [OVERWRITE]
350    
351     =cut
352    
353     sub anim_update { }
354    
355     sub feed_anim {
356     my ($self, $data) = @_;
357    
358 root 1.38 my ($num, $flags, @faces) = unpack "n*", $data;
359 root 1.14
360     $self->{anim}[$num] = \@faces;
361    
362     $self->anim_update ($num);
363     }
364    
365 root 1.28 =item $conn->sound_play ($x, $y, $soundnum, $type)
366 root 1.11
367     =cut
368    
369     sub sound_play { }
370    
371     sub feed_sound {
372     my ($self, $data) = @_;
373    
374 root 1.28 $self->sound_play (unpack "ccnC", $data);
375 root 1.11 }
376    
377 root 1.14 =item $conn->query ($flags, $prompt)
378 root 1.6
379     =cut
380    
381 root 1.14 sub query { }
382 root 1.6
383 root 1.1 sub feed_query {
384     my ($self, $data) = @_;
385 root 1.6
386     my ($flags, $prompt) = split /\s+/, $data, 2;
387    
388     if ($flags == 0 && $prompt =~ /^What is your name\?\s+:$/ && length $self->{user}) {
389     $self->send ("reply $self->{user}");
390     } elsif ($flags == 4 && $prompt =~ /^What is your password\?\s+:$/ && length $self->{pass}) {
391     $self->send ("reply $self->{pass}");
392     } elsif ($flags == 4 && $prompt =~ /^Please type your password again\.\s+:$/ && length $self->{pass}) {
393     $self->send ("reply $self->{pass}");
394     } else {
395     $self->query ($flags, $prompt);
396     }
397 root 1.1 }
398    
399 root 1.35 =item $conn->drawextinfo ($color, $type, $subtype, $message)
400    
401 root 1.14 =item $conn->drawinfo ($color, $text)
402    
403     =cut
404    
405 root 1.35 sub drawextinfo { }
406    
407 root 1.14 sub drawinfo { }
408    
409 root 1.35 sub feed_ExtendedTextSet {
410     my ($self, $data) = @_;
411     }
412    
413     sub feed_drawextinfo {
414     my ($self, $data) = @_;
415    
416     my ($color, $type, $subtype, $message) = split /\s+/, $data, 4;
417    
418     $self->drawextinfo ($color, $type, $subtype, $message);
419     }
420    
421 root 1.14 sub feed_drawinfo {
422     my ($self, $data) = @_;
423    
424 root 1.25 my ($flags, $text) = split / /, $data, 2;
425    
426     utf8::decode $text if utf8::valid $text;
427    
428     $self->drawinfo ($flags, $text);
429 root 1.14 }
430    
431     =item $conn->player_update ($player)
432 root 1.6
433     tag, weight, face, name
434    
435     =cut
436    
437     sub player_update { }
438    
439     sub feed_player {
440     my ($self, $data) = @_;
441    
442     my ($tag, $weight, $face, $name) = unpack "NNN C/a", $data;
443    
444     $self->player_update ($self->{player} = {
445     tag => $tag,
446     weight => $weight,
447     face => $face,
448     name => $name,
449     });
450     }
451    
452 root 1.14 =item $conn->stats_update ($stats)
453 root 1.6
454     =cut
455    
456     sub stats_update { }
457    
458 root 1.33 my %stat_32bit = map +($_ => 1),
459     CS_STAT_WEIGHT_LIM,
460     CS_STAT_SPELL_ATTUNE,
461     CS_STAT_SPELL_REPEL,
462     CS_STAT_SPELL_DENY,
463     CS_STAT_EXP;
464    
465 root 1.1 sub feed_stats {
466     my ($self, $data) = @_;
467 root 1.6
468     while (length $data) {
469     my $stat = unpack "C", substr $data, 0, 1, "";
470     my $value;
471    
472 root 1.33 if ($stat_32bit{$stat}) {
473 root 1.6 $value = unpack "N", substr $data, 0, 4, "";
474 root 1.33 } elsif ($stat == CS_STAT_SPEED || $stat == CS_STAT_WEAP_SP) {
475     $value = (1 / FLOAT_MULTF) * unpack "N", substr $data, 0, 4, "";
476     } elsif ($stat == CS_STAT_RANGE || $stat == CS_STAT_TITLE) {
477 root 1.6 my $len = unpack "C", substr $data, 0, 1, "";
478     $value = substr $data, 0, $len, "";
479 root 1.33 } elsif ($stat == CS_STAT_EXP64) {
480 root 1.31 my ($hi, $lo) = unpack "NN", substr $data, 0, 8, "";
481 root 1.6 $value = $hi * 2**32 + $lo;
482 root 1.33 } elsif (($stat >= CS_STAT_SKILLEXP_START && $stat <= CS_STAT_SKILLEXP_END)
483     || ($stat >= CS_STAT_SKILLINFO && $stat < CS_STAT_SKILLINFO + CS_NUM_SKILLS)) {
484 root 1.6 my ($level, $lo, $hi) = unpack "CNN", substr $data, 0, 9, "";
485     $value = [$level, $hi * 2**32 + $lo];
486     } else {
487 root 1.31 $value = unpack "s", pack "S", unpack "n", substr $data, 0, 2, "";
488 root 1.6 }
489    
490     $self->{stat}{$stat} = $value;
491     }
492    
493     $self->stats_update ($self->{stat});
494 root 1.1 }
495    
496 root 1.33 =item $conn->container_add ($id, $item...)
497 root 1.14
498 root 1.33 =item $conn->container_clear ($id)
499 root 1.14
500 root 1.33 =item $conn->item_update ($item)
501 root 1.14
502 root 1.33 =item $conn->item_delete ($item...)
503 root 1.1
504 root 1.33 =cut
505 root 1.3
506 root 1.33 sub container_add { }
507     sub container_clear { }
508     sub item_delete { }
509     sub item_update { }
510 root 1.1
511 root 1.33 sub _del_items {
512     my ($self, @items) = @_;
513 root 1.6
514 root 1.33 for my $item (@items) {
515     delete $self->{item}{$item->{tag}};
516 root 1.34 $self->{container}{$item->{container}} = [
517     grep $_ != $item, @{ $self->{container}{$item->{container}} }
518     ];
519 root 1.33 }
520     }
521 root 1.6
522 root 1.33 sub feed_delinv {
523 root 1.6 my ($self, $data) = @_;
524    
525 root 1.34 $self->_del_items (@{ $self->{container}{$data} });
526 root 1.33 $self->container_clear ($data);
527 root 1.14 }
528    
529 root 1.33 sub feed_delitem {
530     my ($self, $data) = @_;
531 root 1.6
532 root 1.33 my @items = map $self->{item}{$_}, unpack "N*", $data;
533 root 1.36
534 root 1.33 $self->_del_items (@items);
535     $self->item_delete (@items);
536 root 1.6 }
537    
538 root 1.14 sub feed_item2 {
539 root 1.1 my ($self, $data) = @_;
540 root 1.14
541     my ($location, @values) = unpack "N (NNNN C/a* nC Nn)*", $data;
542    
543     my @items;
544    
545     while (@values) {
546     my ($tag, $flags, $weight, $face, $names, $anim, $animspeed, $nrof, $type) =
547     splice @values, 0, 9, ();
548    
549 root 1.39 $weight = unpack "l", pack "L", $weight; # weight can be -1
550    
551 root 1.33 utf8::decode $names if utf8::valid $names;
552     my ($name, $name_pl) = split /\x00/, $names;
553 root 1.14
554 root 1.33 my $item = {
555     container => $location,
556 root 1.14 tag => $tag,
557     flags => $flags,
558     weight => $weight,
559     face => $face,
560     name => $name,
561     name_pl => $name_pl,
562     anim => $anim,
563 root 1.38 animspeed => $animspeed * TICK,
564 root 1.14 nrof => $nrof,
565     type => $type,
566     };
567 root 1.33
568 root 1.36 if (my $prev = $self->{item}{$tag}) {
569     $self->_del_items ($prev);
570     $self->item_delete ($prev);
571     }
572    
573 root 1.33 $self->{item}{$tag} = $item;
574 root 1.34 push @{ $self->{container}{$location} }, $item;
575 root 1.33 push @items, $item;
576 root 1.14 }
577    
578 root 1.33 $self->container_add ($location, \@items);
579 root 1.1 }
580    
581 root 1.33 sub feed_upditem {
582     my ($self, $data) = @_;
583    
584 root 1.36 my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
585 root 1.33
586     my $item = $self->{item}{$tag};
587    
588     if ($flags & UPD_LOCATION) {
589     $self->item_delete ($item);
590 root 1.34 $self->{container}{$item->{container}} = [
591     grep $_ != $item, @{ $self->{container}{$item->{container}} }
592     ];
593 root 1.33
594     $item->{container} = unpack "N", substr $data, 0, 4, "";
595 root 1.14
596 root 1.34 push @{ $self->{container}{$item->{container}} }, $item;
597 root 1.33 $self->container_add ($item->{location}, $item);
598     }
599    
600 root 1.39 $item->{flags} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FLAGS;
601     $item->{weight} = unpack "l", pack "L", unpack "N", substr $data, 0, 4, "" if $flags & UPD_WEIGHT;
602     $item->{face} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FACE;
603 root 1.33
604     if ($flags & UPD_NAME) {
605     my $len = unpack "C", substr $data, 0, 1, "";
606    
607     my $names = substr $data, 0, $len, "";
608     utf8::decode $names if utf8::valid $names;
609     @$item{qw(name name_pl)} = split /\x00/, $names;
610     }
611 root 1.14
612 root 1.33 $item->{anim} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_ANIM;
613     $item->{animspeed} = TICK * unpack "C", substr $data, 0, 1, "" if $flags & UPD_ANIMSPEED;
614     $item->{nrof} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_NROF;
615 root 1.1
616 root 1.33 $self->item_update ($item);
617 root 1.29 }
618    
619     =item $conn->spell_add ($spell)
620    
621     $spell = {
622     tag => ...,
623     level => ...,
624     casting_time => ...,
625     mana => ...,
626     grace => ...,
627     damage => ...,
628     skill => ...,
629     path => ...,
630     face => ...,
631     name => ...,
632     message => ...,
633     };
634    
635     =item $conn->spell_update ($spell)
636    
637     (the default implementation calls delete then add)
638    
639     =item $conn->spell_delete ($spell)
640    
641     =cut
642    
643     sub spell_add { }
644    
645     sub spell_update {
646     my ($self, $spell) = @_;
647    
648     $self->spell_delete ($spell);
649     $self->spell_add ($spell);
650     }
651    
652     sub spell_delete { }
653    
654     sub feed_addspell {
655     my ($self, $data) = @_;
656    
657     my @data = unpack "(NnnnnnCNN C/a n/a)*", $data;
658    
659     while (@data) {
660     my $spell = {
661     tag => (shift @data),
662     level => (shift @data),
663     casting_time => (shift @data),
664     mana => (shift @data),
665     grace => (shift @data),
666     damage => (shift @data),
667     skill => (shift @data),
668     path => (shift @data),
669     face => (shift @data),
670     name => (shift @data),
671     message => (shift @data),
672     };
673    
674     $self->send ("requestinfo image_sums $spell->{face} $spell->{face}")
675     unless $self->{spell_face}[$spell->{face}]++;
676    
677     $self->spell_add ($self->{spell}{$spell->{tag}} = $spell);
678     }
679     }
680    
681     sub feed_updspell {
682     my ($self, $data) = @_;
683    
684     my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
685    
686     # only 1, 2, 4 supported
687     # completely untested
688    
689     my $spell = $self->{spell}{$tag};
690    
691 root 1.33 $spell->{mana} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_MANA;
692     $spell->{grace} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_GRACE;
693     $spell->{damage} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_DAMAGE;
694 root 1.29
695     $self->spell_update ($spell);
696     }
697    
698     sub feed_delspell {
699     my ($self, $data) = @_;
700    
701     $self->spell_delete (delete $self->{spell}{unpack "N", $data});
702 root 1.1 }
703    
704     sub feed_map1a {
705     my ($self, $data) = @_;
706    
707     my $map = $self->{map} ||= [];
708    
709 root 1.14 my ($dx, $dy) = delete @$self{qw(delayed_scroll_x delayed_scroll_y)};
710    
711     if ($dx || $dy) {
712     my ($mx, $my, $mw, $mh) = @$self{qw(mapx mapy mapw maph)};
713    
714     {
715     my @darkness;
716    
717     if ($dx > 0) {
718     push @darkness, [$mx, $my, $dx - 1, $mh];
719     } elsif ($dx < 0) {
720     push @darkness, [$mx + $mw + $dx + 1, $my, 1 - $dx, $mh];
721     }
722    
723     if ($dy > 0) {
724     push @darkness, [$mx, $my, $mw, $dy - 1];
725     } elsif ($dy < 0) {
726     push @darkness, [$mx, $my + $mh + $dy + 1, $mw, 1 - $dy];
727     }
728    
729     for (@darkness) {
730     my ($x0, $y0, $w, $h) = @$_;
731     for my $x ($x0 .. $x0 + $w) {
732     for my $y ($y0 .. $y0 + $h) {
733    
734     my $cell = $map->[$x][$y]
735     or next;
736    
737     $cell->[0] = -1;
738     }
739     }
740     }
741     }
742    
743     # now scroll
744    
745     $self->{mapx} += $dx;
746     $self->{mapy} += $dy;
747    
748     # shift in new space if moving to "negative indices"
749     if ($self->{mapy} < 0) {
750 root 1.16 unshift @$_, (undef) x -$self->{mapy} for @$map;
751 root 1.14 $self->{mapy} = 0;
752     }
753    
754     if ($self->{mapx} < 0) {
755 root 1.16 unshift @$map, (undef) x -$self->{mapx};
756 root 1.14 $self->{mapx} = 0;
757     }
758    
759     $self->map_scroll ($dx, $dy);
760     }
761    
762 root 1.1 my @dirty;
763     my ($coord, $x, $y, $darkness, $fa, $fb, $fc, $cell);
764    
765     while (length $data) {
766     $coord = unpack "n", substr $data, 0, 2, "";
767    
768 root 1.10 $x = (($coord >> 10) & 63) + $self->{mapx};
769     $y = (($coord >> 4) & 63) + $self->{mapy};
770 root 1.1
771     $cell = $map->[$x][$y] ||= [];
772    
773 root 1.10 if ($coord & 15) {
774 root 1.14 @$cell = () if $cell->[0] < 0;
775    
776 root 1.10 $cell->[0] = $coord & 8
777     ? unpack "C", substr $data, 0, 1, ""
778     : 255;
779    
780     $cell->[1] = unpack "n", substr $data, 0, 2, ""
781     if $coord & 4;
782     $cell->[2] = unpack "n", substr $data, 0, 2, ""
783     if $coord & 2;
784     $cell->[3] = unpack "n", substr $data, 0, 2, ""
785     if $coord & 1;
786     } else {
787     $cell->[0] = -1;
788     }
789 root 1.1
790     push @dirty, [$x, $y];
791     }
792    
793     $self->map_update (\@dirty);
794     }
795    
796     sub feed_map_scroll {
797     my ($self, $data) = @_;
798    
799     my ($dx, $dy) = split / /, $data;
800    
801 root 1.14 $self->{delayed_scroll_x} += $dx;
802     $self->{delayed_scroll_y} += $dy;
803 root 1.24
804     $self->map_scroll ($dx, $dy);
805 root 1.1 }
806    
807     sub feed_newmap {
808     my ($self) = @_;
809    
810     $self->{map} = [];
811     $self->{mapx} = 0;
812     $self->{mapy} = 0;
813    
814 root 1.14 delete $self->{delayed_scroll_x};
815     delete $self->{delayed_scroll_y};
816    
817 root 1.1 $self->map_clear;
818     }
819    
820 root 1.22 sub feed_mapinfo {
821     my ($self, $data) = @_;
822 root 1.24
823     my ($token, @data) = split / /, $data;
824    
825     (delete $self->{mapinfo_cb}{$token})->(@data)
826     if $self->{mapinfo_cb}{$token};
827 root 1.22
828 root 1.24 $self->map_change (@data) if $token eq "-";
829     }
830    
831     sub send_mapinfo {
832     my ($self, $data, $cb) = @_;
833    
834     my $token = ++$self->{token};
835    
836 root 1.32 $self->{mapinfo_cb}{$token} = sub {
837     $self->send_queue;
838     $cb->(@_);
839     };
840     $self->send_queue ("mapinfo $token $data");
841 root 1.22 }
842    
843 root 1.1 sub feed_image {
844     my ($self, $data) = @_;
845    
846 root 1.3 my ($num, $len, $data) = unpack "NNa*", $data;
847 root 1.1
848 root 1.22 $self->send_queue;
849 root 1.3 $self->{face}[$num]{image} = $data;
850 root 1.20 $self->face_update ($num, $self->{face}[$num]);
851 root 1.1
852 root 1.3 my @dirty;
853 root 1.2
854     for my $x (0..$self->{mapw} - 1) {
855     for my $y (0..$self->{maph} - 1) {
856     push @dirty, [$x, $y]
857     if grep $_ == $num, @{$self->{map}[$x][$y] || []};
858     }
859     }
860 root 1.6
861 root 1.2 $self->map_update (\@dirty);
862 root 1.1 }
863    
864 root 1.29 sub feed_replyinfo {
865     my ($self, $data) = @_;
866    
867     if ($data =~ s/^image_sums \d+ \d+ //) {
868     my ($num, $chksum, $faceset, $name) = unpack "n N C C/Z*", $data;
869    
870     $self->need_face ($num, $name, $chksum);
871     } elsif ($data =~ s/^skill_info\s+//) {
872     for (split /\012/, $data) {
873     my ($id, $name) = split /:/, $_, 2;
874     $self->{skill_info}{$id} = $name;
875     }
876     } elsif ($data =~ s/^spell_paths\s+//) {
877     for (split /\012/, $data) {
878     my ($id, $name) = split /:/, $_, 2;
879     $self->{spell_paths}{$id} = $name;
880     }
881     }
882     }
883    
884 root 1.24 =item $conn->map_change ($mode, ...) [OVERWRITE]
885 root 1.22
886     current <flags> <x> <y> <width> <height> <hashstring>
887    
888     =cut
889    
890     sub map_info { }
891    
892 root 1.1 =item $conn->map_clear [OVERWRITE]
893    
894     Called whenever the map is to be erased completely.
895    
896     =cut
897    
898     sub map_clear { }
899    
900     =item $conn->map_update ([ [x,y], [x,y], ...]) [OVERWRITE]
901    
902     Called with a list of x|y coordinate pairs (as arrayrefs) for cells that
903     have been updated and need refreshing.
904    
905     =cut
906    
907     sub map_update { }
908    
909     =item $conn->map_scroll ($dx, $dy) [OVERWRITE]
910    
911     Called whenever the map has been scrolled.
912    
913     =cut
914    
915     sub map_scroll { }
916    
917 root 1.20 =item $conn->face_update ($facenum, $facedata) [OVERWRITE]
918 root 1.1
919     Called with the face number of face structure whenever a face image has
920     changed.
921    
922     =cut
923    
924     sub face_update { }
925    
926 root 1.23 =item $conn->face_find ($facenum, $facedata) [OVERWRITE]
927 root 1.3
928     Find and return the png image for the given face, or the empty list if no
929     face could be found, in which case it will be requested from the server.
930    
931     =cut
932    
933     sub face_find { }
934    
935 root 1.1 =item $conn->send ($data)
936    
937     Send a single packet/line to the server.
938    
939     =cut
940    
941     sub send {
942     my ($self, $data) = @_;
943    
944     $data = pack "na*", length $data, $data;
945    
946     syswrite $self->{fh}, $data;
947     }
948    
949 root 1.27 =item $conn->send_command ($command)
950    
951     Uses either command or ncom to send a user-level command to the
952     server. Encodes the command to UTF-8.
953    
954     =cut
955    
956 root 1.26 sub send_command {
957     my ($self, $command) = @_;
958    
959     utf8::encode $command;
960     $self->send ("command $command");
961     }
962    
963 root 1.17 sub send_queue {
964     my ($self, $cmd) = @_;
965    
966     if (defined $cmd) {
967     push @{ $self->{send_queue} }, $cmd;
968     } else {
969     --$self->{outstanding};
970     }
971    
972 root 1.19 if ($self->{outstanding} < $self->{max_outstanding} && @{ $self->{send_queue} }) {
973 root 1.17 ++$self->{outstanding};
974 root 1.32 $self->send (shift @{ $self->{send_queue} });
975 root 1.17 }
976     }
977    
978 root 1.11 sub send_setup {
979     my ($self) = @_;
980    
981     my $setup = join " ", setup => %{$self->{setup_req}},
982     mapsize => "$self->{mapw}x$self->{maph}";
983 root 1.15
984 root 1.11 $self->send ($setup);
985     }
986    
987 root 1.1 =back
988    
989     =head1 AUTHOR
990    
991     Marc Lehmann <schmorp@schmorp.de>
992     http://home.schmorp.de/
993    
994     Robin Redeker <elmex@ta-sa.org>
995     http://www.ta-sa.org/
996    
997     =cut
998    
999     1