ViewVC Help
View File | Revision Log | Show Annotations | Download File
/cvs/deliantra/Deliantra/Deliantra/Protocol.pm
Revision: 1.39
Committed: Tue May 23 20:50:14 2006 UTC (18 years ago) by root
Branch: MAIN
Changes since 1.38: +5 -3 lines
Log Message:
*** empty log message ***

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 elmex 1.37 if (($flags & UPD_WEIGHT) and ($tag == $self->{player}{tag})) {
587     return; # ignore player updates
588     }
589    
590 root 1.33 my $item = $self->{item}{$tag};
591    
592     if ($flags & UPD_LOCATION) {
593     $self->item_delete ($item);
594 root 1.34 $self->{container}{$item->{container}} = [
595     grep $_ != $item, @{ $self->{container}{$item->{container}} }
596     ];
597 root 1.33
598     $item->{container} = unpack "N", substr $data, 0, 4, "";
599 root 1.14
600 root 1.34 push @{ $self->{container}{$item->{container}} }, $item;
601 root 1.33 $self->container_add ($item->{location}, $item);
602     }
603    
604 root 1.39 $item->{flags} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FLAGS;
605     $item->{weight} = unpack "l", pack "L", unpack "N", substr $data, 0, 4, "" if $flags & UPD_WEIGHT;
606     $item->{face} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_FACE;
607 root 1.33
608     if ($flags & UPD_NAME) {
609     my $len = unpack "C", substr $data, 0, 1, "";
610    
611     my $names = substr $data, 0, $len, "";
612     utf8::decode $names if utf8::valid $names;
613     @$item{qw(name name_pl)} = split /\x00/, $names;
614     }
615 root 1.14
616 root 1.33 $item->{anim} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_ANIM;
617     $item->{animspeed} = TICK * unpack "C", substr $data, 0, 1, "" if $flags & UPD_ANIMSPEED;
618     $item->{nrof} = unpack "N", substr $data, 0, 4, "" if $flags & UPD_NROF;
619 root 1.1
620 root 1.33 $self->item_update ($item);
621 root 1.29 }
622    
623     =item $conn->spell_add ($spell)
624    
625     $spell = {
626     tag => ...,
627     level => ...,
628     casting_time => ...,
629     mana => ...,
630     grace => ...,
631     damage => ...,
632     skill => ...,
633     path => ...,
634     face => ...,
635     name => ...,
636     message => ...,
637     };
638    
639     =item $conn->spell_update ($spell)
640    
641     (the default implementation calls delete then add)
642    
643     =item $conn->spell_delete ($spell)
644    
645     =cut
646    
647     sub spell_add { }
648    
649     sub spell_update {
650     my ($self, $spell) = @_;
651    
652     $self->spell_delete ($spell);
653     $self->spell_add ($spell);
654     }
655    
656     sub spell_delete { }
657    
658     sub feed_addspell {
659     my ($self, $data) = @_;
660    
661     my @data = unpack "(NnnnnnCNN C/a n/a)*", $data;
662    
663     while (@data) {
664     my $spell = {
665     tag => (shift @data),
666     level => (shift @data),
667     casting_time => (shift @data),
668     mana => (shift @data),
669     grace => (shift @data),
670     damage => (shift @data),
671     skill => (shift @data),
672     path => (shift @data),
673     face => (shift @data),
674     name => (shift @data),
675     message => (shift @data),
676     };
677    
678     $self->send ("requestinfo image_sums $spell->{face} $spell->{face}")
679     unless $self->{spell_face}[$spell->{face}]++;
680    
681     $self->spell_add ($self->{spell}{$spell->{tag}} = $spell);
682     }
683     }
684    
685     sub feed_updspell {
686     my ($self, $data) = @_;
687    
688     my ($flags, $tag) = unpack "CN", substr $data, 0, 5, "";
689    
690     # only 1, 2, 4 supported
691     # completely untested
692    
693     my $spell = $self->{spell}{$tag};
694    
695 root 1.33 $spell->{mana} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_MANA;
696     $spell->{grace} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_GRACE;
697     $spell->{damage} = unpack "n", substr $data, 0, 2, "" if $flags & UPD_SP_DAMAGE;
698 root 1.29
699     $self->spell_update ($spell);
700     }
701    
702     sub feed_delspell {
703     my ($self, $data) = @_;
704    
705     $self->spell_delete (delete $self->{spell}{unpack "N", $data});
706 root 1.1 }
707    
708     sub feed_map1a {
709     my ($self, $data) = @_;
710    
711     my $map = $self->{map} ||= [];
712    
713 root 1.14 my ($dx, $dy) = delete @$self{qw(delayed_scroll_x delayed_scroll_y)};
714    
715     if ($dx || $dy) {
716     my ($mx, $my, $mw, $mh) = @$self{qw(mapx mapy mapw maph)};
717    
718     {
719     my @darkness;
720    
721     if ($dx > 0) {
722     push @darkness, [$mx, $my, $dx - 1, $mh];
723     } elsif ($dx < 0) {
724     push @darkness, [$mx + $mw + $dx + 1, $my, 1 - $dx, $mh];
725     }
726    
727     if ($dy > 0) {
728     push @darkness, [$mx, $my, $mw, $dy - 1];
729     } elsif ($dy < 0) {
730     push @darkness, [$mx, $my + $mh + $dy + 1, $mw, 1 - $dy];
731     }
732    
733     for (@darkness) {
734     my ($x0, $y0, $w, $h) = @$_;
735     for my $x ($x0 .. $x0 + $w) {
736     for my $y ($y0 .. $y0 + $h) {
737    
738     my $cell = $map->[$x][$y]
739     or next;
740    
741     $cell->[0] = -1;
742     }
743     }
744     }
745     }
746    
747     # now scroll
748    
749     $self->{mapx} += $dx;
750     $self->{mapy} += $dy;
751    
752     # shift in new space if moving to "negative indices"
753     if ($self->{mapy} < 0) {
754 root 1.16 unshift @$_, (undef) x -$self->{mapy} for @$map;
755 root 1.14 $self->{mapy} = 0;
756     }
757    
758     if ($self->{mapx} < 0) {
759 root 1.16 unshift @$map, (undef) x -$self->{mapx};
760 root 1.14 $self->{mapx} = 0;
761     }
762    
763     $self->map_scroll ($dx, $dy);
764     }
765    
766 root 1.1 my @dirty;
767     my ($coord, $x, $y, $darkness, $fa, $fb, $fc, $cell);
768    
769     while (length $data) {
770     $coord = unpack "n", substr $data, 0, 2, "";
771    
772 root 1.10 $x = (($coord >> 10) & 63) + $self->{mapx};
773     $y = (($coord >> 4) & 63) + $self->{mapy};
774 root 1.1
775     $cell = $map->[$x][$y] ||= [];
776    
777 root 1.10 if ($coord & 15) {
778 root 1.14 @$cell = () if $cell->[0] < 0;
779    
780 root 1.10 $cell->[0] = $coord & 8
781     ? unpack "C", substr $data, 0, 1, ""
782     : 255;
783    
784     $cell->[1] = unpack "n", substr $data, 0, 2, ""
785     if $coord & 4;
786     $cell->[2] = unpack "n", substr $data, 0, 2, ""
787     if $coord & 2;
788     $cell->[3] = unpack "n", substr $data, 0, 2, ""
789     if $coord & 1;
790     } else {
791     $cell->[0] = -1;
792     }
793 root 1.1
794     push @dirty, [$x, $y];
795     }
796    
797     $self->map_update (\@dirty);
798     }
799    
800     sub feed_map_scroll {
801     my ($self, $data) = @_;
802    
803     my ($dx, $dy) = split / /, $data;
804    
805 root 1.14 $self->{delayed_scroll_x} += $dx;
806     $self->{delayed_scroll_y} += $dy;
807 root 1.24
808     $self->map_scroll ($dx, $dy);
809 root 1.1 }
810    
811     sub feed_newmap {
812     my ($self) = @_;
813    
814     $self->{map} = [];
815     $self->{mapx} = 0;
816     $self->{mapy} = 0;
817    
818 root 1.14 delete $self->{delayed_scroll_x};
819     delete $self->{delayed_scroll_y};
820    
821 root 1.1 $self->map_clear;
822     }
823    
824 root 1.22 sub feed_mapinfo {
825     my ($self, $data) = @_;
826 root 1.24
827     my ($token, @data) = split / /, $data;
828    
829     (delete $self->{mapinfo_cb}{$token})->(@data)
830     if $self->{mapinfo_cb}{$token};
831 root 1.22
832 root 1.24 $self->map_change (@data) if $token eq "-";
833     }
834    
835     sub send_mapinfo {
836     my ($self, $data, $cb) = @_;
837    
838     my $token = ++$self->{token};
839    
840 root 1.32 $self->{mapinfo_cb}{$token} = sub {
841     $self->send_queue;
842     $cb->(@_);
843     };
844     $self->send_queue ("mapinfo $token $data");
845 root 1.22 }
846    
847 root 1.1 sub feed_image {
848     my ($self, $data) = @_;
849    
850 root 1.3 my ($num, $len, $data) = unpack "NNa*", $data;
851 root 1.1
852 root 1.22 $self->send_queue;
853 root 1.3 $self->{face}[$num]{image} = $data;
854 root 1.20 $self->face_update ($num, $self->{face}[$num]);
855 root 1.1
856 root 1.3 my @dirty;
857 root 1.2
858     for my $x (0..$self->{mapw} - 1) {
859     for my $y (0..$self->{maph} - 1) {
860     push @dirty, [$x, $y]
861     if grep $_ == $num, @{$self->{map}[$x][$y] || []};
862     }
863     }
864 root 1.6
865 root 1.2 $self->map_update (\@dirty);
866 root 1.1 }
867    
868 root 1.29 sub feed_replyinfo {
869     my ($self, $data) = @_;
870    
871     if ($data =~ s/^image_sums \d+ \d+ //) {
872     my ($num, $chksum, $faceset, $name) = unpack "n N C C/Z*", $data;
873    
874     $self->need_face ($num, $name, $chksum);
875     } elsif ($data =~ s/^skill_info\s+//) {
876     for (split /\012/, $data) {
877     my ($id, $name) = split /:/, $_, 2;
878     $self->{skill_info}{$id} = $name;
879     }
880     } elsif ($data =~ s/^spell_paths\s+//) {
881     for (split /\012/, $data) {
882     my ($id, $name) = split /:/, $_, 2;
883     $self->{spell_paths}{$id} = $name;
884     }
885     }
886     }
887    
888 root 1.24 =item $conn->map_change ($mode, ...) [OVERWRITE]
889 root 1.22
890     current <flags> <x> <y> <width> <height> <hashstring>
891    
892     =cut
893    
894     sub map_info { }
895    
896 root 1.1 =item $conn->map_clear [OVERWRITE]
897    
898     Called whenever the map is to be erased completely.
899    
900     =cut
901    
902     sub map_clear { }
903    
904     =item $conn->map_update ([ [x,y], [x,y], ...]) [OVERWRITE]
905    
906     Called with a list of x|y coordinate pairs (as arrayrefs) for cells that
907     have been updated and need refreshing.
908    
909     =cut
910    
911     sub map_update { }
912    
913     =item $conn->map_scroll ($dx, $dy) [OVERWRITE]
914    
915     Called whenever the map has been scrolled.
916    
917     =cut
918    
919     sub map_scroll { }
920    
921 root 1.20 =item $conn->face_update ($facenum, $facedata) [OVERWRITE]
922 root 1.1
923     Called with the face number of face structure whenever a face image has
924     changed.
925    
926     =cut
927    
928     sub face_update { }
929    
930 root 1.23 =item $conn->face_find ($facenum, $facedata) [OVERWRITE]
931 root 1.3
932     Find and return the png image for the given face, or the empty list if no
933     face could be found, in which case it will be requested from the server.
934    
935     =cut
936    
937     sub face_find { }
938    
939 root 1.1 =item $conn->send ($data)
940    
941     Send a single packet/line to the server.
942    
943     =cut
944    
945     sub send {
946     my ($self, $data) = @_;
947    
948     $data = pack "na*", length $data, $data;
949    
950     syswrite $self->{fh}, $data;
951     }
952    
953 root 1.27 =item $conn->send_command ($command)
954    
955     Uses either command or ncom to send a user-level command to the
956     server. Encodes the command to UTF-8.
957    
958     =cut
959    
960 root 1.26 sub send_command {
961     my ($self, $command) = @_;
962    
963     utf8::encode $command;
964     $self->send ("command $command");
965     }
966    
967 root 1.17 sub send_queue {
968     my ($self, $cmd) = @_;
969    
970     if (defined $cmd) {
971     push @{ $self->{send_queue} }, $cmd;
972     } else {
973     --$self->{outstanding};
974     }
975    
976 root 1.19 if ($self->{outstanding} < $self->{max_outstanding} && @{ $self->{send_queue} }) {
977 root 1.17 ++$self->{outstanding};
978 root 1.32 $self->send (shift @{ $self->{send_queue} });
979 root 1.17 }
980     }
981    
982 root 1.11 sub send_setup {
983     my ($self) = @_;
984    
985     my $setup = join " ", setup => %{$self->{setup_req}},
986     mapsize => "$self->{mapw}x$self->{maph}";
987 root 1.15
988 root 1.11 $self->send ($setup);
989     }
990    
991 root 1.1 =back
992    
993     =head1 AUTHOR
994    
995     Marc Lehmann <schmorp@schmorp.de>
996     http://home.schmorp.de/
997    
998     Robin Redeker <elmex@ta-sa.org>
999     http://www.ta-sa.org/
1000    
1001     =cut
1002    
1003     1